项目需求:
在Android开发中,我们在初始化控件、事件时常常被繁琐的代码困扰,比如这样:
Button button;
TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.btn);
textView = findViewById(R.id.tv);
}
又或者这样:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
button.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return false;
}
});
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
textView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return false;
}
});
这代码看着就让人头疼,当然可以通过实现接口然后在在实现接口方法中使用switch关键字判断,但是依旧很麻烦,因为所有的使用监听器的方法都得使用setOnxxxListener(this)类似的方法。注入插件的出现就是为了简化这些问题,使得代码变得简洁,网络上现在有许多常见的开源框架,如:ButterKnife,xUtils3等,其实他们的实现并没有那么神秘。
注入框架实现
一、控件注入
1、添加注释
使用过开源项目的人都知道简化代码的手段是通过添加一个注释,便能对控件或者事件(监听器)完成设置,如:
@InjectView(R.id.btn)
private Button button;
@InjectView(R.id.tv)
private TextView textView;
于是,我自定义了1个注释:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
int value();
}
@interface表示定义一个注释,@Target表示这个注释作用于类(TYPE),方法(METHOD),或是参数(FIELD)等等。@Retention表示表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)。设置为RUNTIME表示在运行时保留。注解中的value()表示注解的参数值,如:@InjectView(R,id.tv)中的R.id.tv就是value的值
2、对注释的参数进行赋值
没错,最终还是要调用findViewById方法,只是框架把这些都帮开发者做好了,而且通过for循环做好了。
新建一个InjectManager类,然后通过一个方法传入Activity对象,并分别完成对控件和事件的注入
public static void injectInto(Activity activity){
injectLayout(activity);
injectViews(activity);
injectEvents(activity);
}
这里的injectLayout可以无视,这是对布局的注入,即省略了原本的onCreate()方法中的setContentView(),在Activity类完成类注释,比较简单。下面是注入控件的实现:
private static void injectViews(Activity activity) {
//获取Activity类
Class<? extends Activity> clazz = activity.getClass();
//获取Activity类中所有的参数
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
InjectView annotation = field.getAnnotation(InjectView.class);
//得到有InjectView注解的参数
if(annotation != null){
int viewId = annotation.value();//获取属性值,控件ID
try {
Method method = clazz.getMethod("findViewById", int.class); //获取父类的findViewById方法
Object view = method.invoke(activity, viewId); // 调用并返回findViewById方法返回值
field.setAccessible(true); //设置可访问私有属性
field.set(activity,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
首先获取activity的对象clazz,然后获取它本类的所有方法(这里需要注意的是,getDeclaredFields和getFields的区别是:前者是获取当前类的所有变量,后者是获取本类,包括父类,父类的父类…一直到祖宗类的所有变量,这里只需要本类的参数即可),循环遍历所有变量,判断获取InjectView注解是否存在,存在的话获取注解的参数,即为我们自己设置的控件id值。
接下来获取findViewById方法。想想我们在activity中没有定义findViewById(…)方法,却使用可以直接使用它,因为它存在于父类AppCompatActivity中,所以可以直接调用(java基本常识).因此通过clazz.getMethod获取到父类的findViewById方法(前面已说明),第二个参数是获取方法的参数类型,findViewById()的参数是int类型,因此这里就是int.class得到方法method后,使用invoke方法进行回调。回调会得到返回参数,findViewById()的返回值是一个T类型,这里直接通过field.setAccessible(true)设置属性可以被访问,然后通过set方法完成对控件的设置,等同于view = findViewById(…),这样就完成了对控件的注册
注:
invoke方法有2个参数,简单来说,第一个参数表示该方法被调用的对象,第二个参数就是方法的参数,举例来说:
对于activity.findViewById(R.id.xx),findViewById是activity的方法(method对象是findViewById),method.invoke()的第一个参数就是activity,第二个参数就是R.id.xx
对于view.setOnClickListener(new OnClickListenr(…)),setOnClickListener是view的方法(method对象是setOnClickListener),method.invoke()的第一个参数就是view,第二个参数就是new OnClickListenr(…).
一、事件注入
1、添加事件总注解
一个事件,通常可以分解通常可以分解为3个部分:1、监听的方法名,2、监听的对象,3、监听的回调方法。
因此,定义一个注解对这3个部分进行定义:
@Target(ElementType.ANNOTATION_TYPE) //这是一个注解的注解
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
String listenerSetter(); //对应于setOn...Listener
Class<?> listenerType(); //对应于new On...Listener
String callBackListener(); //对应于接口实现方法,如:onClick
}
这个注解是一个注解的注解,顾名思义,就是对注解进行注解,它可以定义一个注解是什么类型的注解,如:
定义一个点击注解
@EventBase(listenerSetter = "setOnClickListener",listenerType = View.OnClickListener.class,callBackListener = "onClick")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface onClick {
int[] value();
}
这个注解有个注解是EventBase,它的3个参数决定了这个注解是个onClick注解,下面是对如何进行注解的实现:
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
//获取每个方法的注解
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
//获取每个方法的注解的注解类型
Class<? extends Annotation> annotationType = annotation.annotationType();
if(annotationType != null){
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
//判断是否是有EventBase注解的注解
if(eventBase != null){
//获取EventBase的三个参数
String listenerSetter = eventBase.listenerSetter();
Class<?> listenerType = eventBase.listenerType();
String callBackListener = eventBase.callBackListener();
//使用代理完成事件的拦截、修改
MyInvocationHandler handler = new MyInvocationHandler(activity);
handler.addMethod(callBackListener,method);
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),new Class[]{listenerType},handler);
try {
//通过注解类型,获取onClick注解的value值
Method valueMethod = annotationType.getDeclaredMethod("value");
int[] viewIds = (int[]) valueMethod.invoke(annotation);
if(viewIds != null) {
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
//获取制定方法,例子:view如果是Button,获取它的setOnClickListener方法
Method setter = view.getClass().getMethod(listenerSetter, listenerType);
setter.invoke(view,listener);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
一样的,先遍历获取activity类中所有的方法,然后继续遍历获取所有方法的所有注解,annotation.annotationType()表示注解类型,通过这个注解类型获得注解的注解,就是自定义的EventBase,如何不为空,才是符合的方法,接着通过自定义注解的对象获得它的三个参数:监听的方法名,监听的对象,监听的回调方法。接着使用Proxy完成对事件的实现。这里详细来说:Proxy.newProxyInstance()方法,3个参数,第一个:ClassLoader,没什么好说的,直接通过监听的对象获得ClassLoader,第二个参数是传入一个接口名数组,这里是callBackListener,,最后是一个InvocationHandler对象,它是一个接口,需要实现invoke方法,具体如下:
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
String name = method.getName(); //这里的method是onClick
method = map.get(name); //当onClick方法被调用时,将method替换成自定义方法
//此时的method已经是自定义方法了
if(method != null){
if(method.getGenericParameterTypes().length == 0) {
return method.invoke(target);
}
return method.invoke(target,objects);
}
return null;
}
刚才第二个参数设置的接口,它需要实现的所有方法,都会回调这个上面的invoke方法,同样地在这里进行进行拦截事件,替换事件。下面先添加拦截方法,
//添加拦截的方法
public void addMethod(String methodName,Method method){
map.put(methodName,method);
}
handler.addMethod(callBackListener,method);
要拦截的方法通过HashMap保存,key值为原本的方法名,value值为要替换的方法,在invoke方法中,当接口的onClick方法被回调时,将它替换成我们自己的方法并返回。
回到injectEvents方法中,通过Proxy.newProxyInstance()方法获得监听器对象,然后获取所有注册此事件的控件,得到他们的父类的setOnClickListener()方法, 通过回调的方法:setter.invoke(view,listener),设置好参数,最后在activity完成注册就完成了,如下:
@onClick({R.id.btn,R.id.tv})
public void show(){
Toast.makeText(this, "AirPods", Toast.LENGTH_SHORT).show();
}
以上是以setOnClickListener方法为举例,其他的方法也一样,但是!值得一提的是,注入的方法返回类型必须与接口中的方法返回类型一致,否则会报错,如:
xxx.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
@onClick(R.id.xxx)
public void show(){
Toast.makeText(this, "AirPods", Toast.LENGTH_SHORT).show();
}
无返回类型,因此用void,又如:
button.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return false;
}
});
@onLongClick(R.id.tv)
public boolean show(View view){
Toast.makeText(this, "AirPods2", Toast.LENGTH_SHORT).show();
return false;
}
返回类型是boolean,因此注入的方法也必须是boolean类型,不能是:
@onLongClick(R.id.tv)
public void show(View view){
Toast.makeText(this, "AirPods2", Toast.LENGTH_SHORT).show();
}