1.概述
上一期我们已经分享了Android Studio插件开发之 - 基础入门篇。那么现在我们来动手写一个IOC注解生成器,有点类似于ButterKnife的插件一样自动给我们生成代码,在网上找了很多资料国内基本就在HelloWorld阶段,也有很多哥们向我反应插件的代码还是有点蒙B。代码方面能理解就理解,不理解也不强求,如果你能改一改别人已经写好的插件就最好了,实在不行我们干脆也别折腾了大不了不用,本文章旨在给自己想写一些奇葩插件的哥们一些引导。
废话不多说,请看具体效果:

GIF.gif
自动生成注解代码,跟ButterKnife的插件类似,但是我们自己写的插件生成的注解代码更加符合google源码规范,而且是基于我们自己动手打造一套IOC注解框架。当然我们可以去参考ButterKnife的插件是怎么写的,但是我看了一下里面的东西太多了,我们干脆自己来吧。
所有分享大纲:2017Android进阶之路与你同行
视频讲解地址:http://pan.baidu.com/s/1gf40cV5
2.实现
2.1 思路整理
我们先来整理一下思路,要实现这么个插件我们需要做一些什么东东:
- 获取光标所在行的布局文件 --> R.layout.xxxx.xml;
- 搜索整个项目获取到R.layout.xxxx.xml文件;
- 通过该布局文件去遍历找出含有id的布局标签,当然如果考虑完善一点需要考虑include等等;
- 遍历完成后生成对话框,让用户可以自己选择需要生成注解的View以及点击事件,这个是Java GUI里面的内容
- 最后当用户点击确定生成最终的注解代码即可
这么说起来还是挺简单的,当然其中的细节还是让人很蛋疼的,需要不断反复的调试。
2.2 具体实现
- 获取光标所在行的布局文件 --> R.layout.xxxx.xml;
1 /** 2 * 获取当前光标的layout文件 3 */ 4 private String getCurrentLayout(Editor editor) { 5 Document document = editor.getDocument(); 6 CaretModel caretModel = editor.getCaretModel(); 7 int caretOffset = caretModel.getOffset(); 8 int lineNum = document.getLineNumber(caretOffset); 9 int lineStartOffset = document.getLineStartOffset(lineNum); 10 int lineEndOffset = document.getLineEndOffset(lineNum); 11 // 获取当前光标所在行的所有内容 12 String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset)); 13 String layoutMatching = "R.layout."; 14 if (!TextUtils.isEmpty(lineContent) && lineContent.contains(layoutMatching)) { 15 // 获取layout文件的字符串 16 int startPosition = lineContent.indexOf(layoutMatching) + layoutMatching.length(); 17 int endPosition = lineContent.indexOf(")", startPosition); 18 String layoutStr = lineContent.substring(startPosition, endPosition); 19 // 可能是另外一种情况 View.inflate 20 if (layoutStr.contains(",")) { 21 endPosition = lineContent.indexOf(",", startPosition); 22 layoutStr = lineContent.substring(startPosition, endPosition); 23 } 24 return layoutStr; 25 } 26 return null; 27 }
- 搜索整个项目获取到R.layout.xxxx.xml文件;
1 @Override 2 public void actionPerformed(AnActionEvent e) { 3 // 获取project 4 Project project = e.getProject(); 5 // 获取选中内容 6 final Editor mEditor = e.getData(PlatformDataKeys.EDITOR); 7 if (null == mEditor) { 8 return; 9 } 10 SelectionModel model = mEditor.getSelectionModel(); 11 mSelectedText = model.getSelectedText(); 12 // 未选中布局内容,显示dialog 13 if (TextUtils.isEmpty(mSelectedText)) { 14 // 获取光标所在位置的布局 15 mSelectedText = getCurrentLayout(mEditor); 16 if (TextUtils.isEmpty(mSelectedText)) { 17 mSelectedText = Messages.showInputDialog(project, "布局内容:(不需要输入R.layout.)", "未选中布局内容,请输入layout文件名", Messages.getInformationIcon()); 18 if (TextUtils.isEmpty(mSelectedText)) { 19 Util.showPopupBalloon(mEditor, "未输入layout文件名", 5); 20 return; 21 } 22 } 23 } 24 // 获取布局文件,通过FilenameIndex.getFilesByName获取 25 // GlobalSearchScope.allScope(project)搜索整个项目 26 PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, mSelectedText + ".xml", GlobalSearchScope.allScope(project)); 27 if (psiFiles.length <= 0) { 28 Util.showPopupBalloon(mEditor, "未找到选中的布局文件" + mSelectedText, 5); 29 return; 30 } 31 XmlFile xmlFile = (XmlFile) psiFiles[0]; 32 List<Element> elements = new ArrayList<>(); 33 Util.getIDsFromLayout(xmlFile, elements); 34 // 将代码写入文件,不允许在主线程中进行实时的文件写入 35 if (elements.size() != 0) { 36 PsiFile psiFile = PsiUtilBase.getPsiFileInEditor(mEditor, project); 37 PsiClass psiClass = Util.getTargetClass(mEditor, psiFile); 38 // 有的话就创建变量和findViewById 39 if (mDialog != null && mDialog.isShowing()) { 40 mDialog.cancelDialog(); 41 } 42 mDialog = new FindViewByIdDialog(mEditor, project, psiFile, psiClass, elements, mSelectedText); 43 mDialog.showDialog(); 44 } else { 45 Util.showPopupBalloon(mEditor, "未找到任何Id", 5); 46 } 47 }
- 通过该布局文件去遍历找出含有id的布局标签,当然如果考虑完善一点需要考虑include等等;
1 /** 2 * 获取所有id 3 * 4 * @param file 5 * @param elements 6 * @return 7 */ 8public static java.util.List<Element> getIDsFromLayout(final PsiFile file, final java.util.List<Element> elements) { 9 // To iterate over the elements in a file 10 // 遍历一个文件的所有元素 11 file.accept(new XmlRecursiveElementVisitor() { 12 @Override 13 public void visitElement(PsiElement element) { 14 super.visitElement(element); 15 // 解析Xml标签 16 if (element instanceof XmlTag) { 17 XmlTag tag = (XmlTag) element; 18 // 获取Tag的名字(TextView)或者自定义 19 String name = tag.getName(); 20 // 如果有include 21 if (name.equalsIgnoreCase("include")) { 22 // 获取布局 23 XmlAttribute layout = tag.getAttribute("layout", null); 24 // 获取project 25 Project project = file.getProject(); 26 // 布局文件 27 XmlFile include = null; 28 PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, getLayoutName(layout.getValue()) + ".xml", GlobalSearchScope.allScope(project)); 29 if (psiFiles.length > 0) { 30 include = (XmlFile) psiFiles[0]; 31 } 32 if (include != null) { 33 // 递归 34 getIDsFromLayout(include, elements); 35 return; 36 } 37 } 38 // 获取id字段属性 39 XmlAttribute id = tag.getAttribute("android:id", null); 40 if (id == null) { 41 return; 42 } 43 // 获取id的值 44 String idValue = id.getValue(); 45 if (idValue == null) { 46 return; 47 } 48 XmlAttribute aClass = tag.getAttribute("class", null); 49 if (aClass != null) { 50 name = aClass.getValue(); 51 } 52 // 添加到list 53 try { 54 Element e = new Element(name, idValue, tag); 55 elements.add(e); 56 } catch (IllegalArgumentException e) { 57 58 } 59 } 60 } 61 }); 62 return elements; 63}
- 遍历完成后生成对话框,让用户可以自己选择需要生成注解的View以及点击事件,这个是Java GUI里面的内容,我就大致黏贴一部分代码把,把对话框中间的部分黏贴出来,具体可以去github下载源码
1 /** 2 * 解析mElements,并添加到JPanel 3 */ 4 private void initContentPanel() { 5 mContentJPanel.removeAll(); 6 // 设置内容 7 for (int i = 0; i < mElements.size(); i++) { 8 Element mElement = mElements.get(i); 9 IdBean itemJPanel = new IdBean(new GridLayout(1, 4, 10, 10), 10 new EmptyBorder(5, 10, 5, 10), 11 new JCheckBox(mElement.getName()), 12 new JLabel(mElement.getId()), 13 new JCheckBox(), 14 new JTextField(mElement.getFieldName()), 15 mElement); 16 // 监听 17 itemJPanel.setEnableActionListener(this); 18 itemJPanel.setClickActionListener(clickCheckBox -> mElement.setIsCreateClickMethod(clickCheckBox.isSelected())); 19 itemJPanel.setFieldFocusListener(fieldJTextField -> mElement.setFieldName(fieldJTextField.getText())); 20 mContentJPanel.add(itemJPanel); 21 mContentConstraints.fill = GridBagConstraints.HORIZONTAL; 22 mContentConstraints.gridwidth = 0; 23 mContentConstraints.gridx = 0; 24 mContentConstraints.gridy = i; 25 mContentConstraints.weightx = 1; 26 mContentLayout.setConstraints(itemJPanel, mContentConstraints); 27 } 28 mContentJPanel.setLayout(mContentLayout); 29 jScrollPane = new JBScrollPane(mContentJPanel); 30 jScrollPane.revalidate(); 31 // 添加到JFrame 32 getContentPane().add(jScrollPane, 1); 33 }
1 /** 2 * 创建注解View变量 3 */ 4 private void generateFields() { 5 for (Element element : mElements) { 6 if (mClass.getText().contains("@ViewById(" + element.getFullID() + ")")) { 7 // 不创建新的变量 8 continue; 9 } 10 // 设置变量名,获取text里面的内容 11 String text = element.getXml().getAttributeValue("android:text"); 12 if (TextUtils.isEmpty(text)) { 13 // 如果是text为空,则获取hint里面的内容 14 text = element.getXml().getAttributeValue("android:hint"); 15 } 16 // 如果是@string/app_name类似 17 if (!TextUtils.isEmpty(text) && text.contains("@string/")) { 18 text = text.replace("@string/", ""); 19 // 获取strings.xml 20 PsiFile[] psiFiles = FilenameIndex.getFilesByName(mProject, "strings.xml", GlobalSearchScope.allScope(mProject)); 21 if (psiFiles.length > 0) { 22 for (PsiFile psiFile : psiFiles) { 23 // 获取src\main\res\values下面的strings.xml文件 24 String dirName = psiFile.getParent().toString(); 25 if (dirName.contains("src\\main\\res\\values")) { 26 text = Util.getTextFromStringsXml(psiFile, text); 27 } 28 } 29 } 30 } 31 32 StringBuilder fromText = new StringBuilder(); 33 if (!TextUtils.isEmpty(text)) { 34 fromText.append("/****" + text + "****/\n"); 35 } 36 fromText.append("@ViewById(" + element.getFullID() + ")\n"); 37 fromText.append("private "); 38 fromText.append(element.getName()); 39 fromText.append(" "); 40 fromText.append(element.getFieldName()); 41 fromText.append(";"); 42 // 创建点击方法 43 if (element.isCreateFiled()) { 44 // 添加到class 45 mClass.add(mFactory.createFieldFromText(fromText.toString(), mClass)); 46 } 47 } 48 } 49 50 /** 51 * 创建OnClick方法 52 */ 53 private void generateOnClickMethod() { 54 for (Element element : mElements) { 55 // 可以使用并且可以点击 56 if (element.isCreateClickMethod()) { 57 // 需要创建OnClick方法 58 String methodName = getClickMethodName(element) + "Click"; 59 PsiMethod[] onClickMethods = mClass.findMethodsByName(methodName, true); 60 boolean clickMethodExist = onClickMethods.length > 0; 61 if (!clickMethodExist) { 62 // 创建点击方法 63 createClickMethod(methodName, element); 64 } 65 } 66 } 67 } 68 69 /** 70 * 创建一个点击事件 71 */ 72 private void createClickMethod(String methodName, Element element) { 73 // 拼接方法的字符串 74 StringBuilder methodBuilder = new StringBuilder(); 75 methodBuilder.append("@OnClick(" + element.getFullID() + ")\n"); 76 methodBuilder.append("private void " + methodName + "(" + element.getName() + " " + getClickMethodName(element) + "){"); 77 methodBuilder.append("\n}"); 78 // 创建OnClick方法 79 mClass.add(mFactory.createMethodFromText(methodBuilder.toString(), mClass)); 80 } 81 82 /** 83 * 获取点击方法的名称 84 */ 85 public String getClickMethodName(Element element) { 86 String[] names = element.getId().split("_"); 87 // aaBbCc 88 StringBuilder sb = new StringBuilder(); 89 for (int i = 0; i < names.length; i++) { 90 if (i == 0) { 91 sb.append(names[i]); 92 } else { 93 sb.append(Util.firstToUpperCase(names[i])); 94 } 95 } 96 return sb.toString(); 97 }
如果在公司的时候比较闲比如说我,那么还是可以去了解一下插件开发的,我们可以利用它去生成代码或者修改代码找没有用到的资源等等等等,还是蛮不错的,如果是天天加班还是应该考虑一下学习成本,因为有些地方刚接触还是容易蒙B。
附上源码地址:https://github.com/Shenmowen/DarrenIOC
所有分享大纲:2017Android进阶之路与你同行
