前言
為何會(huì)自己寫(xiě)插件呢,原因有兩個(gè)糠排,一個(gè)是之前看到鴻神寫(xiě)了一篇學(xué)會(huì)編寫(xiě)Android Studio插件 別停留在用的程度了的博客伶选,另一個(gè)是有些插件是不能滿(mǎn)足自己的需求的词渤,所有就需要自己來(lái)寫(xiě)螺垢;之前因?yàn)橼s項(xiàng)目沒(méi)時(shí)間喧务,今天抽空就學(xué)習(xí)了下。
這是第二篇博客
Android Studio插件GenerateFindViewById
這篇博客是根據(jù)輸入或者選中布局文件(如
R.layout.activity_main
枉圃,只需要選中activity_main
或者輸入activity_main
)功茴,來(lái)自動(dòng)生成字段,和獲取值(findViewById()
)孽亲。
適用Activity和Fragment
編寫(xiě)插件
環(huán)境
Android Studio本身是不支持開(kāi)發(fā)插件的坎穿,所以需要下載IntelliJ IDEA來(lái)編寫(xiě),但是Android Studio是基于IntelliJ IDEA的墨林,用IntelliJ IDEA不會(huì)感到陌生赁酝,官網(wǎng)下載https://www.jetbrains.com/idea/
創(chuàng)建項(xiàng)目
目錄結(jié)構(gòu)
plugin.xml
plugin.xml
是類(lèi)似Android項(xiàng)目里面的AndroidMenifest文件,用來(lái)配置信息的注冊(cè)和聲明旭等。
- id:(com.example.plugin.Name)插件的ID,保證插件上傳倉(cāng)庫(kù)后的唯一性衡载。
- name:插件名稱(chēng)搔耕。
- version:版本號(hào)。
- description:插件的簡(jiǎn)介。
- change-notes:版本更新信息弃榨。
- extensions:擴(kuò)展組件注冊(cè) 菩收。
開(kāi)始編寫(xiě)
創(chuàng)建一個(gè)Action,是繼承AnAction類(lèi)的
右鍵src目錄->
New->Action
填寫(xiě)內(nèi)容
填寫(xiě)ActionID鲸睛,ClassName娜饵,Name,Description官辈;選擇放在哪個(gè)菜單箱舞,Anchor選擇First或者Last;設(shè)置快捷鍵KeyBoard Shortcuts拳亿;
ActionID:代表該Action的唯一的ID晴股,一般的格式為:pluginName.ID
ClassName:類(lèi)名
Name:就是最終插件在菜單上的名稱(chēng)
Description:對(duì)這個(gè)Action的描述信息
Groups:定義這個(gè)菜單選項(xiàng)出現(xiàn)的位置,這里選中CodeMenu(Code)肺魁,在Code菜單里面电湘。
可以在plugin.xml
里面修改對(duì)應(yīng)的Action屬性
編寫(xiě)Action
點(diǎn)擊ok之后會(huì)生成相應(yīng)的Action,在Action里面的
actionPerformed
方法會(huì)在點(diǎn)擊菜單或者快捷鍵的是否觸發(fā)鹅经。
思路
在獲取布局文件內(nèi)容后自動(dòng)解析布局文件并生成字段和findViewById
代碼寂呛。
1.獲取布局文件
2.解析布局文件,獲取屬性
3.將代碼寫(xiě)入action
獲取布局文件
查找文件需要用到PsiFile
類(lèi)瘾晃,通過(guò)FilenameIndex.getFilesByName(project, name, scope)
來(lái)查找布局文件贷痪。
先獲取用戶(hù)選中內(nèi)容,如果沒(méi)選中酗捌,則彈出dialog讓用戶(hù)輸入內(nèi)容呢诬;
// 獲取project
Project project = e.getProject();
// 獲取選中內(nèi)容
final Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
if (null == mEditor) {
return;
}
SelectionModel model = mEditor.getSelectionModel();
String selectedText = model.getSelectedText();
if (TextUtils.isEmpty(selectedText)) {
// 未選中布局內(nèi)容,顯示dialog
selectedText = Messages.showInputDialog(project, "layout(不需要輸入R.layout.):" , "未選中布局內(nèi)容胖缤,請(qǐng)輸入layout文件名", Messages.getInformationIcon());
if (TextUtils.isEmpty(selectedText)) {
Utils.showPopupBalloon(mEditor, "未輸入layout文件名");
return;
}
}
然后根據(jù)輸入的內(nèi)容查找xml文件尚镰;
// 獲取布局文件,通過(guò)FilenameIndex.getFilesByName獲取
// GlobalSearchScope.allScope(project)搜索整個(gè)項(xiàng)目
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, selectedText + ".xml", GlobalSearchScope.allScope(project));
if (psiFiles.length <= 0) {
Utils.showPopupBalloon(mEditor, "未找到選中的布局文件");
return;
}
XmlFile xmlFile = (XmlFile) psiFiles[0];
解析布局文件哪廓,獲取屬性
通過(guò)psiFile.accept(new PsiRecursiveElementWalkingVisitor()…);去遍歷一個(gè)文件的所有元素
/**
* 獲取所有id
*
* @param file
* @param elements
* @return
*/
public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> elements) {
// To iterate over the elements in a file
// 遍歷一個(gè)文件的所有元素
file.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
// 解析Xml標(biāo)簽
if (element instanceof XmlTag) {
XmlTag tag = (XmlTag) element;
// 獲取Tag的名字(TextView)或者自定義
String name = tag.getName();
// 如果有include
if (name.equalsIgnoreCase("include")) {
// 獲取布局
XmlAttribute layout = tag.getAttribute("layout", null);
// 獲取project
Project project = file.getProject();
// 布局文件
XmlFile include = null;
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, getLayoutName(layout.getValue()) + ".xml", GlobalSearchScope.allScope(project));
if (psiFiles.length > 0) {
include = (XmlFile) psiFiles[0];
}
if (include != null) {
// 遞歸
getIDsFromLayout(include, elements);
return;
}
}
// 獲取id字段屬性
XmlAttribute id = tag.getAttribute("android:id", null);
if (id == null) {
return;
}
// 獲取id的值
String idValue = id.getValue();
if (idValue == null) {
return;
}
XmlAttribute aClass = tag.getAttribute("class", null);
if (aClass != null) {
name = aClass.getValue();
}
// 添加到list
try {
Element e = new Element(name, idValue, tag);
elements.add(e);
} catch (IllegalArgumentException e) {
}
}
}
});
return elements;
}
/**
* layout.getValue()返回的值為@layout/layout_view
*
* @param layout
* @return
*/
public static String getLayoutName(String layout) {
if (layout == null || !layout.startsWith("@") || !layout.contains("/")) {
return null;
}
// @layout layout_view
String[] parts = layout.split("/");
if (parts.length != 2) {
return null;
}
// layout_view
return parts[1];
}
對(duì)應(yīng)的實(shí)體類(lèi)Element狗唉,里面包含獲取id的值,獲取類(lèi)型如(TextView或者com.example.CustomView)涡真,根據(jù)id設(shè)置變量名分俯。
// 判斷id正則
private static final Pattern sIdPattern = Pattern.compile("@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
// id
public String id;
// 名字如TextView
public String name;
// 命名1 aa_bb_cc; 2 aaBbCc 3 mAaBbCc
public int fieldNameType = 3;
public XmlTag xml;
/**
* 構(gòu)造函數(shù)
*
* @param name View的名字
* @param id android:id屬性
* @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);
}
if (this.id == null) {
throw new IllegalArgumentException("Invalid format of view id");
}
String[] packages = name.split("\\.");
if (packages.length > 1) {
// com.example.CustomView
this.name = packages[packages.length - 1];
} else {
this.name = name;
}
this.xml = xml;
}
/**
* 獲取id,R.id.id
*
* @return
*/
public String getFullID() {
StringBuilder fullID = new StringBuilder();
String rPrefix = "R.id.";
fullID.append(rPrefix);
fullID.append(id);
return fullID.toString();
}
/**
* 獲取變量名
*
* @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;
}
// 第一個(gè)字母大寫(xiě)
public static String firstToUpperCase(String key) {
return key.substring(0, 1).toUpperCase(Locale.CHINA) + key.substring(1);
}
將代碼寫(xiě)入action
Intellij Platform不允許在主線程中進(jìn)行實(shí)時(shí)的文件寫(xiě)入哆料,需通過(guò)異步任務(wù)來(lái)進(jìn)行缸剪,可以通過(guò)繼承WriteCommandAction.Simple
,然后在run方法里面進(jìn)行寫(xiě)文件操作东亦。
@Override
protected void run() throws Throwable {
}
主要用到的方法
/**
* 根據(jù)當(dāng)前文件獲取對(duì)應(yīng)的class文件
* @param editor
* @param file
* @return
*/
protected PsiClass getTargetClass(Editor editor, PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement element = file.findElementAt(offset);
if(element == null) {
return null;
} else {
PsiClass target = PsiTreeUtil.getParentOfType(element, PsiClass.class);
return target instanceof SyntheticElement ?null:target;
}
}
-
mClass.findMethodsByName("onCreate", false)
判斷類(lèi)是否包含某方法 -
JavaPsiFacade.getInstance(mProject).findClass("android.app.Activity", new EverythingGlobalScope(mProject));
根據(jù)類(lèi)名查找類(lèi) -
PsiUtilBase.getPsiFileInEditor(mEditor, project);
方法為獲取當(dāng)前文件杏节; -
psiclass.add(JavaPsiFacade.getElementFactory(mProject).createMethodFromText(sbInitView.toString(), psiclass))
方法為類(lèi)創(chuàng)建方法; -
mFactory.mFactory.createMethodFromText(method.toString(), mClass)
方法添加字段; -
onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
方法為方法體添加內(nèi)容奋渔。
具體創(chuàng)建內(nèi)容
import com.intellij.codeInsight.actions.ReformatCodeProcessor;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.WriteCommandAction.Simple;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.search.EverythingGlobalScope;
import entity.Element;
import org.apache.http.util.TextUtils;
import java.util.List;
public class IdCreator extends Simple {
private PsiFile mFile;
private Project mProject;
private PsiClass mClass;
private List<Element> mElements;
private PsiElementFactory mFactory;
private String mSelectText;
public IdCreator(PsiFile psiFile, PsiClass psiClass, String command, List<Element> elements, String selectText) {
super(psiClass.getProject(), command);
mFile = psiFile;
mProject = psiClass.getProject();
mClass = psiClass;
mElements = elements;
// 獲取Factory
mFactory = JavaPsiFacade.getElementFactory(mProject);
mSelectText = selectText;
}
@Override
protected void run() throws Throwable {
generateFields();
generateFindViewById();
// 重寫(xiě)class
JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
styleManager.optimizeImports(mFile);
styleManager.shortenClassReferences(mClass);
new ReformatCodeProcessor(mProject, mClass.getContainingFile(), null, false).runWithoutProgress();
}
/**
* 創(chuàng)建變量
*/
private void generateFields() {
for (Element element : mElements) {
// 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) {
continue;
}
// 設(shè)置變量
String text = element.xml.getAttributeValue("android:text");
// text
String fromText = "private " + element.name + " " + element.getFieldName() + ";";
if (!TextUtils.isEmpty(text)) {
fromText = "/** " + text + " */\n" + fromText;
}
// 添加到class
mClass.add(mFactory.createFieldFromText(fromText, mClass));
}
}
/**
* 設(shè)置變量的值FindViewById镊逝,Activity和Fragment
*/
private void generateFindViewById() {
// 根據(jù)類(lèi)名查找類(lèi)
PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass("android.app.Activity", new EverythingGlobalScope(mProject));
PsiClass activityCompatClass = JavaPsiFacade.getInstance(mProject).findClass("android.support.v7.app.AppCompatActivity", new EverythingGlobalScope(mProject));
PsiClass fragmentClass = JavaPsiFacade.getInstance(mProject).findClass("android.app.Fragment", new EverythingGlobalScope(mProject));
PsiClass fragmentV4Class = JavaPsiFacade.getInstance(mProject).findClass("android.support.v4.app.Fragment", new EverythingGlobalScope(mProject));
// 判斷mClass是不是繼承activityClass或者activityCompatClass
if ((activityClass != null && mClass.isInheritor(activityClass, true))
|| (activityCompatClass != null && mClass.isInheritor(activityCompatClass, true))
|| mClass.getName().contains("Activity")) {
// 判斷是否有onCreate方法
if (mClass.findMethodsByName("onCreate", false).length == 0) {
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:run FindViewById again To setValue in initView method\n");
method.append("\tsetContentView(R.layout.");
method.append(mSelectText);
method.append(");\n");
method.append("}");
// 添加
mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
} else {
// 獲取setContentView
PsiStatement setContentViewStatement = null;
// onCreate是否存在initView方法
boolean hasInitViewStatement = false;
PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
// 查找setContentView
if (psiStatement.getFirstChild() instanceof PsiMethodCallExpression) {
PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) psiStatement.getFirstChild()).getMethodExpression();
if (methodExpression.getText().equals("setContentView")) {
setContentViewStatement = psiStatement;
} else if (methodExpression.getText().equals("initView")) {
hasInitViewStatement = true;
}
}
}
if (!hasInitViewStatement && setContentViewStatement != null) {
// 將initView()寫(xiě)到setContentView()后面
onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);
}
generatorLayoutCode(null);
}
// 判斷mClass是不是繼承fragmentClass或者fragmentV4Class
} else if ((fragmentClass != null && mClass.isInheritor(fragmentClass, true))
|| (fragmentV4Class != null && mClass.isInheritor(fragmentV4Class, true))
|| mClass.getName().contains("Fragment")) {
// 判斷是否有onCreateView方法
if (mClass.findMethodsByName("onCreateView", false).length == 0) {
StringBuilder method = new StringBuilder();
method.append("@Override public View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup container, android.os.Bundle savedInstanceState) {\n");
method.append("\t// TODO: run FindViewById again To setValue in initView method\n");
method.append("\tView view = View.inflate(getActivity(), R.layout.");
method.append(mSelectText);
method.append(", null);");
method.append("return view;");
method.append("}");
// 添加
mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
} else {
// 查找onCreateView
PsiReturnStatement returnStatement = null;
// view
String returnValue = null;
// onCreateView是否存在initView方法
boolean hasInitViewStatement = false;
PsiMethod onCreate = mClass.findMethodsByName("onCreateView", false)[0];
for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
if (psiStatement instanceof PsiReturnStatement) {
// 獲取view的值
returnStatement = (PsiReturnStatement) psiStatement;
returnValue = returnStatement.getReturnValue().getText();
} else if (psiStatement.getFirstChild() instanceof PsiMethodCallExpression) {
PsiReferenceExpression methodExpression = ((PsiMethodCallExpression) psiStatement.getFirstChild()).getMethodExpression();
if (methodExpression.getText().equals("initView")) {
hasInitViewStatement = true;
}
}
}
if (!hasInitViewStatement && returnStatement != null && returnValue != null) {
onCreate.getBody().addBefore(mFactory.createStatementFromText("initView(" + returnValue + ");", mClass), returnStatement);
}
generatorLayoutCode(returnValue);
}
}
}
/**
* 寫(xiě)initView方法
*
* @param findPre Fragment的話要view.findViewById
*/
private void generatorLayoutCode(String findPre) {
// 判斷是否已有initView方法
PsiMethod[] initViewMethods = mClass.findMethodsByName("initView", false);
if (initViewMethods.length > 0 && initViewMethods[0].getBody() != null) {
PsiCodeBlock initViewMethodBody = initViewMethods[0].getBody();
for (Element element : mElements) {
String pre = TextUtils.isEmpty(findPre) ? "" : findPre + ".";
String s2 = element.getFieldName() + " = (" + element.name + ") " + pre + "findViewById(" + element.getFullID() + ");";
initViewMethodBody.add(mFactory.createStatementFromText(s2, initViewMethods[0]));
}
} else {
StringBuilder initView = new StringBuilder();
if (TextUtils.isEmpty(findPre)) {
initView.append("private void initView() {\n");
} else {
initView.append("private void initView(View " + findPre + ") {\n");
}
for (Element element : mElements) {
String pre = TextUtils.isEmpty(findPre) ? "" : findPre + ".";
initView.append(element.getFieldName() + " = (" + element.name + ")" + pre + "findViewById(" + element.getFullID() + ");\n");
}
initView.append("}\n");
mClass.add(mFactory.createMethodFromText(initView.toString(), mClass));
}
}
}
使用插件
導(dǎo)出插件
Build->Prepare All Plugin Modules For Deployment
Android Studio導(dǎo)入插件,當(dāng)前是本地嫉鲸,直接通過(guò)
Install plugin from disk...
導(dǎo)入撑蒜。
博客的代碼不是完整的,更多內(nèi)容可以到GitHub上下載查看GitHub玄渗,此插件以后會(huì)繼續(xù)更新座菠,歡迎Start,Issuse
發(fā)布插件
發(fā)布到IntelliJ Plugin倉(cāng)庫(kù)捻爷,支持在plugin中搜索安裝辈灼,參考:
http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html
主要的步驟就是注冊(cè)賬號(hào),提交相應(yīng)的jar文件也榄,然后填寫(xiě)信息巡莹,最后等待審核就可以了。