由于業(yè)務(wù)關(guān)系,經(jīng)常需要寫一些表單頁面樟凄,基本也就是簡(jiǎn)單的增刪改查然后上傳瘩燥,做過幾個(gè)頁面之后就有點(diǎn)想偷懶了,這么低水平重復(fù)性的體力勞動(dòng)不同,能不能用什么辦法自動(dòng)生成呢,查閱相關(guān)資料,發(fā)現(xiàn)android studio插件正好可以滿足需求二拐,在Github上搜了一下服鹅,找到BorePlugin這個(gè)幫助自動(dòng)生成布局代碼的插件挺不錯(cuò)的,在此基礎(chǔ)上修改為符合自己需求的插件百新,整體效果還不錯(cuò)企软。
發(fā)現(xiàn)了android studio插件的魅力,自己也總結(jié)一下饭望,也給小伙伴們提供一點(diǎn)參考仗哨,今天就以實(shí)現(xiàn)自動(dòng)生成findviewbyid
代碼插件的方式來個(gè)簡(jiǎn)單的總結(jié)。這里就不寫行文思路了铅辞,一切從0開始厌漂,一步一步搭建起這個(gè)插件項(xiàng)目吧。效果如下:
一斟珊、搭建環(huán)境
由于android studio是基于Intellij IDEA開發(fā)的苇倡,但Android Studio自身不具備開發(fā)插件的功能,所以插件開發(fā)需要在IntelliJ IDEA上開發(fā)囤踩。
好了旨椒,說了這么多,開始去官網(wǎng)下載吧堵漱,下載地址:https://www.jetbrains.com/idea/
安裝運(yùn)行后我們就可以開始開發(fā)了综慎。
創(chuàng)建項(xiàng)目
創(chuàng)建成功之后的文件夾是這個(gè)樣子的:
我們重點(diǎn)關(guān)注plugin.xml和src,plugin.xml是我們這個(gè)插件項(xiàng)目的配置說明勤庐,類似于android開發(fā)中的AndroidManifest.xml文件示惊,用于配置信息的注冊(cè)和聲明。
<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>
來簡(jiǎn)單介紹下這個(gè)XML配置文件:
id:插件的ID埃元,保證插件的唯一性涝涤,如果上傳倉庫的話。
name:插件名稱岛杀。
version:版本號(hào)阔拳。
description:插件的簡(jiǎn)介。
change-notes:版本更新信息类嗤。
extensions:擴(kuò)展組件注冊(cè) 糊肠。
actions:Action注冊(cè),比如在某個(gè)菜單下增加一個(gè)按鈕就要在這注冊(cè)遗锣。
二货裹、開始編碼
1、編寫菜單選項(xiàng)精偿,用于觸發(fā)我們的插件弧圆。
好了赋兵,現(xiàn)在我們要用到很關(guān)鍵的一個(gè)類:AnAction,選擇new->Action就可以創(chuàng)建:
ActionID:代表該Action的唯一的ID
ClassName:類名
Name:插件在菜單上的名稱
Description:對(duì)這個(gè)Action的描述信息
Groups:定義這個(gè)菜單選項(xiàng)出現(xiàn)的位置,比如圖中設(shè)置當(dāng)點(diǎn)擊菜單欄Edit時(shí)搔预,第一項(xiàng)會(huì)出現(xiàn)GenerateCode的選項(xiàng)霹期,右邊的Anchor是選擇該選項(xiàng)出現(xiàn)的位置,默認(rèn)First即最頂部拯田。
之后會(huì)出現(xiàn)我們創(chuàng)建的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>
這樣历造,一個(gè)菜單選項(xiàng)就完成了,接下來就該實(shí)現(xiàn)當(dāng)用戶點(diǎn)擊GenerateCode菜單或者按快捷鍵Command+ M后的功能代碼了船庇。
2吭产、實(shí)現(xiàn)功能邏輯代碼
在實(shí)現(xiàn)功能邏輯之前,我們要先理清需求鸭轮,首先我們是想在選中布局文件的時(shí)候臣淤,自動(dòng)解析布局文件并生成findviewbyid
代碼。那我們主要關(guān)注三個(gè)點(diǎn)就可以了张弛。
1荒典、如何獲取布局文件
2、如何解析布局文件
3吞鸭、如何根據(jù)將代碼寫入文件
1寺董、如何獲取布局文件
為簡(jiǎn)單起見,我們這里通過讓用戶自己輸入布局文件的方式通過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)
這里使用最后一種方式來獲取圖片,獲取用戶選中的布局文件造虏,如果用戶沒有選中內(nèi)容御吞,通過在狀態(tài)欄彈窗提示:
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);
}
獲取用戶選中內(nèi)容:
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
Editor editor = e.getData(PlatformDataKeys.EDITOR);
if (null == editor) {
return;
}
SelectionModel model = editor.getSelectionModel();
//獲取選中內(nèi)容
final String selectedText = model.getSelectedText();
if (TextUtils.isEmpty(selectedText)) {
Utils.showNotification(project,MessageType.ERROR,"請(qǐng)選中生成內(nèi)容");
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享钞、如何解析布局文件
關(guān)于文件操作揍诽,官方文檔是這樣寫的:
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())方法對(duì)XML文件進(jìn)行解析:
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標(biāo)簽
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag) element;
//解析include標(biāo)簽
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];
}
以及實(shí)體類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()通過給定名稱(不包含具體路徑)搜索對(duì)應(yīng)文件
ReferencesSearch.search()類似于IDE中的Find Usages操作
RefactoringFactory.createRename()重命名
FileContentUtil.reparseFiles()通過VirtualFile重建PSI
Java專用方法
ClassInheritorsSearch.search()搜索一個(gè)類的所有子類
JavaPsiFacade.findClass()通過類名查找類
PsiShortNamesCache.getInstance().getClassesByName()通過一個(gè)短名稱(例如LogUtil)查找類
PsiClass.getSuperClass()查找一個(gè)類的直接父類
JavaPsiFacade.getInstance().findPackage()獲取Java類所在的Package
OverridingMethodsSearch.search()查找被特定方法重寫的方法
3、如何根據(jù)將代碼寫入文件
如Android不允許在UI線程中進(jìn)行耗時(shí)操作一樣栗竖,Intellij Platform也不允許在主線程中進(jìn)行實(shí)時(shí)的文件寫入暑脆,而需要通過一個(gè)異步任務(wù)來進(jìn)行。
new WriteCommandAction(project) {
@Override
protected void run(@NotNull Result result) throws Throwable {
//writing to file
}
}.execute();
也可以繼承自WriteCommandAction.Simple
來執(zhí)行寫操作狐肢。
@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))
方法為類創(chuàng)建方法添吗;用mFactory.createFieldFromText
方法添加字段;用mClass.findMethodsByName
方法查找方法份名,用onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
方法為方法體添加內(nèi)容碟联。
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));
}
至此妓美,我們之前的目標(biāo)已經(jīng)完成了,編碼階段告一段落玄帕。
三部脚、使用插件
我們的插件實(shí)現(xiàn)完了,填寫下plugin.xml文件相關(guān)內(nèi)容裤纹,我們就可以導(dǎo)出需要安裝的jar文件了:
打開android studio,進(jìn)入setting頁面丧没,安裝插件:
到這里鹰椒,重啟android studio就可以使用我們的插件了。
當(dāng)然呕童,還可以把我們的插件發(fā)布到倉庫漆际,支持在plugin中搜索安裝,可以參考官方給的文檔:
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html
我們的插件這樣就完成了夺饲,本文很多地方實(shí)現(xiàn)都參考了BorePlugin的實(shí)現(xiàn)奸汇,如果對(duì)實(shí)現(xiàn)細(xì)節(jié)感興趣,可以查看這個(gè)開源項(xiàng)目的源碼往声,再次也對(duì)作者表示感謝擂找。文章簡(jiǎn)化版本的源碼相對(duì)簡(jiǎn)單,方便理解浩销,可以點(diǎn)此下載贯涎。