Android Studio插件开发-SteadyoungIOC注解生成器:Steadyoung-CodePlug
Posted Steadyoung
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Studio插件开发-SteadyoungIOC注解生成器:Steadyoung-CodePlug相关的知识,希望对你有一定的参考价值。
前言
上一篇文章我已经分享了自己简易打造的IOC注解框架:SteadyoungIOC。留下了文中快速生成代码的插件未解析,今天就来一步步为大家解析这个插件的开发过程。首先为接触过android Studio插件开发的同学可以先阅读:学会编写Android Studio插件 别停留在用的程度了。
简书原文地址:Android Studio插件开发-SteadyoungIOC注解生成器:Steadyoung-CodePlug
分析
下面看看上期中自动生成代码的效果:
Alt+Insert 智能插入 steadyoungioc.gif
自动生成注解代码,跟ButterKnife的插件类似,但是我们自己写的插件生成的注解代码更加符合google源码规范,而且是基于我们 自己简易打造的IOC注解框架:SteadyoungIOC 。我参考了ButterKnife的源码,因为源码过于复杂,为了提高上手度,只引用了部分功能。
我们先来整理一下思路,要实现这么个插件我们需要做一些什么东东:
- 获取光标所在行的布局文件 --> R.layout.xxxx.xml;
- 搜索整个项目获取到R.layout.xxxx.xml文件;
- 通过该布局文件去遍历找出含有id的布局标签,当然如果考虑完善一点需要考虑include等等;
- 遍历完成后生成对话框,让用户可以自己选择需要生成注解的View以及点击事件,这个是Java GUI里面的内容
- 最后当用户点击确定生成最终的注解代码即可
这么说起来还是挺简单的,当然其中的细节还是让人很蛋疼的,需要不断反复的调试。
实现
- 获取光标所在行的布局文件 --> R.layout.xxxx.xml;
/**
* 获取当前光标的layout文件
*/
private String getCurrentLayout(Editor editor)
Document document = editor.getDocument();
CaretModel caretModel = editor.getCaretModel();
int caretOffset = caretModel.getOffset();
int lineNum = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
String layoutMatching = "R.layout.";
if (!TextUtils.isEmpty(lineContent) && lineContent.contains(layoutMatching))
// 获取layout文件的字符串
int startPosition = lineContent.indexOf(layoutMatching) + layoutMatching.length();
int endPosition = lineContent.indexOf(")", startPosition);
String layoutStr = lineContent.substring(startPosition, endPosition);
// 可能是另外一种情况 View.inflate
if (layoutStr.contains(","))
endPosition = lineContent.indexOf(",", startPosition);
layoutStr = lineContent.substring(startPosition, endPosition);
return layoutStr;
return null;
- 搜索整个项目获取到R.layout.xxxx.xml文件;
@Override
public void actionPerformed(AnActionEvent e)
// 获取project
Project project = e.getProject();
// 获取选中内容
final Editor mEditor = e.getData(PlatformDataKeys.EDITOR);
if (null == mEditor)
return;
SelectionModel model = mEditor.getSelectionModel();
mSelectedText = model.getSelectedText();
// 未选中布局内容,显示dialog
if (TextUtils.isEmpty(mSelectedText))
// 获取光标所在位置的布局
mSelectedText = getCurrentLayout(mEditor);
if (TextUtils.isEmpty(mSelectedText))
mSelectedText = Messages.showInputDialog(project, "布局内容:(不需要输入R.layout.)", "未选中布局内容,请输入layout文件名", Messages.getInformationIcon());
if (TextUtils.isEmpty(mSelectedText))
Util.showPopupBalloon(mEditor, "未输入layout文件名", 5);
return;
// 获取布局文件,通过FilenameIndex.getFilesByName获取
// GlobalSearchScope.allScope(project)搜索整个项目
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, mSelectedText + ".xml", GlobalSearchScope.allScope(project));
if (psiFiles.length <= 0)
Util.showPopupBalloon(mEditor, "未找到选中的布局文件" + mSelectedText, 5);
return;
XmlFile xmlFile = (XmlFile) psiFiles[0];
List<Element> elements = new ArrayList<>();
Util.getIDsFromLayout(xmlFile, elements);
// 将代码写入文件,不允许在主线程中进行实时的文件写入
if (elements.size() != 0)
PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(mEditor, project);
PsiClass psiClass = Util.getTargetClass(mEditor, psiFile);
// 有的话就创建变量和findViewById
if (mDialog != null && mDialog.isShowing())
mDialog.cancelDialog();
mDialog = new FindViewByIdDialog(mEditor, project, psiFile, psiClass, elements, mSelectedText);
mDialog.showDialog();
else
Util.showPopupBalloon(mEditor, "未找到任何Id", 5);
- 通过该布局文件去遍历找出含有id的布局标签,当然如果考虑完善一点需要考虑include等等;
/**
* 获取所有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
// 遍历一个文件的所有元素
file.accept(new XmlRecursiveElementVisitor()
@Override
public void visitElement(PsiElement element)
super.visitElement(element);
// 解析Xml标签
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;
- 遍历完成后生成对话框,让用户可以自己选择需要生成注解的View以及点击事件,这个是Java GUI里面的内容,我直接百度找的代码实现了效果,贴出部分源码:
/**
* 解析mElements,并添加到JPanel
*/
private void initContentPanel()
mContentJPanel.removeAll();
// 设置内容
for (int i = 0; i < mElements.size(); i++)
Element mElement = mElements.get(i);
IdBean itemJPanel = new IdBean(new GridLayout(1, 4, 10, 10),
new EmptyBorder(5, 10, 5, 10),
new JCheckBox(mElement.getName()),
new JLabel(mElement.getId()),
new JCheckBox(),
new JTextField(mElement.getFieldName()),
mElement);
// 监听
itemJPanel.setEnableActionListener(this);
itemJPanel.setClickActionListener(clickCheckBox -> mElement.setIsCreateClickMethod(clickCheckBox.isSelected()));
itemJPanel.setFieldFocusListener(fieldJTextField -> mElement.setFieldName(fieldJTextField.getText()));
mContentJPanel.add(itemJPanel);
mContentConstraints.fill = GridBagConstraints.HORIZONTAL;
mContentConstraints.gridwidth = 0;
mContentConstraints.gridx = 0;
mContentConstraints.gridy = i;
mContentConstraints.weightx = 1;
mContentLayout.setConstraints(itemJPanel, mContentConstraints);
mContentJPanel.setLayout(mContentLayout);
jScrollPane = new JBScrollPane(mContentJPanel);
jScrollPane.revalidate();
// 添加到JFrame
getContentPane().add(jScrollPane, 1);
- 最后当用户点击确定生成最终的注解代码即可,主要生成注解@FindView(R.id.XXX)、@OnClick(R.id.XXX)、在OnCreate中生成SteadyoungIOC.jnject(this)等
/**
* 创建变量
*/
private void generateFields()
for (Element element : mElements)
if (mClass.getText().contains("@FindView(" + element.getFullID() + ")"))
// 不创建新的变量
continue;
// 设置变量名,获取text里面的内容
String text = element.getXml().getAttributeValue("android:text");
if (TextUtils.isEmpty(text))
// 如果是text为空,则获取hint里面的内容
text = element.getXml().getAttributeValue("android:hint");
// 如果是@string/app_name类似
if (!TextUtils.isEmpty(text) && text.contains("@string/"))
text = text.replace("@string/", "");
// 获取strings.xml
PsiFile[] psiFiles = FilenameIndex.getFilesByName(mProject, "strings.xml", GlobalSearchScope.allScope(mProject));
if (psiFiles.length > 0)
for (PsiFile psiFile : psiFiles)
// 获取src\\main\\res\\values下面的strings.xml文件
String dirName = psiFile.getParent().toString();
if (dirName.contains("src\\\\main\\\\res\\\\values"))
text = Util.getTextFromStringsXml(psiFile, text);
StringBuilder fromText = new StringBuilder();
if (!TextUtils.isEmpty(text))
fromText.append("/****" + text + "****/\\n");
fromText.append("@FindView(" + element.getFullID() + ")\\n");
fromText.append("private ");
fromText.append(element.getName());
fromText.append(" ");
fromText.append(element.getFieldName());
fromText.append(";");
// 创建点击方法
if (element.isCreateFiled())
// 添加到class
mClass.add(mFactory.createFieldFromText(fromText.toString(), mClass));
/**
* 创建OnClick方法
*/
private void generateOnClickMethod()
for (Element element : mElements)
// 可以使用并且可以点击
if (element.isCreateClickMethod())
// 需要创建OnClick方法
String methodName = getClickMethodName(element) + "Click";
PsiMethod[] onClickMethods = mClass.findMethodsByName(methodName, true);
boolean clickMethodExist = onClickMethods.length > 0;
if (!clickMethodExist)
// 创建点击方法
createClickMethod(methodName, element);
/**
* 创建一个点击事件
*/
private void createClickMethod(String methodName, Element element)
// 拼接方法的字符串
StringBuilder methodBuilder = new StringBuilder();
methodBuilder.append("@OnClick(" + element.getFullID() + ")\\n");
methodBuilder.append("private void " + methodName + "(" + element.getName() + " " + getClickMethodName(element) + ")");
methodBuilder.append("\\n");
// 创建OnClick方法
mClass.add(mFactory.createMethodFromText(methodBuilder.toString(), mClass));
/**
* 获取点击方法的名称
*/
public String getClickMethodName(Element element)
String[] names = element.getId().split("_");
// aaBbCc
StringBuilder sb = new StringBuilder();
for (int i = 0; i < names.length; i++)
if (i == 0)
sb.append(names[i]);
else
sb.append(Util.firstToUpperCase(names[i]));
return sb.toString();
/**
* 在加载布局后根据activity Fragement View 来初始化注解框架
*/
private void generateInjects()
PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.app.Activity", new EverythingGlobalScope(mProject));
PsiClass fragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.app.Fragment", new EverythingGlobalScope(mProject));
PsiClass supportFragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
"android.support.v4.app.Fragment", new EverythingGlobalScope(mProject));
// Check for Activity class
if (activityClass != null && mClass.isInheritor(activityClass, true))
generateActivityBind();
// Check for Fragment class
// else if ((fragmentClass != null && mClass.isInheritor(fragmentClass, true)) || (supportFragmentClass != null && mClass.isInheritor(supportFragmentClass, true)))
// generateFragmentBindAndUnbind();
//
/**
* activity在加载布局后生成ViewUtils.inject(this)代码
*/
private void generateActivityBind()
PsiElementFactory mFactory = JavaPsiFacade.getElementFactory(mProject);
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(...) invocation\\n");
method.append(VIEW_BIND);
method.append("(this);\\n");
method.append("");
mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
else
PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
if (!containsViewInjectLine(onCreate, VIEW_BIND))
for (PsiStatement statement : onCreate.getBody().getStatements())
// Search for setContentView()
if (statement.getFirstChild() instanceof PsiMethodCallExpression)
PsiReferenceExpression methodExpression
= ((PsiMethodCallExpression) statement.getFirstChild())
.getMethodExpression();
// Insert ButterKnife.inject()/ButterKnife.bind() after setContentView()
if (methodExpression.getText().equals("setContentView"))
onCreate.getBody().addAfter(mFactory.createStatementFromText(
VIEW_BIND + "(this);", mClass), statement);
break;
/**
* 判断OnCreate中是否有初始化注解框架代码
* @param method
* @param line
* @return
*/
private boolean containsViewInjectLine(PsiMethod method, String line)
final PsiCodeBlock body = method.getBody();
if (body == null)
return false;
PsiStatement[] statements = body.getStatements();
for (PsiStatement psiStatement : statements)
String statementAsString = psiStatement.getText();
if (psiStatement instanceof PsiExpressionStatement && (statementAsString.contains(line)))
return true;
return false;
学习Android Studio插件开发需要一些时间,如果时间够可以多了解,加班比较多那么先用着这些好用的插件,了解大概插件开发流程就够了,主要精力还是要在Android开发中。
插件源码地址:https://github.com/Steadyoung/SteadyoungIOC-CodePlug
同款框架源码地址:https://github.com/Steadyoung/SteadyoungIOC
以上是关于Android Studio插件开发-SteadyoungIOC注解生成器:Steadyoung-CodePlug的主要内容,如果未能解决你的问题,请参考以下文章