使用Intellij IDEA 编写一个Android Studio插件

本文记录了使用Intellij idea 编写插件的过程

背景

最近在练习Android的的自定义View,遇到把attrs.xml文件的属性设置到代码里的时候,一大堆的代码写得我心烦(其实是因为懒),我就想到可以自动化导入,就像LayoutCreator这个插件一样。

项目分析

  • 在自定义view里根据选择的declare-styleable name来在attrs.xml文件里选择对应的配置。
  • 根据获取的配置名和配置的类型来自动导入代码
    例如:
    我选择了TopBar这个declare-styleable名。我就要在attrs.xml里找到它的配置并自动生成变量和代码

    自动生成的效果如下

搜集资料

毕竟我也是第一次
以下是我搜集到的资料,这个插件的实现离不开这些作者的文章,事实上这篇文章也参考了以下的资料。

正式开始

首先要知道Android studio是基于Intellij Platform开发出来的,和Intellij IDEA是一家。不管是Intelllij IDEA的插件还是Android studio的插件都需要在Intellij IDEA里开发。

1. 在Intellij IDEA里新建一个插件项目

在这一步需要导入两个sdk,一个是java的sdk,一个是Intellij PLatform的sdk。
前者就选择你的java sdk安装目录,我的是C:\Program Files\Java\jdk1.8.0_121
后者就是你的Intellij IDEA安装目录,我的是D:\IntelliJ IDEA Community Edition 15.0

2.新建完之后项目里会出现这几个文件夹


plugin.xml里是这个插件的基本信息和配置,里面代表的信息如下
id:这里填写插件id,比如我的是com.mran.plugin.lazyattrs
name:这里是插件的名字,比如我的是lazyattrs
version:这个是插件的版本
vendor:你的联系方式,email,网址
description:关于这个插件的介绍
change-notes:版本更新信息
actions:这里的内容决定了你的插件将会在哪里出现。

src文件夹主要是放插件代码

3.新建一个代码文件。

右键单击src文件夹-->new-->Action

这个是新建Action的配置,分别是
Action id,
Class name(其实就是生成的java类的名字)
name
Description。
下面的Add to Group决定了插件出现的位置,比如我选择了GenerateGroup(Generate),javaGenerateGroup1(), fitst,这样它就会在你按alt+insert的时候出来,而且还是在第一个位置。如图(图中的我设置的是last,最后一个位置)

下面还有KeyBoard ShortCuts,就是设置快捷键。
填写完之后,点击ok。就会在src文件夹下生成一个以设置的Class name为名的一个文件,同时还会在plugin.xml里写入这些配置

4.编写代码

先观察生成的代码文件

public class LazyattrsAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here
    }
}

当这插件被触发时就会执行actionPerformed()里的方法,所以我们就要把代码写进这个方法里。

4.1 获得在代码编辑器里获得选择到的文字。

 //获取选择的文字
    private String getSelectWord(AnActionEvent e) {
        Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
        if (null != mEditor) {

            SelectionModel model = mEditor.getSelectionModel();
            final String selectedText = model.getSelectedText();
            if (!TextUtils.isEmpty(selectedText)) {
                return selectedText;
            }
        }
        return "";

    }

如果你也需要获取选择的文字,这段代码可以直接使用,事实上,这段代码也是我从别处参考并稍作更改的。

4.2 根据获取到的文字来找到attrs.xml里的对应的declare-styleable

这一步还可以分解成两步
 1.找到attrs.xml文件
 2.解析这个文件

4.2.1 找到attrs.xml文件

Intellij PLatform SDK为我们提供了一个FilenameIndex.getFilesByName方法

    //获取attr文件
    private XmlFile getFile(Project project) {
        PsiFile[] mPsiFiles = FilenameIndex.getFilesByName(project, "attrs.xml", GlobalSearchScope.projectScope(project));
        if (mPsiFiles.length <= 0) {
            return null;
        }
        return (XmlFile) mPsiFiles[0];
    }

第一个参数是当前项目,可以通过e.getProject()得到
第二个参数是需要找的文件名字
第三个参数是搜索的范围,这里的搜索范围我指定的是project,我之前使用的是GlobalSearchScope.allScope(project)),在Intellij IDEA里能正常工作,但是在Android studio里智能找到同一个文件夹下的文件,换成了GlobalSearchScope.projectScope(project)之后就能正常工作了。
这个方法返回的找到的所有匹配文件,一个PsiFile类型的数组。最后为了我们方便解析,还要把它转换成XmlFile类型。

4.2.2 解析xml文件
//对文件进行解析
    private List<ElementWrapper> getAttrs(XmlFile xmlFile) {

        if (xmlFile.getRootTag() == null) {
            return null;
        }
        XmlTag xmlTags[] = xmlFile.getRootTag().getSubTags();
        List<ElementWrapper> elementWrappers = new ArrayList<ElementWrapper>();
        for (XmlTag x1 : xmlTags) {
            //解析到 <declare-styleable name="TopBar">
            ElementWrapper elementWrapper = new ElementWrapper();
            //解析style名
            elementWrapper.setStyleName(x1.getAttributeValue("name"));
            XmlTag xmlTag2[] = x1.getSubTags();
            List<MyElement> elements = new ArrayList<MyElement>();
            //解析到  <attr name="title" format="string"/>
            for (XmlTag x2 : xmlTag2) {
                MyElement myElement = new MyElement();
                //解析配置名和配置对应的数据格式
                myElement.setName(x2.getAttributeValue("name"));
                myElement.setFormat(x2.getAttributeValue("format"));
                elements.add(myElement);
            }
            elementWrapper.setElements(elements);
            elementWrappers.add(elementWrapper);
        }
        return elementWrappers;
    }

这里的返回值是List<ElementWrapper>,其中的ElementWrapepr是我自定义的数据类型.
因为这个文件是xml格式的,用过java的jsoup库或者是python的beautifulsoup库的人对于解析会比较熟悉.
先用

if (xmlFile.getRootTag() == null) {
            return null;
        }

获取根节点,判断是否为空,再进行下一步.
在用getSubTags()获取直接子节点.
比如这个xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string"/>
        <attr name="titleSize" format="dimension"/>
        <attr name="titleColor" format="color"/>
        <attr name="leftText" format="string"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftTextSize" format="dimension"/>
        <attr name="leftBackGround" format="reference|color"/>
        <attr name="rightText" format="string"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="rightTextSize" format="dimension"/>
        <attr name="rightBackGround" format="reference|color"/>
    </declare-styleable>
</resources>

getRootTag()获得的就是<resources>这一层的内容,getSubTags()就是获得<resource>的直接子节点,也就是declare-styleable,再次对declare-styleable这一节点使用getSubTags()就是获得它的直接子节点,也就是

        <attr name="title" format="string"/>
        <attr name="titleSize" format="dimension"/>
        <attr name="titleColor" format="color"/>
        <attr name="leftText" format="string"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftTextSize" format="dimension"/>
        <attr name="leftBackGround" format="reference|color"/>
        <attr name="rightText" format="string"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="rightTextSize" format="dimension"/>
        <attr name="rightBackGround" format="reference|color"/>

使用起来还是很简单哒.
如果要获得这一节点的内容,可以使用getAttributeValue(name),比如我对declare-styleable这一节点使用getAttributeValue("name)获得结果就是"TopBar".很简单哒.
不过要注意一点的就是,每次获取最好都判断一下非空.
官方给我们提供了另一种思路.XML DOM API看这个会详细很多.

4.3将代码写入

还是将问题拆分一下
1.找到要写入的类
2.根据配置生成要写入的代码
3.写入文件的相应位置.

4.3.1 找到要写入的类
   private PsiClass getWriteClass(AnActionEvent event) {
        final Project project = event.getProject();
        PsiFile psiFile;
        PsiClass psiClass = null;
        Editor editor = event.getData(PlatformDataKeys.EDITOR);
        if (project != null) {
            Document document;
            if (editor != null) {
                document = editor.getDocument();
                psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
                if ((psiFile) != null) {
                    psiClass = ((PsiJavaFile) psiFile).getClasses()[0];
                }
            }
        }
        return psiClass;
    }

刚开始也不知怎么找到当前编辑的类,后来去社区搜索,才弄清楚了.

4.3.2生成代码

生成代码的时候要注意,不要在ui线程,也就是主线程写入,要另外开一个线程.
比如

 //写入代码
    private void writeToClass(AnActionEvent e) {
        final Project project = e.getProject();
        final PsiClass psiClass;
        psiClass = getWriteClass(e);
        if (psiClass != null)
            new WriteCommandAction.Simple(project) {
                @Override
                protected void run() throws Throwable {

                    write(psiClass, project);
                }
            }.execute();


    }

生成变量.

    //写入变量
    private void writeField(PsiClass psiClass, PsiElementFactory psiElementFactory) {
        //写入生成的变量
        for (ElementWrapper e : elementWrappers) {
            if (e.getStyleName().equals(styleableName))
                for (MyElement m : e.getElements()) {
                    String type = "";
                    switch (m.getFormat()) {
                        case Costant.STRING:
                            type = "String ";
                            break;
                        case Costant.BOOL:
                            type = "boolean ";
                            break;
                        case Costant.COLOR:
                            type = "int ";
                            break;
                        case Costant.DIMENSION:
                            type = "float ";
                            break;
                    }
                    psiClass.add(psiElementFactory.createFieldFromText(type + m.getName() + ";\n", psiClass));
                }
        }
    }

可以看到关键的写入是这一句psiClass.add(psiElementFactory.createFieldFromText(type + m.getName() + ";\n", psiClass));
其中的psiElementFactory.createFieldFromText(String,PsiElement),还有另外一个同样功能的方法psiElementFactory.createField(String,PsiType)这俩方法的不同之处在于前者是直接写入String里的内容,后者是根据PsiType的值来确定一个类型.我建议使用前者,比较直观一点.

然后是生成方法

    //写入方法
    private void writeMethod(PsiClass psiClass, PsiElementFactory psiElementFactory) {
        //找到要写入的方法.
        PsiMethod psiMethod[] = psiClass.findMethodsByName("getAttrs", false);
        PsiMethod psiMethod1;
        //不存在就创建一个.
        if (psiMethod.length == 0) {
            psiMethod1 = (PsiMethod) psiClass.add(psiElementFactory.createMethod("getAttrs", PsiType.VOID));
        } else
            psiMethod1 = psiMethod[0];
        //写入代码
        for (ElementWrapper e : elementWrappers) {
            if (e.getStyleName().equals(styleableName))
                for (MyElement m : e.getElements()) {
                    psiMethod1.getBody().add(psiElementFactory.createStatementFromText(getStatement(m), psiClass));
                }
        }
    }

要先确定这个方法是否存在,用psiClass.findMethodsByName("getAttrs", false);
不存在就创建一个,psiClass.add(psiElementFactory.createMethod("getAttrs", PsiType.VOID));
这个psiElementFactory.createMethod(String,PsiType)同样也可以用psiElementFactory.createMethodFromText(String,PsiElement).
然后是在这个方法内部写入方法,用psiMethod1.getBody().add(psiElementFactory.createStatementFromText(getStatement(m), psiClass));需要注意的是(psiElementFactory.createStatementFromText(String,PsiElement)中的String,必须是一个语句,不能包含多个语句.

写入之后就完成了插件的编写

5.测试

点击Intellij IDEA的运行,会自动开启一个已经安装好刚刚编写的插件的新IDEA窗口,在里面可以测试你的插件是否已经正确运行.当然下断点进行Debug也是可以的.

6.最后生成一个jar安装包

点击Build->Prepare Plugin Module'name' ForDeployment,生成安装包.这样就可以在Android studio里安装运行了

最后

完整的代码在这里LazyAttrs
这篇文章只是记录我编写这个插件的过程,算是以完成目的为导向,有很多内容没有说到.Intellij Platform SDK有很多还没有了解过,我遇到的很多问题都是通过在上面的开发者社区搜索到的.
如果有幸帮到你,那是最好不过.
下台鞠躬.