Android的View绑定实现----编译时注解实现findViewById和setOnClickListener方式

标题有点长,相信用过xUtils和ButterKnife框架的都知道啥意思,他们都可以通过注解的方式省去繁琐的findViewById和setOnClickListener代码的编写。他们2者的实现原理不一样,前者用的是运行时注解,后者用的是编译时注解,对于不同的注解,会有不同的注解处理器, 针对运行时注解会采用反射机制来处理,针对编译时注解会采用AbstractProcessor来处理,相对来说后者的性能会比前者好点,今天就来介绍下编译时注解的解释器该如何编写。

先来看看注解的使用

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.btn_title)
    TextView mTitleBtn;

    @InjectView(R.id.btn_desc)
    TextView mDescBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //注入
        ViewInjector.inject(this);

        //注入后就可以使用了
        mTitleBtn.setText("我是标题");
        mDescBtn.setText("我是描述");


    }
    
    //设置点击事件
    @InjectMethod({R.id.btn_title, R.id.btn_desc})
    public void showToast(View v) {
        switch (v.getId()) {
            case R.id.btn_title:
                Toast.makeText(this, "标题被点击了", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_desc:
                Toast.makeText(this, "描述被点击了", Toast.LENGTH_SHORT).show();
                break;
        }
    }

}


编译工程后,会在项目的\build\generated\ap_generated_sources\debug\out目录下自动生成一份java代码,效果如下所示:

// Generated code from ViewInjector. Do not modify!
package blog.csdn.net.mchenys.essayjoke;
import mchenys.ViewInjector.ViewBinder;
import blog.csdn.net.mchenys.essayjoke.MainActivity;
import android.view.View;
public class MainActivityInjector implements ViewBinder<MainActivity> {
    @Override
    public void bind(MainActivity args){
        args.mTitleBtn=(android.widget.TextView)args.findViewById(2131165219);
        args.mDescBtn=(android.widget.TextView)args.findViewById(2131165218);
        args.mTitleBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                args.showToast(v);
            }
        });
        args.mDescBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                args.showToast(v);
            }
        });
    }
}

效果图:

先从注解开始介绍,@InjectView和@InjectMethod定义如下:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface InjectView {
    int value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectMethod {
    int[] value();
}

非常简单,@Target用于声明作用的范围,ElementType.FIELD表示只能用在成员变量上,ElementType.METHOD表示只能用在方法上,除了这里用到的2个,它还可以有以下类型:

ElementType.TYPE:只能用在类、接口或者枚举类型。
ElementType.PARAMETER:只能用在参数上。
ElementType.CONSTRUCTOR:只能用在构造方法上。
ElementType.LOCAL_VARIABLE:只能用在局部变量上。
ElementType.ANNOTATION_TYPE:只能用在注解上。
ElementType.PACKAGE:只能用在包上。
ElementType.TYPE_PARAMETER:只能用在类型参数上。
而@Retention表示注解的保留策略,它一共有3种:

RetentionPolicy.SOURCE:源码级注解,只会把注解信息保留在.java文件中,源码在编译时会被丢弃。
RetentionPolicy.CLASS:编译时注解,默认级别,注解信息会保留在.java和.class文件中,当运行java程序时,JVM会丢弃该注解信息。
RetentionPolicy.RUNTIME:运行时注解,当运行java程序时,JVM会保留改注解信息,可以通过反射获取该注解信息。
我这里用的就是编译时注解,下面开始介绍编译时注解处理器的开发步骤。

首先,你得新建一个java Library,注意是一定是java Library而不是android Library,否则会找不到AbstractProcessor类,我这里建的库的名字就叫processor,工程项目如下所示:

其中processor包内的ViewBindProcessor就是注解处理器类,它是继承至AbstractProcessor类的,继承后需要实现process抽象方法:

public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

该方法相当于每个处理器的主函数,在这里可以编写处理注解的代码,以及生成java文件,参数annotations表示需要给该处理器解释的注解的集合,roundEnv参数可以用来查找使用了特定注解的元素。

除此之外,还有3个方法会用到,分别是:

public synchronized void init(ProcessingEnvironment processingEnv) {}

public Set<String> getSupportedAnnotationTypes() {}

public SourceVersion getSupportedSourceVersion() {}   

init:该方法是初始化的地方,我们可以通过ProcessingEnvironment获取到很多有用的工具类,比如Elements(提供了一些和元素相关的操作,如获取所在包的包名等)、Types(提供了和类型相关的一些操作,如获取父类、判断两个类是不是父子关系等)、Filer(用于文件操作,用它去创建生成的代码文件)、Messager(用来打印信息的,它会打印出Element所在的源代码)等等。
getSupportedAnnotationTypes:该方法必须指定,它是指定哪些注解需要被该处理器解释的,需要将要处理的注解的全名放到Set中返回。
getSupportedSourceVersion:这个方法用来指定支持的java版本,通常这里返回SourceVersion。latestSupported().
在java7之后,也可以使用注解来代替getSupportedAnnotationTypes和getSupportedSourceVersion方法。例如:

@SupportedAnnotationTypes({"mchenys.annotations.InjectView",
        "mchenys.annotations.InjectMethod"})//指定这个注解处理器是给哪些注
@SupportedSourceVersion(SourceVersion.RELEASE_7)//设置java版本
public class ViewBindProcessor extends AbstractProcessor {
}

如何注册Processor
编写完我们的Processor之后需要将它注册给java编译器,为了能使用注解处理器,需要用一个服务文件来注册它,关于服务文件的创建有2种方式,先来介绍比较麻烦的一种,步骤如下:
1.首先,在processor库的src/main目录下创建resources/META-INF/services/javax.annotation.processing.Processor文件(即创建resources目录,在resources目录下创建META-INF目录,继续在META-INF目录下创建services目录,最后在services目录下创建javax.annotation.processing.Processor文件)

2.然后编辑javax.annotation.processing.Processor文件,写入自定义的Processor的全路径名称,如果有多个Processor的话,每一行写一个。
例如:mchenys.processor.ViewBindProcessor

如果你觉得这种方式太过繁琐,别急,我马上介绍一种比较简便的方式,使用Google开源的AutoService库来生成服务文件,这个需要在processor库的gradle文件中添加2行依赖:

implementation 'com.google.auto.service:auto-service:1.0-rc6'
//gradle 5.0会忽略annotationProcessor,因此需要手动添加
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

然后在自定义的注解处理器类上添加@AutoService(Processor.class)注解,这样就搞定了。编译processor库,你会发现在\build\classes目录下自动生成了服务文件:


ViewInjector工具类介绍
在正式介绍ViewBindProcessor之前,我先介绍下这个工具类,代码如下:

/**
 * 注解注入工具类
 */
public class ViewInjector {

    //定义注解生成类实现的接口
    public interface ViewBinder<T> {
        void bind(T t);
    }

    //存储已实例化过的注解生成类
    static final Map<Class, ViewBinder> BINDERS = new LinkedHashMap<>();

    //给注解生成类注入Activity
    public static void inject(Object target) {
        Class clazz = target.getClass();
        ViewBinder viewBinder = BINDERS.get(clazz);
        if (viewBinder == null) {
            try {
                viewBinder = (ViewBinder) Class.forName(clazz.getName() + ViewBindProcessor.GEN_CLASS_SUFFIX)
                        .newInstance();
                BINDERS.put(clazz, viewBinder);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (null != viewBinder) {
            viewBinder.bind(target); //执行绑定
        }

    }
}


代码也很简单,我的思路就是在调用inject的方法时候给说有实现了ViewBinder接口的类注入Activity的实例,拿到了Activity的实例,我就可以操作findViewById得到对应的View了,进而又可以给View设置点击监听了,至于ViewBinder接口的实现类从哪里来?当然是通过注解处理器动态生成啦,因此ViewBindProcessor的作用就是找到使用了指定注解类的元素,然后生成对应的代码。

ViewBindProcessor
直接贴代码,看注释也能看懂啥意思了

/**
 * 自定义编译时注解解释器
 */
@AutoService(Processor.class) //自动的注册解释器
@SupportedAnnotationTypes({"mchenys.annotations.InjectView",
        "mchenys.annotations.InjectMethod"})//指定这个注解处理器是给哪些注解使用的.
@SupportedSourceVersion(SourceVersion.RELEASE_7)//设置java版本
public class ViewBindProcessor extends AbstractProcessor {

    public static final String GEN_CLASS_SUFFIX = "Injector";

    private Types mTypeUtils;//提供了和类型相关的一些操作,如获取父类、判断两个类是不是父子关系等
    private Elements mElementUtils;//提供了一些和元素相关的操作,如获取所在包的包名等
    private Filer mFiler;//Filer用于文件操作,用它去创建生成的代码文件
    private Messager mMessager;//用来打印信息的,它会打印出Element所在的源代码

    //init方法是初始化的地方,我们可以通过ProcessingEnvironment获取到很多有用的工具类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mTypeUtils = processingEnv.getTypeUtils();
        mElementUtils = processingEnv.getElementUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();

    }

    //该方法相当于处理器的main函数,在这里写你的扫描/评估和处理逻辑,以及生成java代码,RoundEnvironment可以查询包含
    //特定注解的被注解元素
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {


        //获取与InjectView注解相关的所有元素
        Set<? extends Element> elements = parse2Set(annotations, roundEnv);

        //获取分类后的集合
        Map<TypeElement, List<Element>> elementMap = parse2Map(elements);

        //遍历map,生成代码
        for (Map.Entry<TypeElement, List<Element>> entry : elementMap.entrySet()) {
            //生成注入代码
            generateInjectorCode(entry.getKey(), entry.getValue());
        }
        return true;
    }

    /**
     * 整合所有使用了注解的元素
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    private Set<? extends Element> parse2Set(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<Element> set = new LinkedHashSet<>();
       /* for (TypeElement annotation : annotations) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
            set.addAll(elements);
        }*/
        //为保证字段先解析,这里手动添加
        set.addAll(roundEnv.getElementsAnnotatedWith(InjectView.class));
        set.addAll(roundEnv.getElementsAnnotatedWith(InjectMethod.class));
        return set;
    }

    /**
     * 将元素按TypeElement进行分类
     *
     * @param elements
     * @return
     */
    private Map<TypeElement, List<Element>> parse2Map(Set<? extends Element> elements) {

        Map<TypeElement, List<Element>> elementMap = new LinkedHashMap<>();

        //遍历所有被InjectView注释的元素
        for (Element element : elements) {
            //获取所在类的信息(一般指的是Activity/Fragment)
            TypeElement clazz = (TypeElement) element.getEnclosingElement();
            //按类存入map中,key相同会被覆盖,key代表的是元素所在的类信息,value是一个List
            //这样就可以把某个类和该类上使用了特定注解所有元素进行关联
            addElement(elementMap, clazz, element);
        }
        return elementMap;
    }


    //递归判断android.view.View是不是其父类
    private boolean isView(TypeMirror type) {
        //获取所有父类类型
        List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
        if (supers.size() == 0) {
            return false;
        }
        for (TypeMirror superType : supers) {
            if (superType.toString().equals("android.view.View") || isView(superType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 添加元素到map集合中
     *
     * @param elementMap key=clazz value=List<Element>
     * @param clazz      指定元素所在类的信息
     * @param element    指定元素
     */
    private void addElement(Map<TypeElement, List<Element>> elementMap,
                            TypeElement clazz, Element element) {
        List<Element> list = elementMap.get(clazz);
        if (list == null) {
            list = new ArrayList<>();
            elementMap.put(clazz, list);
        }
        list.add(element);
    }

    /**
     * 生成注入代码
     *
     * @param typeElement 元素所在类的Element
     * @param elements    需要注入的元素
     */
    private void generateInjectorCode(TypeElement typeElement, List<Element> elements) {

        //取出所在的类名
        String className = typeElement.getSimpleName().toString();
        String qualifiedName = typeElement.getQualifiedName().toString();
        //该类所在的包名
        String packageName = mElementUtils.getPackageOf(typeElement).asType().toString();
        //存储所有的字段名
        Map<Integer, String> fieldMap = new HashMap<>();

        //开始编写java类
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code from ViewInjector. Do not modify!\n");
        builder.append("package " + packageName + ";\n"); //声明包
        builder.append("import mchenys.ViewInjector.ViewBinder;\n");//导包
        builder.append("import " + qualifiedName + ";\n");//导包
        builder.append("import android.view.View;\n");//导包
        builder.append("public class " + className + GEN_CLASS_SUFFIX + " implements ViewBinder<" + className + "> {\n");//声明类实现ViewBinder接口
        builder.append("\t@Override\n");
        builder.append("\tpublic void bind(" + className + " args" + "){\n");//定义方法


        //解析注解
        for (Element element : elements) {
            if (element.getKind() == ElementKind.FIELD) {
                //如果不是View的子类则报错
                if (!isView(element.asType())) {
                    mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
                }
                //处理字段的注入
                //获取变量类型
                String type = element.asType().toString();
                //获取变量名
                String fieldName = element.getSimpleName().toString();
                //id
                int resourceId = element.getAnnotation(InjectView.class).value();

                //将id和字段名关联存储
                fieldMap.put(resourceId, fieldName);

                //开始findViewById并赋值
                builder.append("\t\targs." + fieldName + "=(" + type + ")args.findViewById(" + resourceId + ");\n");


            } else if (element.getKind() == ElementKind.METHOD) {
                //处理方法
                int[] ids = element.getAnnotation(InjectMethod.class).value();
                String methodName = element.getSimpleName().toString();
                for (int id : ids) {
                    //获取对应id的字段名
                    String fieldName = fieldMap.get(id);
                    if (null != fieldName) {
                        builder.append("\t\targs." + fieldName + ".setOnClickListener(new View.OnClickListener() {\n");
                    } else {
                        builder.append("\t\targs.findViewById(" + id + ").setOnClickListener(new View.OnClickListener() {\n");
                    }
                    builder.append("\t\t\t@Override\n")
                            .append("\t\t\tpublic void onClick(View v) {\n")
                            .append("\t\t\t\targs." + methodName + "(v);\n")
                            .append("\t\t\t}\n")
                            .append("\t\t});\n");
                }

            } else {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a FIELD or METHOD ", element);
            }
        }
        //添加结尾
        builder.append("\t}\n").append("}");

        //生成代码
        generateCode(className + GEN_CLASS_SUFFIX, builder.toString());

    }

    /**
     * 生成代码
     *
     * @param className java文件名
     * @param code      java代码
     */
    private void generateCode(String className, String code) {
        try {
            JavaFileObject file = mFiler.createSourceFile(className);
            Writer writer = file.openWriter();//拿到输出流
            writer.write(code);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面类中出现了很多Element接口,在process方法中使用getElementsAnnotatedWith获取到的都是Element接口,其实我们用Element.getKind获取到类型之后可以将他们强转成对应的子接口,这些子接口提供了一些针对性的操作,这些子接口有:

TypeElement:表示一个类或接口元素。
PackageElement:表示一个包元素。
VariableElement:表示一个属性、enum 常量、方法或构造方法参数、局部变量或异常参数。
ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
ok,注解处理器弄好了,该如何使用呢?在Android项目的app的gradle文件中添加下依赖:

 annotationProcessor project(path: ':processor')
 implementation project(path: ':processor')
1
2
很简单,简单说一下这里为什么要用同时使用annotationProcessor 和implementation 来添加依赖,前者是专门用来添加注解处理器的,后者的添加是因为,我的ViewInjector工具类以及InjectMethod和InjectView注解都放在了processor库中,所以还需要单独引进来,你也可以把工具类和注解类单独放到一个java Library,这样Android项目和processor库都需要添加一下注解所在的库。

ButterKnife 7.0.1之后的版本就是把注解和处理器单独分开为独立的库,使用的时候需要同时引入2者,也就是这样:

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
}

这里有个坑,顺带提一下,在很多文章,你会看到引入处理器库的时候是使用android-apt插件的,这个插件在gradle2.2之后的版本被annotationProcessor替代了,官方已经宣布不再维护android-apt这个插件了,如果你的gradle版本比较低,那么引入的步骤就比较麻烦了,大致有3个步骤如下:
1.android工程(project)的build.gradle下的dependencies下添加:
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’

2.android工程(app)的build.gradle的dependencies中以apt的方式引入注解处理器,例如:
apt project(’:processor’)

3.android工程((app)的build.gradle文件顶部添加:
apply plugin: ‘com.neenbedankt.android-apt’

支持Fragment的使用
先来看看使用效果:

在MainActivity的内部添加了一个Fragment,代码如下:

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.btn_title)
    TextView mTitleBtn;

    @InjectView(R.id.btn_desc)
    TextView mDescBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //注入
        ViewInjector.inject(this);

        //注入后就可以使用了
        mTitleBtn.setText("我是标题");
        mDescBtn.setText("我是描述");

        getSupportFragmentManager().beginTransaction()
                .replace(R.id.fl_container, new MainFragment())
                .commitAllowingStateLoss();

    }

    //设置点击事件
    @InjectMethod({R.id.btn_title, R.id.btn_desc})
    public void showToast(View v) {
        switch (v.getId()) {
            case R.id.btn_title:
                Toast.makeText(this, "标题被点击了", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_desc:
                Toast.makeText(this, "描述被点击了", Toast.LENGTH_SHORT).show();
                break;
        }
    }
    
    //内部的Fragment
    public static class MainFragment  extends Fragment {
        private View rootView;

        @InjectView(R.id.tv_info)
        TextView mInfoTv;

        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {

            if (rootView == null) {
                rootView = inflater.inflate(R.layout.fragment_main, container, false);

                //注入
                ViewInjector.inject(this, rootView);

            }else{
                ViewParent parent = rootView.getParent();
                if (null != parent) {
                    ((ViewGroup) parent).removeView(rootView);
                }
            }
            return rootView;
        }

        @Override
        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);

            mInfoTv.setText("我是Fragment");
        }

        @InjectMethod(R.id.tv_info)
        public void onClick(View view) {
            Toast.makeText(getActivity(), "fragment被点击了", Toast.LENGTH_SHORT).show();
        }
    }

}

在Fragment注入的地方是用的是ViewInjector.inject(this, rootView);进行注入,很显然,这里需要对ViewInjector以及注解处理器做相应的修改

修改ViewInjector
/**
 * 注解注入工具类
 */
public class ViewInjector {

    //定义注解生成类实现的接口
    public interface ViewBinder<T> {
        void bind(T t, Object source);
    }

    //存储已实例化过的注解生成类
    static final Map<Class, ViewBinder> BINDERS = new LinkedHashMap<>();

    /**
     * 注入Activity时用这个
     *
     * @param activity
     */
    public static void inject(Object activity) {
        inject(activity, activity);
    }

    /**
     * 给注解生成类注入值
     *
     * @param target Activity/Fragment
     * @param source target=Activity时,是Activity,如果是target=Fragment时,就传根Fragment的根View
     */
    public static void inject(Object target, Object source) {
        Class clazz = target.getClass();
        ViewBinder viewBinder = BINDERS.get(clazz);
        if (viewBinder == null) {
            try {
                String packageName = clazz.getPackage().getName();
                String className = clazz.getSimpleName();
                String qualifiedName = packageName + "." + className + ViewBindProcessor.GEN_CLASS_SUFFIX;
                viewBinder = (ViewBinder) Class.forName(qualifiedName).newInstance();
                BINDERS.put(clazz, viewBinder);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (null != viewBinder) {
            viewBinder.bind(target, source); //执行绑定
        }

    }
}


修改ViewBindProcessor
主要是generateInjectorCode方法的修改,如下所示:

/**
     * 生成注入代码
     *
     * @param typeElement 元素所在类的Element
     * @param elements    需要注入的元素
     */
    private void generateInjectorCode(TypeElement typeElement, List<Element> elements) {

        //取出所在的类名
        String className = typeElement.getSimpleName().toString();
        String qualifiedName = typeElement.getQualifiedName().toString();
        //该类所在的包名
        String packageName = mElementUtils.getPackageOf(typeElement).asType().toString();
        //存储所有的字段名
        Map<Integer, String> fieldMap = new HashMap<>();

        //开始编写java类
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code from ViewInjector. Do not modify!\n");
        builder.append("package " + packageName + ";\n"); //声明包
        builder.append("import mchenys.ViewInjector.ViewBinder;\n");//导包
        builder.append("import " + qualifiedName + ";\n");//导包
        builder.append("import android.view.*;\n");//导包
        builder.append("public class " + className + GEN_CLASS_SUFFIX + " implements ViewBinder<" + className + "> {\n");//声明类实现ViewBinder接口
        builder.append("\t@Override\n");
        builder.append("\tpublic void bind(" + className + " target, Object source){\n");//定义方法

        //判断是否是Fragment
        boolean isFragment = isFragment(typeElement.asType());
        if (isFragment) {
            builder.append("\t\tViewGroup rootView = (ViewGroup)source;\n");
        }

        //解析注解
        for (Element element : elements) {
            if (element.getKind() == ElementKind.FIELD) {
                //如果不是View的子类则报错
                if (!isView(element.asType())) {
                    mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View", element);
                }
                //处理字段的注入
                //获取变量类型
                String type = element.asType().toString();
                //获取变量名
                String fieldName = element.getSimpleName().toString();
                //id
                int resourceId = element.getAnnotation(InjectView.class).value();

                //将id和字段名关联存储
                fieldMap.put(resourceId, fieldName);

                //开始findViewById并赋值
                if (isFragment) {
                    builder.append("\t\ttarget." + fieldName + "=(" + type + ")rootView.findViewById(" + resourceId + ");\n");
                } else {
                    builder.append("\t\ttarget." + fieldName + "=(" + type + ")target.findViewById(" + resourceId + ");\n");
                }
            } else if (element.getKind() == ElementKind.METHOD) {
                //处理方法
                int[] ids = element.getAnnotation(InjectMethod.class).value();
                //得到方法名
                String methodName = element.getSimpleName().toString();
                for (int id : ids) {
                    //获取对应id的字段名
                    String fieldName = fieldMap.get(id);
                    if (null != fieldName) {
                        builder.append("\t\ttarget." + fieldName + ".setOnClickListener(new View.OnClickListener() {\n");
                    } else {
                        if (isFragment) {
                            builder.append("\t\trootView");
                        }else{
                            builder.append("\t\ttarget");
                        }
                        builder.append(".findViewById(" + id + ").setOnClickListener(new View.OnClickListener() {\n");
                    }
                    builder.append("\t\t\t@Override\n")
                            .append("\t\t\tpublic void onClick(View v) {\n")
                            .append("\t\t\t\ttarget." + methodName + "(v);\n")
                            .append("\t\t\t}\n")
                            .append("\t\t});\n");
                }

            } else {
                mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a FIELD or METHOD ", element);
            }


        }
        //添加结尾
        builder.append("\t}\n").append("}");

        //生成代码
        generateCode(className + GEN_CLASS_SUFFIX, builder.toString());

    }

以及添加了一个判断当前类是否是Fragment的方法

//递归判断是否是Fragment
private boolean isFragment(TypeMirror type) {
    //获取所有父类类型
    List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(type);
    if (supers.size() == 0) {
        return false;
    }
    for (TypeMirror superType : supers) {
        if (superType.toString().equals("android.support.v4.app.Fragment")
                || isFragment(superType)) {
            return true;
        }
    }
    return false;
}

搞定,编译android项目的时候,会看到项目的\build\generated\ap_generated_sources\debug\out目录下会多了2个自动生成的java文件:

其中MainFragmentInjector的代码如下:

// Generated code from ViewInjector. Do not modify!
package blog.csdn.net.mchenys.essayjoke;
import mchenys.ViewInjector.ViewBinder;
import blog.csdn.net.mchenys.essayjoke.MainActivity.MainFragment;
import android.view.*;
public class MainFragmentInjector implements ViewBinder<MainFragment> {
    @Override
    public void bind(MainFragment target, Object source){
        ViewGroup rootView = (ViewGroup)source;
        target.mInfoTv=(android.widget.TextView)rootView.findViewById(2131165328);
        target.mInfoTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                target.onClick(v);
            }
        });
    }
}

内容就是在注解处理器的generateInjectorCode的生成结果。

扩展
对于InjectMethod声明的方法,如果想使用空参数的方法,亦或者执行方法的时候还有可能会有异常抛出,而我不想在APP上直接看到crash的弹弹窗,这些都是可以实现的,修改处理器的generateInjectorCode方法,针对方法处理的逻辑修改如下:

else if (element.getKind() == ElementKind.METHOD) {
//处理方法
 int[] ids = element.getAnnotation(InjectMethod.class).value();
 //得到方法名
 String methodName = element.getSimpleName().toString();

 //判断方法是否有参数,如果有的话参数类型必须是View的子类,且最多只能有一个参数
 ExecutableElement executableElement = (ExecutableElement) element;
 List<? extends VariableElement> parameters = executableElement.getParameters();
 boolean hasParams = !parameters.isEmpty();
 if (hasParams) {
     VariableElement variableElement = parameters.get(0);
     if (!isView(variableElement.asType())) {
         mMessager.printMessage(Diagnostic.Kind.ERROR, variableElement.asType().toString()
         +" is not a View parameter", variableElement);
     }
     if (parameters.size() != 1) {
         mMessager.printMessage(Diagnostic.Kind.ERROR, "can be at most one parameter", executableElement);
     }
 }

 for (int id : ids) {
     //获取对应id的字段名
     String fieldName = fieldMap.get(id);
     //设置点击事件
     if (null != fieldName) {
         builder.append("\t\ttarget." + fieldName + ".setOnClickListener(new View.OnClickListener() {\n");
     } else {
         if (isFragment) {
             builder.append("\t\trootView");
         } else {
             builder.append("\t\ttarget");
         }
         builder.append(".findViewById(" + id + ").setOnClickListener(new View.OnClickListener() {\n");
     }
     builder.append("\t\t\t@Override\n")
             .append("\t\t\tpublic void onClick(View v) {\n")
             .append("\t\t\t\ttry {\n"); //添加try cache
     if (hasParams) {
         builder.append("\t\t\t\t\ttarget." + methodName + "(v);\n"); //执行带参方法
     }else{
         builder.append("\t\t\t\t\ttarget." + methodName + "();\n");//执行空参方法
     }
     builder.append("\t\t\t\t} catch (Exception ex) {\n")
             .append("\t\t\t\t\tex.printStackTrace();\n")
             .append("\t\t\t\t}\n")
             .append("\t\t\t}\n")
             .append("\t\t});\n");
 }
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值