在Android开发中,在初始化的时候需要查找控件、为控件设置监听,因此我们经常会写如下的代码。
tvInfo = findViewById(R.id.tvInfo);
findViewById(R.id.btnChange).setOnClickListener(v->{
});
这种代码初看非常合理,这不是Android开发的固有套路么。但设想一下,要是页面的控件有几十个,需要设置的监听有几十个。嗯…是不是就感觉没那么科学了呢?随着技术的发展,Butterknife横空出世,解决了需要写一堆类似的代码的问题。相信做了一定时间的大家都用过这个库吧,非常的好用。今天我们先来用注解+反射的方式实现一个我们自己的Butterknife。声明:该文章仅是为了体现注解的妙用和架构的思维,这种性能是极低的。由于需要用到注解、反射,因此对这两部分的知识比较陌生的,可以先补习下基础知识。注解 、反射
先确定我们想要实现的效果。对成员变量控件,我们可以通过一个注解自动注入控件。对控件的事件监听,我们可以通过对方法的注解自动绑定事件。效果如下:
@BindView(R.id.tvInfo)
TextView tvInfo;
@BindView(R.id.btnChange)
Button btnChange;
当注解后,我们就可以直接调用tvInfo.setText("我是改变后的数据");这样的代码,而不用在写findViewById了。
@OnClick(R.id.btnChange)
public void onClick(View view){
if(view.getId() == R.id.btnChange){
tvInfo.setText("我是改变后的数据");
}
}
对应事件监听,我们期望的是对方法注解后就自动进行事件绑定,不用在自己去写setOnClickListener了。
效果我们已经确定了,那该怎么实现呢?由于注解是被动使用的,永远不会主动调用。那可以想像一下,我们在onCreate中调用注入的方法,由该方法发起后续的注入能力,如:InjectUtil.inject(this);这样就可以注入了。先把我们的注解类写出来先。
/**
* 绑定控件
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
这个注解是用于控件的注入的,使用在成员变量上。注解控件的方法如下,整体比较简单,就是反射findViewById这个方法实现变量赋值。
/**
* 绑定View
* @param context
*/
private static void injectView(Object context){
Class<?> clazz = context.getClass();
//知识点getFields() 获取某个类的所有的公共的字段,包括父类中的字段
//getDeclaredFields() 获取某个类的所有声明的字段,包括public、private、proteced,但不包括父类的申明字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
BindView annotation = field.getAnnotation(BindView.class);
if(annotation != null){
try {
int id = annotation.value();
Method findViewById = clazz.getMethod("findViewById",int.class);
Object view = findViewById.invoke(context,id);
if(view != null){
field.setAccessible(true);
field.set(context,view);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
经过这个方法的注入,就实现了控件的自动注入赋值了,不需要我们在手动去写findViewById进行控件初始化了。
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseEvent {
//事件方法
String listenerSetter();
//接口的父类
String listenerClass();
//接口类
String listenerInteface();
}
/**
* 单击事件
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BaseEvent(listenerSetter = "setOnClickListener",
listenerClass = "android.view.View",
listenerInteface = "android.view.View.OnClickListener")
public @interface OnClick {
int[] value() default -1;
}
/**
* 长按事件
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@BaseEvent(listenerSetter = "setOnLongClickListener",
listenerClass = "android.view.View",
listenerInteface = "android.view.View.OnLongClickListener")
public @interface OnLongClick {
int[] value() default -1;
}
这里做了单击、长按事假,因为这种事件有很多,这里抽象了个基础注解类,可理解为注解的多态,用来标记事件的几个要素。
findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
listenerSetter对应setOnClickListener
listenerClass 这个是为了拿接口类的父类,对应android.view.View
listenerInteface 接口类,比如OnClickListener这个接口
到这里可能比较绕的是为什么要有这3个值的存在,最主要是后面两个。其实想一下我们设置监听的方式,View.setOnClickListener(View.OnClickListener)参数是一个接口,我们在反射setOnClickListener这个方法的时候要指定参数类,对应listenerInteface,但这个类是属于View类里面的,因此我们要先找到View这个类,具体可以看下注入的代码。
/**
* 注入Click
* @param context
*/
private static void injectClick(Object context){
Class<?> clazz = context.getClass();
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
Class<? extends Annotation> aClass = annotation.annotationType();
BaseEvent baseEvent = aClass.getAnnotation(BaseEvent.class);
//如果不存在BaseEvent的注解,那么说明不是需要处理的Click事件
if(baseEvent == null){
continue;
}
String listenerSetter = baseEvent.listenerSetter();
String listenerClass = baseEvent.listenerClass();
String listenerInteface = baseEvent.listenerInteface();
try {
Method value = aClass.getDeclaredMethod("value");
int[] ids = (int[])value.invoke(annotation);
for (int id : ids) {
//先根据id找到对应的控件
Method findViewById = clazz.getMethod("findViewById",int.class);
Object view = findViewById.invoke(context,id);
if(view == null){
continue;
}
ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context,method);
//先找View类
Class<?> viewClass = Class.forName(listenerClass);
//根据View类找内部的类
Class<?>[] declaredClasses = viewClass.getDeclaredClasses();
for (Class<?> declaredClass : declaredClasses) {
//匹配对应的接口类
if(declaredClass.getCanonicalName().equals(listenerInteface)){
Object proxy = Proxy.newProxyInstance(declaredClass.getClassLoader(), new Class[]{declaredClass}, listenerInvocationHandler);
Method click = view.getClass().getMethod(listenerSetter,declaredClass);
click.invoke(view,proxy);
break;
}
}
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
上面这段代码整体还是很好理解的,只是多出来了一个知识点:动态代理。对这个知识点不清晰的可以进去补习下。这样我们就将事件通过一个注解就注入进去了,不用在去写setOnClickListener这样的代码了。最后我们使用的方式就很简单了。
public class MainActivity extends AppCompatActivity{
@BindView(R.id.tvInfo)
TextView tvInfo;
@BindView(R.id.btnChange)
Button btnChange;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
InjectUtil.inject(this);
// tvInfo = findViewById(R.id.tvInfo);
// findViewById(R.id.btnChange).setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
//
// }
// });
// btnChange.setOnClickListener(view -> {
// tvInfo.setText("我是改变后的数据");
// });
}
@OnClick(R.id.btnChange)
public void onClick(View view){
if(view.getId() == R.id.btnChange){
tvInfo.setText("我是改变后的数据");
}
}
@OnLongClick(R.id.btnChange)
public boolean onLongClick(View view){
if(view.getId() == R.id.btnChange){
tvInfo.setText("我是长按后改变后的数据");
}
return true;
}
}
本篇只实现了控件的注入、简单事件的注入,有兴趣的可以在此基础上扩展。但目的我们是为了学习这种设计理念,并且掌握注解、反射这样的知识技巧,真实的开发中是不会使用这样损耗性能的方式的哈。