Android Studio插件开发

2,353 阅读6分钟

前言

在开发插件前,我们先提一个需求,就是使用java语言开发安卓,实现页面不直接通过findViewById来获取控件并设置点击事件(不可以使用kotlin开发或ButterKnife之类的插件。)。

一.需求实现

需求实现原理

在activity,fragment或其他的类中先把要用到的类声明好,通过反射的方式获取声明好的属性名称。通过属性名称来获取R文件中的控件,然后再为控件添加上点击的监听器即可。是不是很简单咧。

代码分析

public void bindView(Object obj,Activity activity, View.OnClickListener clickListener){
        bindView(obj,null,clickListener,activity);
    }

    public void bindView(Object obj,View v, View.OnClickListener clickListener,Activity activity){
        Field[] field = obj.getClass().getDeclaredFields();
        for (Field f:field){
            try {
                f.setAccessible(true);
                Class clz = f.getType();
                //如果是基本数据类型,不处理,直接跳过
                if (clz.isPrimitive()){
                    continue;
                }
                //判断实例是不是view的子类,不是直接跳过,不处理
                if (!View.class.isAssignableFrom(f.getType())){
                    continue;
                }
                //获取属性名称
                String name = f.toString().substring(f.toString().lastIndexOf(".")+1);
                //通过属性名称获取view的id
                int id = activity.getResources().getIdentifier(name,"id",activity.getPackageName());
                //实例化view
                View view;
                if (v == null){
                    view = activity.findViewById(id);
                }else {
                    view = v.findViewById(id);
                }
                //设置点击事件
                if (clickListener != null && view != null){
                    view.setOnClickListener(clickListener);
                }
                //设置view
                f.set(obj,view);
            } catch (IllegalAccessException e) {
                Log.e("FindViewUtilsError","reason:"+e.getLocalizedMessage());
                e.printStackTrace();
            }
        }
    }

上面的代码其实已经可以使用一句话代替findViewById了。当然,调用上面的代码位置必须是页面装载完成后,否则view都是空的。上面虽然实现了不用频繁的findViewById和略过了设置点击监听器的代码,但是我们仍然要手动声明控件的属性名称,同时属性名称必须和layout中的id一致,这不仅容易出错,还有点麻烦,毕竟属性名称一写错,view就为空了,这是不能忍的。那么怎么实现连同代码声明这个步骤一并去掉呢,这时候我们就可以开发一个插件来实现了。

二.插件开发

插件开发工具准备

Android Studio本身这个工具就是针对安卓开发的,所以Android Studio本身是无法支持插件开发的,开发插件我们可以用到IntelliJ IDEA去进行开发。IntelliJ IDEA社区版是免费的,所以可以选择社区版进行开发。

建立项目工程


建完工程后切记在src目录下建立包名,否则有可能出现插件可以在intellij idea下允许,但在android studio下允许报错的情况。建立完成后我们需要给插件创建动作,步骤如下图:

经过上图的几个步骤后,工程基本搭建完成了。下面就开始编写Action中的代码了。

代码编写

public class Test extends AnAction {
    private HashMap<String,String> map;

    @Override
    public void actionPerformed(AnActionEvent e) {
        //获取编辑器
        Editor data = e.getData(PlatformDataKeys.EDITOR);
        //获取鼠标选中区域
        SelectionModel selectionModel = data.getSelectionModel();
        if (selectionModel == null){
            return;
        }
        // TODO: insert action logic here
        //这个map用于保存要写入的声明对象的类和类名。
        map = new HashMap<>();
        //读取xml文件。selectionModel.getSelectedText()这句话为获取鼠标点击高亮区部分的文字内容。下面整句话是在工程中寻找名字为xxx.xml的文件。xxx值就是鼠标选中的高亮区的文字。
        PsiFileSystemItem[] timeUtils = FilenameIndex.getFilesByName(e.getProject(), selectionModel.getSelectedText()+".xml", GlobalSearchScope.allScope(e.getProject()), false);
        //如果没找到直接结束掉操作。
        if (timeUtils != null && timeUtils.length != 0){
            //理论上来说,xml文件大多数情况下名字是唯一的,所以这里直接取了第一个xml文件来解析
            VirtualFile virtualFile = timeUtils[0].getVirtualFile();
            //获取对应的文件
            PsiFile manifestFile = PsiManager.getInstance(e.getProject()).findFile( virtualFile);
            // XmlDocument xml = (XmlDocument) manifestFile.getOriginalFile();
            //将文件解析为xml文件
            XmlFile xmlFile = (XmlFile) PsiManager.getInstance(e.getProject()).findFile(virtualFile);
            XmlDocument document = xmlFile.getDocument();
            if (document != null) {
                //获取顶层的xml内容
                XmlTag rootTag = document.getRootTag();
                if (rootTag != null) {
                    //获取顶层后的下一层内容
                    XmlTag[] subTags = rootTag.getSubTags();
                    for (XmlTag tag : subTags) {
                        //获取个各标签的内容,里面的方法使用了递归。
                        getTag(tag);
                    }
                }
            }
        }else {
            return;
        }
        //======================下面的代码是将声明和引用写入到java文件中去==================================
        Document document = data.getDocument();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //这个stringBuffer为最终需要写入的内容
                StringBuffer stringBuffer = new StringBuffer();
                //这个map用于装载要引入的包,之所以使用hashmap,因为hashmap键不可重复,避免重复引入包名。
                HashMap<String,String> importPackage = new HashMap<>();
                for (String key:map.keySet()){
                    if (!map.get(key).contains(".")){
                        //应为没有包含“.”的一般是一些基本控件,基本控件基本都是出自android.widget里面,所以这里就固定引入android.widget.+控件类名
                        importPackage.put(map.get(key),"import android.widget."+map.get(key)+";\n");
                        stringBuffer.append("\tprivate "+map.get(key)+" "+key+";\n");
                    }else {
                        //如果包含“.”的,可能是自定义布局或谷歌后面加入的布局,这些布局一般前面都带有一串包名加上控件类名,这种我们直接引入即可。
                        importPackage.put(map.get(key),"import "+map.get(key)+";\n");
                        stringBuffer.append("\tprivate "+map.get(key).substring(map.get(key).lastIndexOf(".")+1)+" "+key+";\n");
                    }
                    //System.out.println(key+","+map.get(key));
                }

                StringBuffer packageResult = new StringBuffer();
                for (String key:importPackage.keySet()){
                    packageResult.append(importPackage.get(key));
                }
                System.out.println(stringBuffer.toString());
                //在类中写入属性。写入位置为类文件中第一次出现“{”的地点的后一行
                document.insertString(document.getText().indexOf("{")+1,"\n"+stringBuffer.toString());
                //写入导包的代码。写入位置为类文件中第一次出现“;”的位置,即package xxx;的后一行
                document.insertString(document.getText().indexOf(";")+1,"\n"+packageResult.toString());

            }
        };
        WriteCommandAction.runWriteCommandAction(e.getProject(),runnable);
    }

    private void getTag(XmlTag tag) {
        //获取标签下的所有属性
        XmlAttribute[] attributes = tag.getAttributes();
        for (XmlAttribute attribute:attributes){
            //我们只需要获取到id的内容和标签的内容即可。这里的id内容是指安卓xml里面的id值,标签内容是指id该id指代的对象的名称,比如LinearLayout等。
            if ("android:id".equals(attribute.getName())){
                //将id和该id指代的对象以键值对的形式保存到hashmap中去。
                map.put(attribute.getValue().split("/")[1],tag.getName());
                //System.out.println(attribute.getValue().split("/")[1]+","+tag.getName());
            }
        }

        //判断该标签下是否存在子标签,如果存在则进行递归调用
        XmlTag[] subTags = tag.getSubTags();
        for (XmlTag t : subTags) {
            getTag(t);
        }
    }
}

写完上面的代码后就可以实现代码注入的功能了。上面的注释也写得很多了,当然,有些接口是intellij idea里面独有的,可能之前没见过,不过不要紧,记住就行。


调试插件

插件写完后自然需要调试一下

不过要注意的是,调试插件时intellij idea会多开一个进程来安装该插件,因为打开的进程是全新的,所以在打开后随便打开一个项目即可。选中一个xml文件布局,使用快捷键或在顶部导航条处触发插件,即可惊奇的发现不到一秒,所有声明一下就添加完了,要引入的包也都引入了。再配合文中最初的那段代码,就可实现前面的需求了。(混淆后的代码无法使用,因为混淆后声明的变量名会变,导致view无法正常找到。我可以想到的就是在声明的属性前添加注解,在反射时读取注解而不是变量名称来实现)

三.总结

上面的插件没有涉及到任何的图形界面,这多少有点low,如果有需要了解图形界面插件的搭建,可以私聊我,或在后面开一篇内容来讲解图形界面插件的搭建及功能实现。