《撸代码学习 IOC注入技术2》—— 事件注入

380 阅读9分钟

不诗意的女程序媛不是好厨师~ 转载请注明出处,From李诗雨---blog.csdn.net/cjm24848365…

源代码下载地址:github.com/junmei520/i…

在这里插入图片描述

在上一篇 的文章 《撸代码 学习 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;
}

运行结果:

在这里插入图片描述

在这里插入图片描述

源代码下载地址:github.com/junmei520/i…

积累点滴,做好自己~