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

动手试试Android Studio插件开发

2016-11-20 22:30 483 查看
由于业务关系,经常需要写一些表单页面,基本也就是简单的增删改查然后上传,做过几个页面之后就有点想偷懒了,这么低水平重复性的体力劳动,能不能用什么办法自动生成呢,查阅相关资料,发现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、如何获取布局文件

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)


这里使用最后一种方式来获取图片,获取用户选中的布局文件,如果用户没有选中内容,通过在状态栏弹窗提示:

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.

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];
}


以及实体类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);
}
}


一些有用的方法

通用方法

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()查找被特定方法重写的方法

3、如何根据将代码写入文件

如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

我们的插件这样就完成了,本文很多地方实现都参考了BorePlugin的实现,如果对实现细节感兴趣,可以查看这个开源项目的源码,再次也对作者表示感谢。文章简化版本的源码相对简单,方便理解,可以点此下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息