追风赶月莫停留,平芜尽处是春山。这篇文章主要讲述动手试试Android Studio插件开发相关的知识,希望能为你提供帮助。
由于业务关系,
经常需要写一些表单页面,
基本也就是简单的增删改查然后上传,
做过几个页面之后就有点想偷懒了,
这么低水平重复性的体力劳动,
能不能用什么办法自动生成呢,
查阅相关资料,
发现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>
来简单介绍下这个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?
}?
}
而
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>
这样, 一个菜单选项就完成了, 接下来就该实现当用户点击GenerateCode菜单或者按快捷键Command+ M后的功能代码了。
2、实现功能逻辑代码 在实现功能逻辑之前, 我们要先理清需求, 首先我们是想在选中布局文件的时候, 自动解析布局文件并生成
findviewbyid
代码。那我们主要关注三个点就可以了。1、如何获取布局文件1、如何获取布局文件
2、如何解析布局文件
3、如何根据将代码写入文件
为简单起见, 我们这里通过让用户自己输入布局文件的方式通过
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)
这里使用最后一种方式来获取图片, 获取用户选中的布局文件, 如果用户没有选中内容, 通过在状态栏弹窗提示:
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);
}
获取用户选中内容:
@
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;
}
}
文章图片
获取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];
至此, 布局文件获取到了, 我们开始下一步, 解析布局文件啦。
2、如何解析布局文件
关于文件操作, 官方文档是这样写的:
Most interesting modification operations are performed on the level of individual PSI elements, not files as a whole.我们这里通过file.accept(new XmlRecursiveElementVisitor())方法对XML文件进行解析:
To iterate over the elements in a file, use
psiFile.accept(new PsiRecursiveElementWalkingVisitor()…);
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];
}
以及实体类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 idValue 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);
}
}
一些有用的方法3、如何根据将代码写入文件
通用方法
FilenameIndex.getFilesByName()通过给定名称( 不包含具体路径) 搜索对应文件
ReferencesSearch.search()类似于IDE中的Find Usages操作
RefactoringFactory.createRename()重命名
FileContentUtil.reparseFiles()通过VirtualFile重建PSI
Java专用方法
ClassInheritorsSearch.search()搜索一个类的所有子类
JavaPsiFacade.findClass()通过类名查找类
PsiShortNamesCache.getInstance().getClassesByName()通过一个短名称( 例如LogUtil) 查找类
PsiClass.getSuperClass()查找一个类的直接父类
JavaPsiFacade.getInstance().findPackage()获取Java类所在的Package
OverridingMethodsSearch.search()查找被特定方法重写的方法
如Android不允许在UI线程中进行耗时操作一样, Intellij Platform也不允许在主线程中进行实时的文件写入, 而需要通过一个异步任务来进行。
new WriteCommandAction(project) {
@
Override
protected void run(@
NotNull Result result) throws Throwable {
//writing to file
}
}.execute();
也可以继承自
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();
}
主要使用
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));
}
至此, 我们之前的目标已经完成了, 编码阶段告一段落。
三、使用插件
我们的插件实现完了, 填写下plugin.xml文件相关内容, 我们就可以导出需要安装的jar文件了:
文章图片
文章图片
打开android studio, 进入setting页面, 安装插件:
文章图片
到这里, 重启android studio就可以使用我们的插件了。
当然, 还可以把我们的插件发布到仓库, 支持在plugin中搜索安装, 可以参考官方给的文档:
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html
【动手试试Android Studio插件开发】我们的插件这样就完成了, 本文很多地方实现都参考了BorePlugin的实现, 如果对实现细节感兴趣, 可以查看这个开源项目的源码, 再次也对作者表示感谢。文章简化版本的源码相对简单, 方便理解, 可以点此下载。
推荐阅读
- Android需求之点击跳转至市场评价
- Android中隐藏顶部状态栏的那些坑
- Android开发重要参考资料
- MQTT与Mosquitto服务器搭建以及Android推送MQTT简介
- android handler 调用原理
- Android 中三种启用线程的方法
- Android--split()分割字符串特殊用法
- Android性能优化之TraceView和Lint使用详解
- android的Drawable详解