您的位置:首页 > 移动开发 > Android开发

动手试试Android Studio插件开发

2016-11-20 22:54 295 查看
原博客:http://blog.csdn.net/zhangke3016/article/details/53245530由于业务关系,经常需要写一些表单页面,基本也就是简单的增删改查然后上传,做过几个页面之后就有点想偷懒了,这么低水平重复性的体力劳动,能不能用什么办法自动生成呢,查阅相关资料,发现Android studio插件正好可以满足需求,在Github上搜了一下,找到BorePlugin这个帮助自动生成布局代码的插件挺不错的,在此基础上修改为符合自己需求的插件,整体效果还不错。 
发现了android studio插件的魅力,自己也总结一下,也给小伙伴们提供一点参考,今天就以实现自动生成
findviewbyid
代码插件的方式来个简单的总结。这里就不写行文思路了,一切从0开始,一步一步搭建起这个插件项目吧。效果如下:

一、搭建环境

由于android studio是基于Intellij IDEA开发的,但Android Studio自身不具备开发插件的功能,所以插件开发需要在IntelliJ IDEA上开发。 
好了,说了这么多,开始去官网下载吧,下载地址:https://www.jetbrains.com/idea/ 
安装运行后我们就可以开始开发了。 
创建项目

创建成功之后的文件夹是这个样子的:

我们重点关注plugin.xml和src,plugin.xml是我们这个插件项目的配置说明,类似于android开发中的AndroidManifest.xml文件,用于配置信息的注册和声明。
<idea-plugin version="2">
<id>com.your.company.unique.plugin.id</id>
<name>Plugin display name here</name>
<version>1.0</version>
<vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>

<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>

<change-notes><![CDATA[
Add change notes here.<br>
<em>most HTML tags may be used</em>
]]>
</change-notes>

<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description -->
<idea-version since-build="141.0"/>

<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html on how to target different products -->
<!-- uncomment to enable plugin in all products
<depends>com.intellij.modules.lang</depends>
-->

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>

<actions>
<!-- Add your actions here -->
</actions>

</idea-plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
来简单介绍下这个XML配置文件: 
id:插件的ID,保证插件的唯一性,如果上传仓库的话。 
name:插件名称。 
version:版本号。 
description:插件的简介。 
change-notes:版本更新信息。 
extensions:扩展组件注册 。 
actions:Action注册,比如在某个菜单下增加一个按钮就要在这注册。

二、开始编码

1、编写菜单选项,用于触发我们的插件。


好了,现在我们要用到很关键的一个类:AnAction,选择new->Action就可以创建:



ActionID:代表该Action的唯一的ID 
ClassName:类名 
Name:插件在菜单上的名称 
Description:对这个Action的描述信息 
Groups:定义这个菜单选项出现的位置,比如图中设置当点击菜单栏Edit时,第一项会出现GenerateCode的选项,右边的Anchor是选择该选项出现的位置,默认First即最顶部。 
之后会出现我们创建的GenerateCodeAction类:
public class GenerateCodeAction extends AnAction {


@Override

public void actionPerformed(AnActionEvent e) {

// TODO: insert action logic here

}

}
1
2
3
4
5
6
7
1
2
3
4
5
6
7
plugin.xml
中也多了一段代码:
<action id="HelloWorld.TestGenerateCodeAction" class="com.example.helloworld.GenerateCodeAction" text="GenerateCode"
description="generate findviewbyid code ">
<add-to-group group-id="CodeMenu" anchor="first"/>
<keyboard-shortcut keymap="$default" first-keystroke="meta I"/>
</action>
1
2
3
4
5
1
2
3
4
5
这样,一个菜单选项就完成了,接下来就该实现当用户点击GenerateCode菜单或者按快捷键Command+ M后的功能代码了。

2、实现功能逻辑代码

在实现功能逻辑之前,我们要先理清需求,首先我们是想在选中布局文件的时候,自动解析布局文件并生成
findviewbyid
代码。那我们主要关注三个点就可以了。1、如何获取布局文件 
2、如何解析布局文件 
3、如何根据将代码写入文件1、如何获取布局文件 
为简单起见,我们这里通过让用户自己输入布局文件的方式通过
FilenameIndex.getFilesByName
方法来查找布局文件。 
查找文件我们要用到
PsiFile
类,官方文档给我们的提供了几种方式:
From an action:
e.getData(LangDataKeys.PSI_FILE).
From a VirtualFile:
PsiManager.getInstance(project).findFile()
From a Document:
PsiDocumentManager.getInstance(project).getPsiFile()
From an element inside the file:
psiElement.getContainingFile()
To find files with a specific name anywhere in the project, use :
FilenameIndex.getFilesByName(project, name, scope)
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
这里使用最后一种方式来获取图片,获取用户选中的布局文件,如果用户没有选中内容,通过在状态栏弹窗提示:
public static void showNotification(Project project, MessageType type, String text) {
StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);

JBPopupFactory.getInstance()
.createHtmlTextBalloonBuilder(text, type, null)
.setFadeoutTime(7500)
.createBalloon()
.show(RelativePoint.getCenterOf(statusBar.getComponent()), Balloon.Position.atRight);
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
获取用户选中内容:
@Override
public void actionPerformed(AnActionEvent e) {

Project project = e.getProject();
Editor editor = e.getData(PlatformDataKeys.EDITOR);
if (null == editor) {
return;
}

SelectionModel model = editor.getSelectionModel();
//获取选中内容
final String selectedText = model.getSelectedText();
if (TextUtils.isEmpty(selectedText)) {
Utils.showNotification(project,MessageType.ERROR,"请选中生成内容");
return;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


获取XML文件:
PsiFile[] mPsiFiles = FilenameIndex.getFilesByName(project, selectedText+".xml", GlobalSearchScope.allScope(project));
if (mPsiFiles.length<=0){
Utils.showNotification(project,MessageType.INFO,"所输入的布局文件没有找到!");
return;
}
XmlFile xmlFile =  (XmlFile) mPsiFiles[0];
1
2
3
4
5
6
1
2
3
4
5
6
至此,布局文件获取到了,我们开始下一步,解析布局文件啦。 
2、如何解析布局文件 
关于文件操作,官方文档是这样写的:Most interesting modification operations are performed on the level of individual PSI elements, not files as a whole. 
To iterate over the elements in a file, use 
psiFile.accept(new PsiRecursiveElementWalkingVisitor()…);我们这里通过file.accept(new XmlRecursiveElementVisitor())方法对XML文件进行解析:
public static ArrayList<Element> getIDsFromLayout(final PsiFile file, final ArrayList<Element> elements) {
file.accept(new XmlRecursiveElementVisitor() {

@Override
public void visitElement(final PsiElement element) {
super.visitElement(element);
//解析XML标签
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag) element;
//解析include标签
if (tag.getName().equalsIgnoreCase("include")) {
XmlAttribute layout = tag.getAttribute("layout", null);

if (layout != null) {
Project project = file.getProject();
//                            PsiFile include = findLayoutResource(file, project, getLayoutName(layout.getValue()));
PsiFile include = null;
PsiFile[] mPsiFiles = FilenameIndex.getFilesByName(project, getLayoutName(layout.getValue())+".xml", GlobalSearchScope.allScope(project));
if (mPsiFiles.length>0){
include = mPsiFiles[0];
}

if (include != null) {
getIDsFromLayout(include, elements);

return;
}
}
}

// get element ID
XmlAttribute id = tag.getAttribute("android:id", null);
if (id == null) {
return; // missing android:id attribute
}
String value = id.getValue();
if (value == null) {
return; // empty value
}

// check if there is defined custom class
String name = tag.getName();
XmlAttribute clazz = tag.getAttribute("class", null);
if (clazz != null) {
name = clazz.getValue();
}

try {
Element e = new Element(name, value, tag);
elements.add(e);
} catch (IllegalArgumentException e) {
// TODO log
}
}
}
});

return elements;
}

public static String getLayoutName(String layout) {
if (layout == null || !layout.startsWith("@") || !layout.contains("/")) {
return null; // it's not layout identifier
}

String[] parts = layout.split("/");
if (parts.length != 2) {
return null; // not enough parts
}

return parts[1];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
以及实体类Element:
package com.example.helloworld.entity;

import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Element {

// constants
private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
private static final Pattern sValidityPattern = Pattern.compile("^([a-zA-Z_\\$][\\w\\$]*)$", Pattern.CASE_INSENSITIVE);
public String id;
public boolean isAndroidNS = false;
public String nameFull; // element mClassName with package
public String name; // element mClassName
public int fieldNameType = 1; // 1 aa_bb_cc; 2 aaBbCc 3 mAaBbCc
public boolean isValid = false;
public boolean used = true;
public boolean isClickable = false; // Button, view_having_clickable_attr etc.
public boolean isItemClickable = false; // ListView, GridView etc.
public boolean isEditText = false; // EditText
public XmlTag xml;

//GET SET mClassName
public String strGetMethodName;
public String strSetMethodName;

/**
* Constructs new element
*
* @param name Class mClassName of the view
* @param id   Value in android:id attribute
* @throws IllegalArgumentException When the arguments are invalid
*/
public Element(String name, String id, XmlTag xml) {
// id
final Matcher matcher = sIdPattern.matcher(id);
if (matcher.find() && matcher.groupCount() > 1) {
this.id = matcher.group(2);

String androidNS = matcher.group(1);
this.isAndroidNS = !(androidNS == null || androidNS.length() == 0);
}

if (this.id == null) {
throw new IllegalArgumentException("Invalid format of view id");
}

// mClassName
String[] packages = name.split("\\.");
if (packages.length > 1) {
this.nameFull = name;
this.name = packages[packages.length - 1];
} else {
this.nameFull = null;
this.name = name;
}

this.xml = xml;

// clickable
XmlAttribute clickable = xml.getAttribute("android:clickable", null);
boolean hasClickable = clickable != null &&
clickable.getValue() != null &&
clickable.getValue().equals("true");
String xmlName = xml.getName();
if (xmlName.contains("RadioButton")) {
// TODO check
} else {
if ((xmlName.contains("ListView") || xmlName.contains("GridView")) && hasClickable) {
isItemClickable = true;
} else if (xmlName.contains("Button") || hasClickable) {
isClickable = true;
}
}

// isEditText
isEditText = xmlName.contains("EditText");
}

/**
* Create full ID for using in layout XML files
*
* @return
*/
public String getFullID() {
StringBuilder fullID = new StringBuilder();
String rPrefix;

if (isAndroidNS) {
rPrefix = "android.R.id.";
} else {
rPrefix = "R.id.";
}

fullID.append(rPrefix);
fullID.append(id);

return fullID.toString();
}

/**
* Generate field mClassName if it's not done yet
*
* @return
*/
public String getFieldName() {
String fieldName = id;
String[] names = id.split("_");
if (fieldNameType == 2) {
// aaBbCc
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.length; i++) {
if (i == 0) {
sb.append(names[i]);
} else {
sb.append(firstToUpperCase(names[i]));
}
}
fieldName = sb.toString();
} else if (fieldNameType == 3) {
// mAaBbCc
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.length; i++) {
if (i == 0) {
sb.append("m");
}
sb.append(firstToUpperCase(names[i]));
}
fieldName = sb.toString();
}
return fieldName;
}

/**
* Check validity of field mClassName
*
* @return
*/
public boolean checkValidity() {
Matcher matcher = sValidityPattern.matcher(getFieldName());
isValid = matcher.find();

return isValid;
}
public static String firstToUpperCase(String key) {
return key.substring(0, 1).toUpperCase(Locale.CHINA) + key.substring(1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
一些有用的方法 
通用方法 
FilenameIndex.getFilesByName()通过给定名称(不包含具体路径)搜索对应文件 
ReferencesSearch.search()类似于IDE中的Find Usages操作 
RefactoringFactory.createRename()重命名 
FileContentUtil.reparseFiles()通过VirtualFile重建PSIJava专用方法 
ClassInheritorsSearch.search()搜索一个类的所有子类 
JavaPsiFacade.findClass()通过类名查找类 
PsiShortNamesCache.getInstance().getClassesByName()通过一个短名称(例如LogUtil)查找类 
PsiClass.getSuperClass()查找一个类的直接父类 
JavaPsiFacade.getInstance().findPackage()获取Java类所在的Package 
OverridingMethodsSearch.search()查找被特定方法重写的方法3、如何根据将代码写入文件 
如Android不允许在UI线程中进行耗时操作一样,Intellij Platform也不允许在主线程中进行实时的文件写入,而需要通过一个异步任务来进行。
new WriteCommandAction(project) {
@Override
protected void run(@NotNull Result result) throws Throwable {
//writing to file
}
}.execute();
1
2
3
4
5
6
1
2
3
4
5
6
也可以继承自
WriteCommandAction.Simple
来执行写操作。
@Override
public void run() throws Throwable {

generateFields();
generateFindViewById();
// reformat class
JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
styleManager.optimizeImports(mFile);
styleManager.shortenClassReferences(mClass);
new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false).runWithoutProgress();
}
1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
主要使用
psiclass.add(JavaPsiFacade.getElementFactory(mProject).createMethodFromText(sbInitView.toString(), psiclass))
方法为类创建方法;用
mFactory.createFieldFromText
方法添加字段;用
mClass.findMethodsByName
方法查找方法,用
onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
方法为方法体添加内容。
protected void generateFields() {
for (Iterator<Element> iterator = mElements.iterator(); iterator.hasNext(); ) {
Element element = iterator.next();

if (!element.used) {
iterator.remove();
continue;
}

// remove duplicate field
PsiField[] fields = mClass.getFields();
boolean duplicateField = false;
for (PsiField field : fields) {
String name = field.getName();
if (name != null && name.equals(element.getFieldName())) {
duplicateField = true;
break;
}
}

if (duplicateField) {
iterator.remove();
continue;
}
String hint = element.xml.getAttributeValue("android:hint");
mClass.add(mFactory.createFieldFromText("/** "+hint+" */\nprivate " + element.name + " " + element.getFieldName() + ";", mClass));
}
}

protected void generateFindViewById() {
PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.app.Activity", new EverythingGlobalScope(mProject));
PsiClass compatActivityClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.support.v7.app.AppCompatActivity", new EverythingGlobalScope(mProject));

// Check for Activity class
if ((activityClass != null && mClass.isInheritor(activityClass, true))
|| (compatActivityClass != null && mClass.isInheritor(compatActivityClass, true))
|| mClass.getName().contains("Activity")) {
if (mClass.findMethodsByName("onCreate", false).length == 0) {
// Add an empty stub of onCreate()
StringBuilder method = new StringBuilder();
method.append("@Override protected void onCreate(android.os.Bundle savedInstanceState) {\n");
method.append("super.onCreate(savedInstanceState);\n");
method.append("\t// TODO: add setContentView(...) and run LayoutCreator again\n");
method.append("}");

mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
} else {
PsiStatement setContentViewStatement = null;
boolean hasInitViewStatement = false;

PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
for (PsiStatement statement : onCreate.getBody().getStatements()) {
// Search for setContentView()
if (statement.getFirstChild() instanceof PsiMethodCallExpression) {
PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) statement.getFirstChild()).getMethodExpression();
if (methodExpression.getText().equals("setContentView")) {
setContentViewStatement = statement;
} else if (methodExpression.getText().equals("initView")) {
hasInitViewStatement = true;
}
}
}

if(!hasInitViewStatement && setContentViewStatement != null) {
// Insert initView() after setContentView()
onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
}
generatorLayoutCode();
}
}
}
private void generatorLayoutCode() {
// generator findViewById code in initView() method
StringBuilder initView = new StringBuilder();
initView.append("private void initView() {\n");

for (Element element : mElements) {
initView.append(element.getFieldName() + " = (" + element.name + ")findViewById(" + element.getFullID() + ");\n");
}
initView.append("}\n");
mClass.add(mFactory.createMethodFromText(initView.toString(), mClass));

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
至此,我们之前的目标已经完成了,编码阶段告一段落。

三、使用插件

我们的插件实现完了,填写下plugin.xml文件相关内容,我们就可以导出需要安装的jar文件了:



打开android studio,进入setting页面,安装插件:

到这里,重启android studio就可以使用我们的插件了。 
当然,还可以把我们的插件发布到仓库,支持在plugin中搜索安装,可以参考官方给的文档: 
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html我们的插件这样就完成了,本文很多地方实现都参考了BorePlugin的实现,如果对实现细节感兴趣,可以查看这个开源项目的源码,再次也对作者表示感谢。文章简化版本的源码相对简单,方便理解,可以点此下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: