EasyAnnotation框架数据绑定注解(三)

876 阅读4分钟

如何成为T型人才,垂直在一个行业中,必须要有一整套知识体系,在这里,就一个字坚持~

前言

平时开发过程中,经常需要进行数据绑定,由于每次都重复写一下代码,有时候我们就在想,可不可通过一种技术(Annotation)编译出样板代码,提高开发效率,毕竟程序员都喜欢偷懒。这种框架很早之前就有人开发出来(ButterKnife),其实内部源码不复杂,但是却通过编译时注解,APT技术解决我们的问题。那么如何手写该框架(ButterKnife),实现数据绑定?接下来直接代码编码,注解理论知识,请移步到 细聊注解知识点。谢谢~

Contents

一、定义数据绑定注解

  • @BindView:该注解主要解决findViewById控件绑定,并自动类型转换类型。这里我就不细说如何创建注解,可以看我另一篇 细聊注解知识点。@Retention(RetentionPolicy.CLASS) :表示编译时注解,@Target(ElementType.FIELD) :表示修饰字段,注解参数定义为Int类型value值,表示控件ID值。
/**
 * @Author: WeiShuai
 * @Time: 2020/5/13
 * @Description: 控件绑定
 */
@Retention(RetentionPolicy.CLASS)  
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
  • @OnClick: 该注解主要解决OnClickListener事件绑定。@Retention(RetentionPolicy.CLASS)表示编译时注解,@Target(ElementType.METHOD)表示修饰方法,注解参数定义为Int[]类型value值。内部很根据Int[]类型Id值,绑定不同事件,处理不同事件逻辑。
/**
 * @Author: WeiShuai
 * @Time: 2020/5/13
 * @Description: 事件绑定
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

二、自定义注解处理器

不同注解类型,对应不同注解处理器,注解处理器主要作用就是注解解析。在定义编译时注解,我们需要通过继承AbstractProcessor这个类来创建注解处理器,并重写父类方法,一般只需要重新几个重要方法分别:

1、注解初始化
public synchronized void init(ProcessingEnvironment processingEnvironment)
2、注解解析
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
3、注解版本
public SourceVersion getSupportedSourceVersion()
4、解析类型
public Set<String> getSupportedAnnotationTypes()

2.1、注解处理器初始化

AbstractProcessor注解处理器每次初始化只会调用一次init(ProcessingEnvironment processingEnvironment)方法,所以可以用来初始化工具类

  @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //注解处理器工具类
        mProcessorUtils = ProcessorUtils.getInstance(processingEnvironment);
        //解析注解信息类
        mViewBindingGroupedClasses = new ViewBindingGroupedClasses();
        //生产模板对象
        mViewBindingGenerator = new ViewBindingGenerator();
    }

2.2、绑定注解到注解处理器

getSupportedAnnotationTypes()方法是注解处理器能够识别注解类型。这里把两个注解@BindView、@OnClick存储到LinkedHashSet数据结构中,并返回集合,标识注解处理器识别目标注解对象。

@Override
public Set<String> getSupportedAnnotationTypes() {
    LinkedHashSet<String> type = new LinkedHashSet<>(2);
    type.add(BindView.class.getCanonicalName());
    type.add(OnClick.class.getCanonicalName());
    return type;
}

2.3、注解解析处理

RoundEnvironment这个类中提供#getElementsAnnotatedWith()方法,可以根据注解类型返回所有该类型注解所有信息,通过Element可以获取注解值信息。checkValidFiled(fieldElement)是对@BindView修饰变量校验,checkMethodElement(executableElement)是对@OnClick修饰方法校验,校验成功后获取注解信息,并使用APT技术封装成CodeBlock.Builder。ViewBindingGenerator类根据CodeBlock.Builder配置信息,生成样板代码。

@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //获取@BindView注解信息
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //获取@OnClick注解信息
        Set<? extends Element> onClickElements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);

        //校验@BindView,声明变量public View v
        for (Element fieldElement : bindViewElements) {
            if(checkValidFiled(fieldElement)) 
                mViewBindingGroupedClasses.parseBindView(fieldElement);
        }

        //校验@OnClick,声明方法public void onClick(View view){}
        for (Element executableElement : onClickElements) {
            if (checkMethodElement(executableElement))
                mViewBindingGroupedClasses.parseListenerView(executableElement);
        }

        try {
            //解析注解配置信息,并生成写入文件
            HashMap<TypeElement, List<CodeBlock.Builder>> bindMaps = mViewBindingGroupedClasses.getBindMaps();
            if(bindMaps.size()>0) {
                for (Map.Entry<TypeElement, List<CodeBlock.Builder>> entry : bindMaps.entrySet()) {
                    mViewBindingGenerator.generator(entry.getKey(), entry.getValue(), mProcessorUtils, processingEnv);
                }

                mViewBindingGroupedClasses.deleteAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
            mProcessorUtils.eLog(e.getMessage());
        }
        return true;
    }

2.4、注解信息校验

  1. 校验变量访问修饰符,如果变量修饰符public,则返回true,反正返回false。
    /**
     * 校验字段,格式:public View v;
     * @param element
     * @return
     */
    private boolean checkValidFiled(Element element){
        if(element==null) return false;
        //判断变量访问修饰符
        if(!element.getModifiers().contains(Modifier.PUBLIC)){
            mProcessorUtils.eLog("The field $s not public",
                    element.getSimpleName());
            return false;
        }
        return true;
    }
  1. 校验变量访问修饰符,如果方法修饰符,方法返回类型,方法形参个数。
/**
 * 校验方法,格式: public void onClick(View v){}
 * @param element
 * @return
 */
private boolean checkMethodElement(Element element){
    if(element==null) return false;
    ExecutableElement executableElement=(ExecutableElement)element;
    //判断方法访问修饰符
    if(!executableElement.getModifiers().contains(Modifier.PUBLIC)){
        mProcessorUtils.eLog("The method $s not public",
                element.getSimpleName());
        return false;
    }
    //判断方法的返回类型
    TypeMirror returnType = executableElement.getReturnType();
    if(returnType.getKind()!= TypeKind.VOID){
        mProcessorUtils.eLog("The  method $s return type not void",
                element.getSimpleName());
        return false;
    }
    //判断方法返回参数
    List<? extends VariableElement> parameters = executableElement.getParameters();
    if(parameters.size()!=1){
        mProcessorUtils.eLog("The  method $s parameter size entry",
                element.getSimpleName());
        return  false;
    }

    return true;
}

三、解析注解信息,生成样板代码

3.1、解析注解信息

  1. @ViewBind 注解信息解析,获取变量类型、变量名称,注解参数信息,获取数据并封装到CodeBlock.Builder中,并成功返回CodeBlock.Builder对象。
       /**
        * 解析ViewBind注解信息,并转换为CodeBlock.builder数据
        * @return 构造配置信息
        */
       CodeBlock.Builder parseBindView(Element element){
           //读取变量类型
           String type = element.asType().toString();
           //读取变量名
           String name = element.getSimpleName().toString();
           //读取注解信息
           int annotationValue = element.getAnnotation(BindView.class).value();
   
           CodeBlock.Builder builder = CodeBlock.builder()
                   .add("target.$L=",name)
                   .add("($L)source.findViewById($L)",type,annotationValue);
           return builder;
       }
  1. **@OnClick **注解信息解析,该注解参数信息存储事件控件ID数组,遍历数组数据并封装到CodeBlock.Builder中,并成功返回CodeBlock.Builder对象。
       /**
        * 解析OnClick注解信息,并转换为CodeBlock.builder数据
        * @param element
        * @return
        */
   
       ArrayList<CodeBlock.Builder> parseListenerView(Element element){
            //读取注解信息
            int[] annotationValue = element.getAnnotation(OnClick.class).value();
   
            //读取变量名
            String name = element.getSimpleName().toString();
   
            ArrayList<CodeBlock.Builder> builders = new ArrayList<>();
   
            for(int value:annotationValue){
                CodeBlock.Builder builder = CodeBlock.builder()
                        .add("source.findViewById($L).setOnClickListener(new android.view.View.OnClickListener() " +
                                "{ public void onClick(View v) { target.$L(v); }})", value, name);
                builders.add(builder);
            }
            return builders;
        }

3.2、封装注解信息

根据解析出来数据CodeBlock.Builder,需要根据不同类TypeElement进行区分,因为需要TypeElement获取类信息,进而根据类信息生成样板代码。

public class ViewBindingGroupedClasses {

    private HashMap<TypeElement, List<CodeBlock.Builder>> bindMaps = new HashMap<>();

    private ViewBindingClasses mViewBindingClasses;

    public ViewBindingGroupedClasses(){
        mViewBindingClasses = new ViewBindingClasses();
    }


    /**
     * ViewBind注解信息,根据TypeElement进行存储
     *
     * @param element
     */
    public void parseBindView(Element element) {
        ElementKind kind = element.getKind();
        if (kind == ElementKind.FIELD) {
            CodeBlock.Builder builder = mViewBindingClasses.parseBindView(element);
            if(builder!=null) {
                saveCodeBlockData(element, builder);
            }
        }
    }

    /**
     * onClick注解信息,根据TypeElement进行存储
     *
     * @param element
     */
    public void parseListenerView(Element element) {
        ElementKind kind = element.getKind();
        if (kind == ElementKind.METHOD) {
            ArrayList<CodeBlock.Builder> builder = mViewBindingClasses.parseListenerView(element);
            if(builder!=null) {
                saveCodeBlockData(element, builder);
            }
        }
    }


    /**
     * 判断Map信息,并根据TypeElement类型进行存储
     *
     * @param element
     */
    private void saveCodeBlockData(Element element, ArrayList<CodeBlock.Builder> codeBlockList) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        //判断是否存在TypeElement类型List数据
        List<CodeBlock.Builder> codeBlockLists = bindMaps.get(typeElement);
        if (codeBlockLists == null) {
            codeBlockLists = new ArrayList<>();
        }
        //保存CodeBlock数据
        if (codeBlockList != null) {
            codeBlockLists.addAll(codeBlockList);
        }

        bindMaps.put(typeElement, codeBlockLists);
    }

    /**
     * 判断Map信息,并根据TypeElement类型进行存储
     *
     * @param element
     */
    private void saveCodeBlockData(Element element, CodeBlock.Builder codeBlock) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        //判断是否存在TypeElement类型List数据
        List<CodeBlock.Builder> codeBlockLists = bindMaps.get(typeElement);
        if (codeBlockLists == null) {
            codeBlockLists = new ArrayList<>();
        }
        //保存CodeBlock数据
        if (codeBlock != null) {
            codeBlockLists.add(codeBlock);
        }

        bindMaps.put(typeElement, codeBlockLists);
    }

    /**
     * 根据TypeElement类型删除List数据
     *
     * @param element
     */
    public void deleteCodeBlockData(Element element) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        //判断是否存在TypeElement类型List数据
        List<CodeBlock.Builder> codeBlockLists = bindMaps.get(typeElement);
        if (codeBlockLists != null) {
            bindMaps.remove(typeElement);
        }
    }

    /**
     * 清楚所有数据
     */
    public void deleteAll(){
        bindMaps.clear();
    }


    /**
     * 生成模板代码注解
     *
     * @return
     */
    public HashMap<TypeElement, List<CodeBlock.Builder>> getBindMaps() {
        return bindMaps;
    }
}

3.3、生成样板代码

public class ViewBindingGenerator {

    /**
     *
     * @param typeElement
     * @param codeBlocks  配置信息
     * @param processorUtils
     * @param processingEnv
     */
    public void generator(TypeElement typeElement,
                          List<CodeBlock.Builder> codeBlocks,
                          ProcessorUtils processorUtils,
                          ProcessingEnvironment processingEnv) {
        //获取类名
        String className = typeElement.getSimpleName().toString();

        //根据TypeElement类型,获取类型名
        TypeName typeName = TypeName.get(typeElement.asType());
        if(typeName instanceof ParameterizedTypeName){
            typeName=((ParameterizedTypeName)typeName).rawType;
        }

        //配置构造函数,并设置参数
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(typeName,"target",Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"),
                        "source",Modifier.FINAL);

        //配置构造函数内容信息
        for(CodeBlock.Builder builder:codeBlocks){
            methodBuilder.addStatement(builder.build());
        }

        processorUtils.writeToFile( className+ Constant.VIEW_BINDING_SUFFIX,
                processorUtils.getPackageName(typeElement),
                methodBuilder.build(), processingEnv, null);



    }
}

四、功能点测试

4.1、测试代码

public class ThreeActivity extends AppCompatActivity {

    @BindView(R.id.mTvTitle)
    public TextView mTvTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_three);
        AnnotationUtils.bind(this);

    }

    @OnClick({R.id.mBtSubmit})
    public void onClick(View view){
        if(view.getId()==R.id.mBtSubmit){
            Toast.makeText(this,"点击了",Toast.LENGTH_SHORT).show();
        }
    }
}

4.2、样板代码

public final class ThreeActivity$ViewBinding {
  public ThreeActivity$ViewBinding(final ThreeActivity target, final View source) {
    target.mTvTitle=(android.widget.TextView)source.findViewById(2131165293);
    source.findViewById(2131165291).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onClick(v); }});
  }
}

About me