自己编写Android Studio插件

作者: 叫我旺仔 | 来源:发表于2016-12-05 15:14 被阅读1135次

    前言

    为何会自己写插件呢,原因有两个,一个是之前看到鸿神写了一篇学会编写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文件,用来配置信息的注册和声明。

    plugin.xml
    • id:(com.example.plugin.Name)插件的ID,保证插件上传仓库后的唯一性。
    • name:插件名称。
    • version:版本号。
    • description:插件的简介。
    • change-notes:版本更新信息。
    • extensions:扩展组件注册 。

    开始编写

    创建一个Action,是继承AnAction类的

    右键src目录->New->Action

    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

    编写Action

    action
    点击ok之后会生成相应的Action,在Action里面的actionPerformed方法会在点击菜单或者快捷键的是否触发。

    思路

    在获取布局文件内容后自动解析布局文件并生成字段和findViewById代码。

    1.获取布局文件
    2.解析布局文件,获取属性
    3.将代码写入action

    获取布局文件

    查找文件需要用到PsiFile类,通过FilenameIndex.getFilesByName(project, name, scope)来查找布局文件。
    先获取用户选中内容,如果没选中,则弹出dialog让用户输入内容;

    
            // 获取project
            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)) {
                // 未选中布局内容,显示dialog
                selectedText = Messages.showInputDialog(project, "layout(不需要输入R.layout.):" , "未选中布局内容,请输入layout文件名", Messages.getInformationIcon());
                if (TextUtils.isEmpty(selectedText)) {
                    Utils.showPopupBalloon(mEditor, "未输入layout文件名");
                    return;
                }
            }
    

    然后根据输入的内容查找xml文件;

            // 获取布局文件,通过FilenameIndex.getFilesByName获取
            // GlobalSearchScope.allScope(project)搜索整个项目
            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) {
            // 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;
        }
    
        /**
         * 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];
        }
    

    对应的实体类Element,里面包含获取id的值,获取类型如(TextView或者com.example.CustomView),根据id设置变量名。

        // 判断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;
    
        /**
         * 构造函数
         *
         * @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;
        }
    
        // 第一个字母大写
        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;
            // 获取Factory
            mFactory = JavaPsiFacade.getElementFactory(mProject);
            mSelectText = selectText;
        }
    
        @Override
        protected void run() throws Throwable {
            generateFields();
            generateFindViewById();
            // 重写class
            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) {
    
                // 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;
                }
                // 设置变量
                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));
            }
        }
    
        /**
         * 设置变量的值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));
            // 判断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()写到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);
                }
            }
        }
    
        /**
         * 写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));
            }
    
        }
    }
    

    使用插件

    导出插件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

    相关文章

      网友评论

      本文标题:自己编写Android Studio插件

      本文链接:https://www.haomeiwen.com/subject/pjokmttx.html