前言
为何会自己写插件呢,原因有两个,一个是之前看到鸿神写了一篇学会编写Android Studio插件 别停留在用的程度了的博客,另一个是有些插件是不能满足自己的需求的,所有就需要自己来写;之前因为赶项目没时间,今天抽空就学习了下。
可以看下一篇 Android Studio插件GenerateFindViewById
这篇博客是根据输入或者选中布局文件(如R.layout.activity_main,只需要选中activity_main或者输入activity_main),来自动生成字段,和获取值(findViewById())。
适用Activity和Fragment
编写插件
环境
Android Studio本身是不支持开发插件的,所以需要下载IntelliJ IDEA来编写,但是Android Studio是基于IntelliJ IDEA的,用IntelliJ IDEA不会感到陌生,官网下载https://www.jetbrains.com/idea/
创建项目
目录结构
plugin.xml
plugin.xml是类似Android项目里面的AndroidMenifest文件,用来配置信息的注册和声明。
id:(com.example.plugin.Name)插件的ID,保证插件上传仓库后的唯一性。 name:插件名称。 version:版本号。 description:插件的简介。 change-notes:版本更新信息。 extensions:扩展组件注册 。
开始编写
创建一个Action,是继承AnAction类的
右键src目录->New->Action
填写内容
填写ActionID,ClassName,Name,Description;选择放在哪个菜单,Anchor选择First或者Last;设置快捷键KeyBoard Shortcuts;
ActionID:代表该Action的唯一的ID,一般的格式为:pluginName.ID ClassName:类名 Name:就是最终插件在菜单上的名称 Description:对这个Action的描述信息 Groups:定义这个菜单选项出现的位置,这里选中CodeMenu(Code),在Code菜单里面。
可以在plugin.xml里面修改对应的Action属性
编写Action
点击ok之后会生成相应的Action,在Action里面的actionPerformed方法会在点击菜单或者快捷键的是否触发。
思路
在获取布局文件内容后自动解析布局文件并生成字段和findViewById代码。
1.获取布局文件 2.解析布局文件,获取属性 3.将代码写入action
获取布局文件
查找文件需要用到PsiFile类,通过FilenameIndex.getFilesByName(project, name, scope)来查找布局文件。 先获取用户选中内容,如果没选中,则弹出dialog让用户输入内容;
Project project = e.getProject();
final Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
if (
null == mEditor) {
return;
}
SelectionModel model = mEditor.getSelectionModel();
String selectedText = model.getSelectedText();
if (TextUtils.isEmpty(selectedText)) {
selectedText = Messages.showInputDialog(project,
"layout(不需要输入R.layout.):" ,
"未选中布局内容,请输入layout文件名", Messages.getInformationIcon());
if (TextUtils.isEmpty(selectedText)) {
Utils.showPopupBalloon(mEditor,
"未输入layout文件名");
return;
}
}
然后根据输入的内容查找xml文件;
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, selectedText +
".xml", GlobalSearchScope.allScope(project));
if (psiFiles.length <=
0) {
Utils.showPopupBalloon(mEditor,
"未找到选中的布局文件");
return;
}
XmlFile xmlFile = (XmlFile) psiFiles[
0];
解析布局文件,获取属性
通过psiFile.accept(new PsiRecursiveElementWalkingVisitor()…);去遍历一个文件的所有元素
/**
* 获取所有id
*
* @param file
* @param elements
* @return
*/
public static java.util.List<Element>
getIDsFromLayout(
final PsiFile file,
final java.util.List<Element> elements) {
file.accept(
new XmlRecursiveElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (element
instanceof XmlTag) {
XmlTag tag = (XmlTag) element;
String name = tag.getName();
if (name.equalsIgnoreCase(
"include")) {
XmlAttribute layout = tag.getAttribute(
"layout",
null);
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;
}
}
XmlAttribute id = tag.getAttribute(
"android:id",
null);
if (id ==
null) {
return;
}
String idValue = id.getValue();
if (idValue ==
null) {
return;
}
XmlAttribute aClass = tag.getAttribute(
"class",
null);
if (aClass !=
null) {
name = aClass.getValue();
}
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;
}
String[] parts = layout.split(
"/");
if (parts.length !=
2) {
return null;
}
return parts[
1];
}
对应的实体类Element,里面包含获取id的值,获取类型如(TextView或者com.example.CustomView),根据id设置变量名。
private static final Pattern sIdPattern = Pattern.compile(
"@\\+?(android:)?id/([^$]+)$", Pattern.CASE_INSENSITIVE);
public String id;
public String name;
public int fieldNameType =
3;
public XmlTag xml;
/**
* 构造函数
*
* @param name View的名字
* @param id android:id属性
* @throws IllegalArgumentException When the arguments are invalid
*/
public Element(String name, String id, XmlTag xml) {
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) {
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) {
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) {
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;
}
public static String
firstToUpperCase(String key) {
return key.substring(
0,
1).toUpperCase(Locale.CHINA) + key.substring(
1);
}
将代码写入action
Intellij Platform不允许在主线程中进行实时的文件写入,需通过异步任务来进行,可以通过继承WriteCommandAction.Simple,然后在run方法里面进行写文件操作。
@Override
protected void run()
throws Throwable {
}
主要用到的方法
/**
* 根据当前文件获取对应的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)判断类是否包含某方法JavaPsiFacade.getInstance(mProject).findClass("android.app.Activity", new EverythingGlobalScope(mProject));根据类名查找类PsiUtilBase.getPsiFileInEditor(mEditor, project);方法为获取当前文件;psiclass.add(JavaPsiFacade.getElementFactory(mProject).createMethodFromText(sbInitView.toString(), psiclass))方法为类创建方法;mFactory.mFactory.createMethodFromText(method.toString(), mClass)方法添加字段;onCreate.getBody().addAfter(mFactory.createStatementFromText("initView();", mClass), setContentViewStatement);方法为方法体添加内容。
具体创建内容
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;
mFactory = JavaPsiFacade.getElementFactory(mProject);
mSelectText = selectText;
}
@Override
protected void run()
throws Throwable {
generateFields();
generateFindViewById();
JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(mProject);
styleManager.optimizeImports(mFile);
styleManager.shortenClassReferences(mClass);
new ReformatCodeProcessor(mProject, mClass.getContainingFile(),
null,
false).runWithoutProgress();
}
/**
* 创建变量
*/
private void generateFields() {
for (Element element : mElements) {
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;
}
String text = element.xml.getAttributeValue(
"android:text");
String fromText =
"private " + element.name +
" " + element.getFieldName() +
";";
if (!TextUtils.isEmpty(text)) {
fromText =
"/** " + text +
" */\n" + fromText;
}
mClass.add(mFactory.createFieldFromText(fromText, mClass));
}
}
/**
* 设置变量的值FindViewById,Activity和Fragment
*/
private void generateFindViewById() {
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));
if ((activityClass !=
null && mClass.isInheritor(activityClass,
true))
|| (activityCompatClass !=
null && mClass.isInheritor(activityCompatClass,
true))
|| mClass.getName().contains(
"Activity")) {
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 {
PsiStatement setContentViewStatement =
null;
boolean hasInitViewStatement =
false;
PsiMethod onCreate = mClass.findMethodsByName(
"onCreate",
false)[
0];
for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
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) {
onCreate.getBody().addAfter(mFactory.createStatementFromText(
"initView();", mClass), setContentViewStatement);
}
generatorLayoutCode(
null);
}
}
else if ((fragmentClass !=
null && mClass.isInheritor(fragmentClass,
true))
|| (fragmentV4Class !=
null && mClass.isInheritor(fragmentV4Class,
true))
|| mClass.getName().contains(
"Fragment")) {
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 {
PsiReturnStatement returnStatement =
null;
String returnValue =
null;
boolean hasInitViewStatement =
false;
PsiMethod onCreate = mClass.findMethodsByName(
"onCreateView",
false)[
0];
for (PsiStatement psiStatement : onCreate.getBody().getStatements()) {
if (psiStatement
instanceof PsiReturnStatement) {
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);
}
}
}
/**
* 写initView方法
*
* @param findPre Fragment的话要view.findViewById
*/
private void generatorLayoutCode(String findPre) {
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));
}
}
}
使用插件
导出插件Build->Prepare All Plugin Modules For Deployment
Android Studio导入插件,当前是本地,直接通过Install plugin from disk...导入。
博客的代码不是完整的,更多内容可以到GitHub上下载查看GitHub,此插件以后会继续更新,欢迎Start,Issuse
发布插件
发布到IntelliJ Plugin仓库,支持在plugin中搜索安装,参考: http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/publishing_plugin.html
主要的步骤就是注册账号,提交相应的jar文件,然后填写信息,最后等待审核就可以了。
感谢
学会编写Android Studio插件 别停留在用的程度了
http://blog.csdn.net/zhangke3016/article/details/53245530