当前位置:   article > 正文

Android Studio插件开发-SteadyoungIOC注解生成器:Steadyoung-CodePlug_python编写摩托罗拉cps程序保存的ctb文件

python编写摩托罗拉cps程序保存的ctb文件

前言

上一篇文章我已经分享了自己简易打造的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;
  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. String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
  12. String layoutMatching = "R.layout.";
  13. if (!TextUtils.isEmpty(lineContent) && lineContent.contains(layoutMatching)) {
  14. // 获取layout文件的字符串
  15. int startPosition = lineContent.indexOf(layoutMatching) + layoutMatching.length();
  16. int endPosition = lineContent.indexOf(")", startPosition);
  17. String layoutStr = lineContent.substring(startPosition, endPosition);
  18. // 可能是另外一种情况 View.inflate
  19. if (layoutStr.contains(",")) {
  20. endPosition = lineContent.indexOf(",", startPosition);
  21. layoutStr = lineContent.substring(startPosition, endPosition);
  22. }
  23. return layoutStr;
  24. }
  25. return null;
  26. }
  • 搜索整个项目获取到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. */
  8. public 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. return elements;
  62. }
  • 遍历完成后生成对话框,让用户可以自己选择需要生成注解的View以及点击事件,这个是Java GUI里面的内容,我直接百度找的代码实现了效果,贴出部分源码:
  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. }
  • 最后当用户点击确定生成最终的注解代码即可,主要生成注解@FindView(R.id.XXX)、@OnClick(R.id.XXX)、在OnCreate中生成SteadyoungIOC.jnject(this)等
  1. /**
  2. * 创建变量
  3. */
  4. private void generateFields() {
  5. for (Element element : mElements) {
  6. if (mClass.getText().contains("@FindView(" + 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. StringBuilder fromText = new StringBuilder();
  32. if (!TextUtils.isEmpty(text)) {
  33. fromText.append("/****" + text + "****/\n");
  34. }
  35. fromText.append("@FindView(" + element.getFullID() + ")\n");
  36. fromText.append("private ");
  37. fromText.append(element.getName());
  38. fromText.append(" ");
  39. fromText.append(element.getFieldName());
  40. fromText.append(";");
  41. // 创建点击方法
  42. if (element.isCreateFiled()) {
  43. // 添加到class
  44. mClass.add(mFactory.createFieldFromText(fromText.toString(), mClass));
  45. }
  46. }
  47. }
  48. /**
  49. * 创建OnClick方法
  50. */
  51. private void generateOnClickMethod() {
  52. for (Element element : mElements) {
  53. // 可以使用并且可以点击
  54. if (element.isCreateClickMethod()) {
  55. // 需要创建OnClick方法
  56. String methodName = getClickMethodName(element) + "Click";
  57. PsiMethod[] onClickMethods = mClass.findMethodsByName(methodName, true);
  58. boolean clickMethodExist = onClickMethods.length > 0;
  59. if (!clickMethodExist) {
  60. // 创建点击方法
  61. createClickMethod(methodName, element);
  62. }
  63. }
  64. }
  65. }
  66. /**
  67. * 创建一个点击事件
  68. */
  69. private void createClickMethod(String methodName, Element element) {
  70. // 拼接方法的字符串
  71. StringBuilder methodBuilder = new StringBuilder();
  72. methodBuilder.append("@OnClick(" + element.getFullID() + ")\n");
  73. methodBuilder.append("private void " + methodName + "(" + element.getName() + " " + getClickMethodName(element) + "){");
  74. methodBuilder.append("\n}");
  75. // 创建OnClick方法
  76. mClass.add(mFactory.createMethodFromText(methodBuilder.toString(), mClass));
  77. }
  78. /**
  79. * 获取点击方法的名称
  80. */
  81. public String getClickMethodName(Element element) {
  82. String[] names = element.getId().split("_");
  83. // aaBbCc
  84. StringBuilder sb = new StringBuilder();
  85. for (int i = 0; i < names.length; i++) {
  86. if (i == 0) {
  87. sb.append(names[i]);
  88. } else {
  89. sb.append(Util.firstToUpperCase(names[i]));
  90. }
  91. }
  92. return sb.toString();
  93. }
  94. /**
  95. * 在加载布局后根据activity Fragement View 来初始化注解框架
  96. */
  97. private void generateInjects() {
  98. PsiClass activityClass = JavaPsiFacade.getInstance(mProject).findClass(
  99. "android.app.Activity", new EverythingGlobalScope(mProject));
  100. PsiClass fragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
  101. "android.app.Fragment", new EverythingGlobalScope(mProject));
  102. PsiClass supportFragmentClass = JavaPsiFacade.getInstance(mProject).findClass(
  103. "android.support.v4.app.Fragment", new EverythingGlobalScope(mProject));
  104. // Check for Activity class
  105. if (activityClass != null && mClass.isInheritor(activityClass, true)) {
  106. generateActivityBind();
  107. // Check for Fragment class
  108. }
  109. // else if ((fragmentClass != null && mClass.isInheritor(fragmentClass, true)) || (supportFragmentClass != null && mClass.isInheritor(supportFragmentClass, true))) {
  110. // generateFragmentBindAndUnbind();
  111. // }
  112. }
  113. /**
  114. * activity在加载布局后生成ViewUtils.inject(this)代码
  115. */
  116. private void generateActivityBind() {
  117. PsiElementFactory mFactory = JavaPsiFacade.getElementFactory(mProject);
  118. if (mClass.findMethodsByName("onCreate", false).length == 0) {
  119. // Add an empty stub of onCreate()
  120. StringBuilder method = new StringBuilder();
  121. method.append("@Override protected void onCreate(android.os.Bundle savedInstanceState) {\n");
  122. method.append("super.onCreate(savedInstanceState);\n");
  123. method.append("\t// TODO: add setContentView(...) invocation\n");
  124. method.append(VIEW_BIND);
  125. method.append("(this);\n");
  126. method.append("}");
  127. mClass.add(mFactory.createMethodFromText(method.toString(), mClass));
  128. } else {
  129. PsiMethod onCreate = mClass.findMethodsByName("onCreate", false)[0];
  130. if (!containsViewInjectLine(onCreate, VIEW_BIND)) {
  131. for (PsiStatement statement : onCreate.getBody().getStatements()) {
  132. // Search for setContentView()
  133. if (statement.getFirstChild() instanceof PsiMethodCallExpression) {
  134. PsiReferenceExpression methodExpression
  135. = ((PsiMethodCallExpression) statement.getFirstChild())
  136. .getMethodExpression();
  137. // Insert ButterKnife.inject()/ButterKnife.bind() after setContentView()
  138. if (methodExpression.getText().equals("setContentView")) {
  139. onCreate.getBody().addAfter(mFactory.createStatementFromText(
  140. VIEW_BIND + "(this);", mClass), statement);
  141. break;
  142. }
  143. }
  144. }
  145. }
  146. }
  147. }
  148. /**
  149. * 判断OnCreate中是否有初始化注解框架代码
  150. * @param method
  151. * @param line
  152. * @return
  153. */
  154. private boolean containsViewInjectLine(PsiMethod method, String line) {
  155. final PsiCodeBlock body = method.getBody();
  156. if (body == null) {
  157. return false;
  158. }
  159. PsiStatement[] statements = body.getStatements();
  160. for (PsiStatement psiStatement : statements) {
  161. String statementAsString = psiStatement.getText();
  162. if (psiStatement instanceof PsiExpressionStatement && (statementAsString.contains(line))) {
  163. return true;
  164. }
  165. }
  166. return false;
  167. }

学习Android Studio插件开发需要一些时间,如果时间够可以多了解,加班比较多那么先用着这些好用的插件,了解大概插件开发流程就够了,主要精力还是要在Android开发中。
插件源码地址:https://github.com/Steadyoung/SteadyoungIOC-CodePlug
同款框架源码地址:https://github.com/Steadyoung/SteadyoungIOC

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号