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