不诗意的女程序媛不是好厨师~
转载请注明出处,From李诗雨---https://blog.csdn.net/cjm2484836553/article/details/104581855
源代码下载地址:https://github.com/junmei520/iocStudy
在上一篇 的文章 《撸代码 学习 IOC注入技术1 》—— 布局注入 与 控件注入中,我们已经自己通过敲代码,一步一步实现了,运行时注入的---布局注入和控件注入。那么今天,我将来继续敲代码,来一步一步实现,事件的注入。
先来看一下我要达到的效果:
即:我想通过这两句代码,就实现点击事件。
根据上一篇中我们讲的 布局注入 和 控件注入 的经验,大家对于实现我们今天的 事件注入 有没有什么想法和思路呢?
对!我们还是要自己造个女朋友InjectUtils,然后在BaseActivity中就进行注入。
那接下来呢?接下来继续要怎么做?你会不会是这样想的:
你是不是想:"那还不简单吗?和之前的类似啊,先自定义一个注解OnClick,然后具体实现InjetUtils中的injectEvent()方法呀!"
那我有要问:"那你打算具体怎么实现injectEvent()呢?"
你是不是还会像这样回答:“当然主要还是通过反射啦,①先获取activity的所有方法;②再获取方法上的 OnClick 注解,进而得到注解后面的id ,然后得到button;③最后反射执行 btn1.setOnClickListener(new View.OnClickListener() {...}巴拉巴拉巴拉...”
emmm... 我想说,你这么想其实也没有什么大问题,就是有点,emmm...,有点太low啦。因为如果你这样做的话,那就是把代码写死了呀~~~
比如说,如果我还想加个长按事件呢,像这样:
你可能会说,那我就改injectEvent()代码呀!
emmm...我忍!那如果我再继续增加几个事件呢?你还打算继续改injectEvent()的内部代码吗?还打算增加许多的if/else或很多的谜之缩进吗???你自己体会一下~~~
显然这样把代码写死是不可行的,那我们怎么样才能使得我们自己的代码变得灵活呢?
那我们就必须寻找不同事件的共同点了,然后把相同点抽取出来。
让我们再用新的眼光来审视一下短按和长按事件:
我们可以总结出,所有的事件都具有三要素:
- 1.事件源
- 2.事件
- 3.事件的处理
- 最后还要进行订阅(订阅关系)
既然知道了这一点,那我们再自定义注解OnClick的时候就可以把这三要素也加进去了。
下面我们就来正式的讲讲正确的思路了:
首先我们先自定义一个注解BaseEvent,它可以用来接受事件的三要素信息,并且将来会把它用在OnClick注解的身上:
@Target(ElementType.ANNOTATION_TYPE) //该注解是用在自定义注解上的
@Retention(RetentionPolicy.RUNTIME) //可以保留到程序运行时
public @interface BaseEvent {
Class<?> enventType(); //事件 ---> 即相当于 new View.OnClickListener()
String setterMethod(); //订阅关系 ---> 即相当于 setOnClickListener()
String callbackMethod(); // 事件回调方法 ---> 即相当于 onClick()
}
然后我们再来定义OnClick注解,它的头上使用了BaseEvent注解,并传入三要素。
@Target(ElementType.METHOD) //该注解是用在方法上的
@Retention(RetentionPolicy.RUNTIME) //该注解可以保持到程序运行时
@BaseEvent(enventType = View.OnClickListener.class,
setterMethod = "setOnClickListener",
callbackMethod = "onClick")
public @interface OnClick {
int[] value() default -1; //由于可能是多个id,所以此处要用数组来接收
}
使用的时候就这样:
@OnClick({R.id.button1, R.id.button2})
public void click(View view) {
//...具体操作...
}
如果要再加入其他的事件,也很好办,比如我要再加一个长按事件,那我就只要多增加一个OnLongClick的注解就可以了,它的地方都不用做任何的修改。其实,这就是我们所说的 注解的多态。
//增加一个长按事件
@OnLongClick({R.id.button1, R.id.button2})
public boolean longClick(View view) {
//...具体操作...
return false;
}
//只要多增加一个自定义的注解就可以了,传入具体的事件三要素。
@Target(ElementType.METHOD) //该注解是用在方法上的
@Retention(RetentionPolicy.RUNTIME) //该注解可以保持到程序运行时
@BaseEvent(enventType = View.OnLongClickListener.class,
setterMethod = "setOnLongClickListener",
callbackMethod = "onLongClick")
public @interface OnLongClick {
int[] value() default -1; //由于可能是多个id,所以此处要用数组来接收
}
一些小说明:
1.由于我们在使用OnClick注解时传入了控件的id, 所以在自定义BaseEvent注解时,事件源就没有必要再传进去了。
2.由于在使用OnClick注解时,可能传入的是多个控件的id, 所以自定义OnClick注解时,要用int[]数组来接收。
好了,完成了这些铺垫工作,下面就让我们来集中精力实现InJectUtils中的injectEvent()方法吧~
首先,我们来思考一下,我们需要做哪些事情呢?
我们来分析一下,首先由于我们的OnClick注解是用在方法上的,所以
第一步,就是要获取activity上的所有方法。(目的是为了可以找到使用了OnClick注解的click()方法)
第二步,我们要对所有的注解进行遍历,获取每一个方法上的所有注解。(通过这一步我们可以获取click()上的OnClick注解)
第三步,我们要拿到注解类型对应的Class,通过Class去找,看看有没有BaseEvent,如果有则说明这个方法就是事件方法。(通过这一步我们可以得到OnClick对应注解类型的Class,从而进一步找到BaseEvent,并且可以确定click()就是事件方法)
-
第四步,从BaseEvent中拿到事件的三要素。
(①通过enventType()-->得到事件:即相当于 new View.OnClickListener();
②通过setterMethod()-->得到订阅关系:即相当于 setOnClickListener();
③通过callbackMethod()-->得到事件回调方法:即相当于 onClick())
-
第五步,接下来就是反射执行
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { } });
了。
我们来看一下代码实现:
private static void injectEvent(Object context) {
Class<?> clazz = context.getClass();
//1.获取该activity上的所有方法
Method[] methods = clazz.getDeclaredMethods();
//2.循环遍历方法,拿到每一个方法上的所有注解
for (Method method : methods) {
Annotation[] annotations = method.getAnnotations();
//3.循环遍历注解,拿到注解类型对应的Class,通过class去找,看看有没有BaseEvent
for (Annotation annotation : annotations) {
//拿到注解类型对应的Class
Class<?> annotationClass = annotation.annotationType();
//通过Class去找,看看有没有BaseEvent
BaseEvent baseEvent = annotationClass.getAnnotation(BaseEvent.class);
//如果没有BaseEvent,则表示当前方法不是一个事件处理的方法
if (baseEvent == null) {
continue;
}
//4.如果有BaseEvent,则表示是事件处理的方法,那我们就去拿事件的三要素
//拿到三要素
Class<?> eventType = baseEvent.enventType();
String setterMethodStr = baseEvent.setterMethod();
String callbackMethod = baseEvent.callbackMethod();
//5.接下来我们要反射执行
// btn1.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
//
// }
// });
}
}
}
关于第五步,反射执行btn.setOnClickListener(new View.OnClickListener(){ public void onClick()})
,我们要单独提出来分析一下。
-
1.首先我们需要拿到事件源,(即对应的view控件,此处即指btn按钮)。
那我们要怎么做呢?对,首先我们要拿到控件id.
✪那控件id要怎么样才能拿到呢?我们是不是在上面已经拿到了注解类型对应的Class,那我们就可以根据方法名,通过反射拿到value()对应的method;然后我们再反射执行valueMethod,就可以拿到id了。
✪有了id就好办了,再反射执行findViewById,就可以拿到对应的view控件了。
2.要拿到事件(即此处的[new View.OnClickListener()]),这个我们通过上面的BaseEvent已经得到了,即eventTpye。
3.我们还要拿到订阅关系(即setOnClickListener()),这个也好办,通过上面的BaseEvent我们不是已经拿到了方法名的字符串了吗,那再通过反射拿到对应的setterMethod就可以啦。
4.我们,是不是还差一个事件的处理(onClick())。但是,我们写的这个框架,并不知道将来按钮要具体执行哪些操作呀?对于未知的东西我们该怎么处理呢?对啦!用动态代理。
那下面我们就来具体看看这个动态代理该怎么写吧~
首先我们要自定义一个MyInvocationHandler类,因为要代理的真实对象是activity中的click()方法,所以,我们需要两个属性,并在构造函数中直接传入,我们还知道最终会调用这个类里的invoke()方法,而这里要执行的应该是真实对象要执行的操作,所以此处直接调用activityMethod.invoke(activity,objects);
//代理的是 new View.OnClickLisener()对象
//并且最终执行的是activity的click()方法
public class MyInvocationHandler implements InvocationHandler {
private Object activity;
private Method activityMethod;
public MyInvocationHandler(Object activity, Method activityMethod) {
this.activity = activity;
this.activityMethod = activityMethod;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return activityMethod.invoke(activity, objects);
}
}
好了,那我们就把第五步的步骤也补充上去吧:
private static void injectEvent(Object context) {
Class<?> clazz = context.getClass();
//1.获取该activity上的所有方法
Method[] methods = clazz.getDeclaredMethods();
//2.循环遍历方法,拿到每一个方法上的所有注解
for (Method method : methods) {
Annotation[] annotations = method.getAnnotations();
//3.循环遍历注解,拿到注解类型对应的Class,通过class去找,看看有没有BaseEvent
for (Annotation annotation : annotations) {
//拿到注解类型对应的Class
Class<?> annotationClass = annotation.annotationType();
//通过Class去找,看看有没有BaseEvent
BaseEvent baseEvent = annotationClass.getAnnotation(BaseEvent.class);
//如果没有BaseEvent,则表示当前方法不是一个事件处理的方法
if (baseEvent == null) {
continue;
}
//4.如果有BaseEvent,则表示是事件处理的方法,那我们就去拿事件的三要素
//拿到三要素
Class<?> eventType = baseEvent.enventType();
String setterMethodStr = baseEvent.setterMethod();
String callbackMethod = baseEvent.callbackMethod();
//5.接下来我们要反射执行
// btn1.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
//
// }
// });
//5.1首先我们需要拿到事件源,(即对应的view控件,此处即指btn按钮)
try {
//先获取注解中的value方法,即 OnClick中的value()
Method valueMethod = annotationClass.getDeclaredMethod("value");
//再反射执行 OnClick注解的 value()方法,得到id
int[] ids = (int[]) valueMethod.invoke(annotation);
for (int id : ids) {
//反射执行context.findViewById(id)得到对应的view
Method findViewByIdMethod = clazz.getMethod("findViewById", int.class);
View view = (View) findViewByIdMethod.invoke(context, id);
if (view == null) {
continue;
}
//5.2要拿到事件(即[new View.OnClickListener()]),这个我们通过上面的BaseEvent已经得到了,即eventTpye。
//5.3拿到订阅关系(即setOnClickListener()),即根据setterMethodStr得到setterMethod
//5.4动态代理了 //activity==context click===method
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(context, method);
Object proxy = Proxy.newProxyInstance(eventType.getClassLoader(),
new Class[]{eventType}, myInvocationHandler);
// 让proxy执行的click()
//参数1 setOnClickListener()的名称
//参数2 new View.OnClickListener()对象
Method setterMethod = view.getClass().getMethod(setterMethodStr, eventType);
// 反射执行 view.setOnClickListener(new View.OnClickListener())
setterMethod.invoke(view, proxy);
//这时候,点击按钮时就会去执行代理类中的invoke方法()了
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
好了,到这里我们所有的事件注入工作就都完成了,赶快在测试一下吧:
//在MainActivity中进行使用测试
@OnClick({R.id.button1, R.id.button2})
public void click(View view) {
switch (view.getId()) {
case R.id.button1:
Toast.makeText(this, "短按下了", Toast.LENGTH_SHORT).show();
break;
case R.id.button2:
Toast.makeText(this, "短按下了222", Toast.LENGTH_SHORT).show();
break;
}
}
//增加一个长按事件,注意这里的方法返回类型要和系统中的保持一致
@OnLongClick({R.id.button1, R.id.button2})
public boolean longClick(View view) {
switch (view.getId()) {
case R.id.button1:
Toast.makeText(this, "好好学习", Toast.LENGTH_SHORT).show();
break;
case R.id.button2:
Toast.makeText(this, "天天向上", Toast.LENGTH_SHORT).show();
break;
}
return false;
}
运行结果:
源代码下载地址:https://github.com/junmei520/iocStudy
积累点滴,做好自己~