如何成为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、注解信息校验
- 校验变量访问修饰符,如果变量修饰符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;
}
- 校验变量访问修饰符,如果方法修饰符,方法返回类型,方法形参个数。
/**
* 校验方法,格式: 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、解析注解信息
- @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;
}
- **@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
-
Email:linwei9605@gmail.com
-
Blog: offer.github.io/
-
Github: github.com/WeiShuaiDev