动手试试Android Studio插件开发

追风赶月莫停留,平芜尽处是春山。这篇文章主要讲述动手试试Android Studio插件开发相关的知识,希望能为你提供帮助。
由于业务关系, 经常需要写一些表单页面, 基本也就是简单的增删改查然后上传, 做过几个页面之后就有点想偷懒了, 这么低水平重复性的体力劳动, 能不能用什么办法自动生成呢, 查阅相关资料, 发现android studio插件正好可以满足需求, 在Github上搜了一下, 找到BorePlugin这个帮助自动生成布局代码的插件挺不错的, 在此基础上修改为符合自己需求的插件, 整体效果还不错。
发现了android studio插件的魅力, 自己也总结一下, 也给小伙伴们提供一点参考, 今天就以实现自动生成findviewbyid代码插件的方式来个简单的总结。这里就不写行文思路了, 一切从0开始, 一步一步搭建起这个插件项目吧。效果如下:

动手试试Android Studio插件开发

文章图片

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

文章图片

创建成功之后的文件夹是这个样子的:
动手试试Android Studio插件开发

文章图片

我们重点关注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、编写菜单选项, 用于触发我们的插件。
动手试试Android Studio插件开发

文章图片

好了, 现在我们要用到很关键的一个类: AnAction,选择new-> Action就可以创建:
动手试试Android Studio插件开发

文章图片

动手试试Android Studio插件开发

文章图片

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

动手试试Android Studio插件开发

文章图片

获取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 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); } }

一些有用的方法
通用方法
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插件开发

文章图片

动手试试Android Studio插件开发

文章图片

打开android studio, 进入setting页面, 安装插件:
动手试试Android Studio插件开发

文章图片

到这里, 重启android studio就可以使用我们的插件了。
当然, 还可以把我们的插件发布到仓库, 支持在plugin中搜索安装, 可以参考官方给的文档:
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html
【动手试试Android Studio插件开发】我们的插件这样就完成了, 本文很多地方实现都参考了BorePlugin的实现, 如果对实现细节感兴趣, 可以查看这个开源项目的源码, 再次也对作者表示感谢。文章简化版本的源码相对简单, 方便理解, 可以点此下载。

    推荐阅读