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

1,322 阅读8分钟

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

背景

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

项目分析

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

    自动生成的效果如下

    搜集资料

    毕竟我也是第一次
    以下是我搜集到的资料,这个插件的实现离不开这些作者的文章,事实上这篇文章也参考了以下的资料。
  • Intellij IDEA插件开发(一)快速入门
  • 动手试试Android Studio插件开发
  • 学会编写Android Studio插件 别停留在用的程度了
    官方文档
  • Intellij IDEA插件开发者社区
  • Intellij IDEA插件开发文档

    正式开始

    首先要知道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有很多还没有了解过,我遇到的很多问题都是通过在上面的开发者社区搜索到的.
如果有幸帮到你,那是最好不过.
下台鞠躬.

上一篇:模仿知乎安卓客户端的banner广告条以及一些思考

下一篇:使用Glide来自定义加载数据过程core