阅读 228

徒手撸一个框架-ButterKnife三步走

实现目标:

  1. 通过 @BindView() 实现ID自动绑定
  2. 通过 @OnClick() 实现点击事件自动绑定

ButterKnife简易原理解析:

  1. 通过 @BindView()@OnClick() 注解获得控件的ID及类型
  2. 拿到控件ID后自动生成一个以 XXActivity_ViewBinding 格式为文件名的java文件
  3. 在生成 XXActivity_ViewBinding 的java文件(类文件)的同时,声明一个 bind(Activity target) 方法并写入以下语句:
    target.textView=(WidgetType)fb(viewId)fb(viewId).setOnClickListener()
  4. 通过 ButterKnife.bind(Activity activity) 传入 Activity 实例并 执行 步骤四中的语句

步骤分析

通过以上的分析我们就可以明确以下 极简 步骤:

  1. 声明两个注解 @BindView()@OnClick()
  2. 实现注解处理器
  3. 执行注解处理器生成的类文件中的方法

开始撸码

码前准备,环境配置

  1. 新建两个module,分别命名为annotations(用来声明注解)、annotation_compiler(用来处理注解)
  2. module - annptation_compiler 下的 build.gradle 中添加注解处理依赖包
    (别问,问就是Google出的,用来处理注解的,AS3.4.1以下只需要compileOnly一行)
   annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
   compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
复制代码

顺手Sync下

  1. module 之间相互依赖
    module(app) 依赖
   implementation project(path: ':annotations')
   annotationProcessor project(path: ':annotation_compiler')
复制代码

annotation_compiler module 依赖

    implementation project(path: ':annotations')
复制代码

准备工作到此结束

工程目录

先看结构再看码

实现步骤1-声明注解

annotation module 中新建两个注解类

分别实现以下方法:
绑定控件的注解是一个注解对应一个控件,所以每次只获取一个控件Id,注解接收一个int值

绑定点击事件的注解需要接收多个控件的Id,所以注解接收一个int[]

备注:

  1. 注意interface关键字前面的 @ 注解标识
  2. @Target :即明确该注解作用在哪种对象上面( eg: ElementType.METHOD - 注解作用在方法上,ElementType.FIELD - 注解作用在成员变量上)
  3. @Rentention :即明确该注解在何时生效( eg: RetentionPolicy.SOURCE - 源码期,RetentionPolicy.RUNTIME - 运行时)
  4. default 缺省值:-1=View.NoId

实现步骤2-注解处理

捋一下这里的思路

每一个带有 @BindView@OnClick 注解声明的Activity类都要相应的生成一个XXActivity_ViewBinding类,并在类中声明一个 bind(Activity target) 方法,该方法能够接收 不同的Activity对象

  1. 声明一个接口类
    由于每个自动生成的XXActivity_ViewBinding类中都有一个bind(Activity target) 方法,所以我们抽象一个接口出来并最终让XXActivity_ViewBinding类实现该接口,并且利用泛型来保证每个实现该接口的方法都能接收不同的Activity对象module(app) 下新建一个Binder接口: 这里的T将会用来 传递Activity对象 ,这样我们就可以在 bind() 方法里轻松调用 fb()setOnClcikListner()
  2. 创建注解处理类

  注解处理类的功能才是真正用来进行注解处理并且创建我们需要的java类文件的
   a. 在module(annotation_compiler)下新建AnnotationComPiler类

@AutoService(Processor.class)  //注册注解处理器,此处用到环境配置中依赖的AutoService库
public class AnnotationCompiler extends AbstractProcessor {}
复制代码

  b. 声明Filer对象
  注意:Filer对象配合Writer对象可以创建一个有内容的文件

    //声明文件对象(成员变量)
 private Filer filer;

 @Override
 public synchronized void init(ProcessingEnvironment processingEnvironment) {
     super.init(processingEnvironment);
     //就是这么写的,别问
     filer = processingEnvironment.getFiler();
 }
复制代码

  c. 重写getSupportedAnnotationTypes(),筛选我们的目标注解

    /**
     * 声明注解处理器要处理的注解
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        //getCanonicalName()获取的是包名+类名
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }
复制代码

  d. 重写getSupportedSourceVersion(),声明支持的java版本
  这里我们用默认就可以

    /**
     * 声明注解处理器支持的java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        //processingEnv这是父类变量
        return processingEnv.getSourceVersion();
    }  
复制代码

  d. 重写process(),生成XXActivity_ViewBinding类文件
  首先看一下我们需要的XXActivity_ViewBinding类的样子

//声明包路径
package com.junt.annotationdemo;
//由于实现了Binder接口,所以要声明导入Binder
import com.junt.annotationdemo.Binder;
//点击事件需要实例化View.OnClickListener()接口,所以还要声明导入View包
import android.view.View;

public class MainActivity_ViewBinding implements Binder<com.junt.annotationdemo.MainActivity>{
    @Override
    public void bind(final com.junt.annotationdemo.MainActivity target) {
    //首先对于即用到了@BindView又用到了@OnClick的控件,我们在生成代码时应该需要以下内容
    //textView-说明我们需要获取控件名,
    //(android.widget.TextView)-说明我们还需要知道控件的类型
    //2131165319-控件的Id
        target.textView=(android.widget.TextView)target.findViewById(2131165319);
        target.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    target.onClick(view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    //其次,对于仅使用了@OnClick的控件,仅需要一个Id即可
        target.findViewById(2131165320).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    target.onClick(view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}
复制代码

  在生成的java类文件中写入代码说明
比如我们需要写入

package com.junt.annotationdemo;
复制代码

只需要调用

writer.write("package com.junt.annotationdemo;\n")
复制代码

全部流程代码

    /**
     * 写一个自动findViewById的文件
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
        //获取所有用到@BindView的节点元素
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        
        //根据节点所在的Activity进行分类(一个Activity中会有多个@BindBiew),便于接下来创建每一个单独的XXActivity_ViewBinding类文件
        Map<String, List<VariableElement>> mapBindView = new HashMap<>();
        for (Element element : elementsAnnotatedWith) {
            //获取控件元素
            VariableElement variableElement = (VariableElement) element;
            //获取控件元素的父元素Name(activity或fragment)
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
            List<VariableElement> variableElements = mapBindView.get(activityName);
            if (variableElements == null) {
                variableElements = new ArrayList<>();
                mapBindView.put(activityName, variableElements);
            }
            variableElements.add(variableElement);
        }

        //获取所有用到@onClick的节点
        Set<? extends Element> elementsAnnotatedWithClick=roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        
        //同样根据Activity进行分类(一个Activity中仅有一个@OnClick)
        Map<String, Element> mapClickView = new HashMap<>();
        for (Element element : elementsAnnotatedWithClick) {
            String activityName = element.getEnclosingElement().getSimpleName().toString();
            mapClickView.put(activityName, element);
        }

        //开始创建XXActivity_ViewBinding类文件
        Writer writer = null;
        //用For循环,每一次循环写一个xxActivity_ViewBinding
        for (String activityName : mapBindView.keySet()) {  
         
            //取出activityName下的带注解控件元素
            List<VariableElement> variableElements = mapBindView.get(activityName);
            
            //获取activity所在包名(同一个Activity下的元素的父元素都是该Activity,所以任意取一个就行,这里取0)
            Element enclosingElement = variableElements.get(0).getEnclosingElement();
            String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
            
            try {
                //实例化一个JavaFileObject对象(我们写的是.java文件--类文件)
                JavaFileObject sourceFile = filer.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding");
                //实例化writer对象用来写入向.java文件中写入代码
                writer = sourceFile.openWriter();
                
                //1.写入-导包代码
                writer.write("package " + packageName + ";\n");
                writer.write("import " + packageName + ".Binder;\n");
                writer.write("import android.view.View;\n");
                
                //2.写入-声明类及实现接口代码
                writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n");
                        
                //3.写入-实现接口方法代码
                writer.write("    @Override\n" +
                        "    public void bind(final " + packageName + "." + activityName + " target) {\n");
                        
                //取出带onCLick注解的元素Id
                Element clickVariableElement = mapClickView.get(activityName);
                int[] clickIds = clickVariableElement.getAnnotation(OnClick.class).value();
                List<Integer> id = new ArrayList<>(clickIds.length);

                //4.写入-所有控件findViewById代码
                for (VariableElement variableElement : variableElements) {
                    //获取控件Name
                    String variableName = variableElement.getSimpleName().toString();
                    //获取控件ID
                    int variableId = variableElement.getAnnotation(BindView.class).value();

                    //获取控件Type
                    TypeMirror typeMirror = variableElement.asType();
                    //写FindViewById
                    writeFindViewById(writer, variableName, typeMirror, variableId);
                    //同时看看这个控件元素有没有@OnClick注解,有的话同时写setOnClickListener
                    if (contains(clickIds, variableId)) {
                        //已经设置过点击事件的所有控件Id
                        id.add(variableId);
                        writeSetOnClickListener(writer, packageName, activityName, variableName);
                    }
                }

                for (int clickId : clickIds) {
                    //如果id集合中没有这个clickId则说明这个空间需要设置点击事件,但还没设置点击事件
                    if (!id.contains(clickId)) {
                        writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickId);
                    }
                }
                //5.写入-补全类的构造
                writer.write("    \n    }\n}\n");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
               //这个Activity已经设置过Id绑定与点击事件、故移除
                mapClickView.remove(activityName);

                if (writer != null) {
                    try {
                        writer.close();
                        writer = null;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        //处理Activity中仅有@OnClick绑定的情况,上面的步骤仅是处理了所有含有@BindView的Activity(点击事件也同时处理并且从mapClickView中移除)
        if (mapClickView.size() <= 0) {
            return false;
        }
        for (String activityName : mapClickView.keySet()) {
            Element element = mapClickView.get(activityName);
            String packageName = processingEnv.getElementUtils().getPackageOf(element.getEnclosingElement()).toString();
            
            try {
                JavaFileObject sourceFile = filer.createSourceFile(
                        packageName + "." + activityName + "_ViewBinding");
                writer = sourceFile.openWriter();
                
                //1.写入-导包代码
                writer.write("package " + packageName + ";\n");
                writer.write("import " + packageName + ".Binder;\n");
                writer.write("import android.view.View;\n");
                
                //2.写入-声明类及实现接口代码
                writer.write("public class " + activityName + "_ViewBinding implements Binder<"
                        + packageName + "." + activityName + ">{\n");
                
                //3.写入-实现接口方法代码
                writer.write("    @Override\n" +
                        "    public void bind(final " + packageName + "." + activityName + " target) {\n");
                
                //取出改Activity下所有@OnClick的Id
                int[] clickViewIds = element.getAnnotation(OnClick.class).value();
                
                //4.写入-setOnClickListener代码
                for (int clickViewId : clickViewIds) {
                    writeSetOnClickListenerWithoutName(writer, packageName, activityName, clickViewId);
                }
                
                //5.补全类构造
                writer.write("    \n    }\n}\n");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (writer!=null){
                        writer.close();
                        writer = null;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }
    
    
    /**
     * 写入findViewById代码
     * @param writer writer
     * @param variableName 控件名
     * @param typeMirror 控件类型
     * @param variableId 控件Id
     * @throws IOException 写入异常
     */
    private void writeFindViewById(Writer writer, String variableName, TypeMirror typeMirror, int variableId) throws IOException {
        writer.write("        target." + variableName + "=(" + typeMirror + ")target.findViewById(" + variableId + ");\n");
    }

    /**
     * 已经调用过finfViewById()方法的控件设置点击事件
     * @param writer writer
     * @param variableName 控件名
     * @throws IOException 写入异常
     */
    private void writeSetOnClickListener(Writer writer, String packageName, String className, String variableName) throws IOException {
        writer.write("        target." + variableName + ".setOnClickListener(new View.OnClickListener() {\n" +
                "            @Override\n" +
                "            public void onClick(View view) {\n" +
                "                try {\n" +
                "                    target.onClick(view);\n" +
                "                } catch (Exception e) {\n" +
                "                    e.printStackTrace();\n" +
                "                }\n" +
                "            }\n" +
                "        });\n");
    }

    /**
     * 没有调用过findViewById()方法的控件设置点击事件
     * @param writer writer
     * @param viewId 控件Id
     * @throws IOException 写入异常
     */
    private void writeSetOnClickListenerWithoutName(Writer writer, String packageName, String className, int viewId) throws IOException {
        writer.write("        target.findViewById(" + viewId + ")" + ".setOnClickListener(new View.OnClickListener() {\n" +
                "            @Override\n" +
                "            public void onClick(View view) {\n" +
                "                try {\n" +
                "                    target.onClick(view);\n" +
                "                } catch (Exception e) {\n" +
                "                    e.printStackTrace();\n" +
                "                }\n" +
                "            }\n" +
                "        });\n");
    }

    /**
     * 判断数组中是否含有某个值
     * 这里用来判断,所有设置@OnClick注解的控件Id中是否也同时设置了@BindView
     * @param arr 某一个Activity下所有设置了@OnClick的控件Id
     * @param arg 某一个Activity下设置了@BindView的控件Id
     * @return true/false
     */
    private boolean contains(int[] arr, int arg) {
        boolean isContain = false;
        if (arr.length == 0) {
            isContain = false;
        } else {
            for (int i : arr) {
                if (i == arg) {
                    isContain = true;
                    break;
                }
            }
        }
        return isContain;
    }
复制代码

实现步骤3-执行XXActivity_ViewBinding中的onBind()方法

module(app)新建一个ButterKnife类

在Activity中使用