Android IOC注解—事件三要素,打造通用事件注解工具类

image

好久没有写东西了一直在忙公司项目,最近在学习Android的面向切面和APT,学到了很多有意思的东西。ICO注入事件---打造兼容性事件监听的工具类,也是自己这两天学到的一种事件监听方式。(也就是类似Butterknife中@OnClick)做个笔记帮助自己总结一下学到的技能,也方便自己以后查阅。
Android事件、不管是点击事件OnClickListener、长按事件OnLongClickListener、按键OnKeyListener等事件、都有一些同性;所有的事件都是通过view.setOnxxxListener()进行设置,然后传入OnxxxListener事件接口,最后由系统调用接口中的onXX()方法接口触发该监听事件,如下
普通事件设置

Button btn_hw;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    btn_hw = findViewById(R.id.btn_hw);
    btn_hw.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {}
    });
}

根据上面的分析,我们可以将事件划分三个重要点,也就是事件三要素:

  1. 事件调用的方法名xxx.setOnClickListener
  2. 事件的接口:View.OnClickListener
  3. 事件调用执行的方法:onClick (这个可能用不到)

通过上面的划分后,我们可以定义一个公共的事件注解类来存放这三个要素。这个公共事件注解并不是给调用类中的方法使用,它是专门给注解使用的因此我们需要将ElementType定义为注解类型@Target(ElementType.ANNOTATION_TYPE)。里面定义三个值:view设置事件的方法名、事件的接口Class对象,系统调用执行的事件回调方法。
公共事件注解

@Target(ElementType.ANNOTATION_TYPE)//在注解上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface OnBaseListener {
    //事件三要素---方法名 setOnClickListener
    String setListener();
    //事件三要素---事件接口 OnClickListener
    Class setListenerClass();
    //事件三要素---事件执行的方法 onClick
    String setCallbackMethod();
}

定义完公共事件数组@OnBaseListener后,就可以在具体的事件监听注解中使用该注解了。文中只定义了两个事件,其他事件注解的定义类似**
点击事件

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnBaseListener(setListener = "setOnClickListener",
        setListenerClass = View.OnClickListener.class,
        setCallbackMethod = "onClick"
)
public @interface OnClick {
    int value();//组件id
}

长按事件

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@OnBaseListener(setListener = "setOnLongClickListener",
        setListenerClass = View.OnLongClickListener.class,
        setCallbackMethod = "onLongClick")
public @interface OnLongClick {
    int value();//组件id
}

事件定义完后,下面我们就要定义注解的解析工具类。该工具类主要通过反射来获取注解信息、组件id、view,设置事件监听,如下面定义的 InjectListener

public class InjectListener { 
    ...省略无关代码
    public void init(Object activityObj) {
            Class<?> activityClass = activityObj.getClass();
            for (Method declaredMethod : activityClass.getDeclaredMethods()) {
                declaredMethod.setAccessible(true);
                findOnListener(activityObj, activityClass, declaredMethod);
            }
        }
}

我们先定义init(Object activityObj) 将Activity具体需要注解的类实例传过来,然后反射拿到该类中的所有方法。这里需要注意一点是 我们使用的是getDeclaredMethods() 方法,该方法返回的是activityObj对象中自己定义的方法,并不会返回父类中定义的方法。当然如果你使用 getMethod() 方法也是可以的。不过没有那个必要因为我的事件注解都是在xxxActivity中自定义的方法上使用的,所以只要获取本来方法就好不需要浪费时间遍历父类中的方法。
拿到遍历的方法后将遍历得到的Method传到自己写的findOnListener()方法中进行解析置事件监听。该方法大致流程是:

  1. 拿到activityObj对象中自己定义方法上的注解
  2. 解析每个注解、判断是否包含公共事件注解@OnBaseListener,有则取出该直接中的OnBaseListener注解
  3. 解析OnBaseListener注解中的值
  4. 获取自定义方法上Onxx注解中的id
  5. 反射执行activityObj的findViewById()方法获取到view控件
  6. 动态代理创建事件监听接口的实例
  7. 在代理类中的代理方法中反射执行**

事件注解处理工具类

public class InjectListener {
    private static String TAG = InjectListener.class.getSimpleName();
    private static InjectListener mInstance;
    public static InjectListener getInstance() {
        if (mInstance == null) {
            synchronized (InjectListener.class) {
                if (mInstance == null) {
                    mInstance = new InjectListener();
                }
            }
        }
        return mInstance;
    }
    public void init(Object activityObj) {
        Class<?> activityClass = activityObj.getClass();
        for (Method declaredMethod : activityClass.getDeclaredMethods()) {
            declaredMethod.setAccessible(true);
            findOnListener(activityObj, activityClass, declaredMethod);
        }
    }
    private void findOnListener(final Object activityObj, Class<?> activityClass, final Method listenerMethod) {
        //因为我们要拿的具体事件注解中的公共事件注解@OnBaseListener,一个类中可能有多个事件,事件有可能有不相同
        //因此我们需要通过getAnnotations()获取方法上的所有注解,然后通过annotationTyp来判断是否为@OnBaseListener注解过的
        //注解,如果是则取出其中的设置的值
        for (Annotation annotation : listenerMethod.getAnnotations()) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            OnBaseListener onBaseListener = annotationType.getAnnotation(OnBaseListener.class);
            if (onBaseListener == null) {
                continue;
            }
            //拿到事件三元素注解元素中的值
            String setListener = onBaseListener.setListener();
            Class setListenerClass = onBaseListener.setListenerClass();
            String setCallbackMethod = onBaseListener.setCallbackMethod();
            try {
                //因为要兼容其他事件方法,因此要通过反射动态获取事件监听注解中的方法
                Method valueMthod = annotationType.getDeclaredMethod("value");
                //获取view中的id
                int value = (int) valueMthod.invoke(annotation);
                //根据id获取到view getMethod()不仅可以获取自己的方法,还能获取到父类中的方法
                Method findViewById = activityClass.getMethod("findViewById", int.class);
                Object view = findViewById.invoke(activityObj, value);
                Log.d(TAG, ">>>>>>>>>>>" + value);
                //设置view的事件监听 必须要用于getMethod()来获取view设置事件监听的方法,因为我们不知道当前控件的事件是不是
                //它自己定义的还是父类View以及View的事件,getMethod()不仅会把自己类中的返回返回,还会将父类中以及父类中父类
                // 的方法返回
                Method setListenerMethod = view.getClass().getMethod(setListener, setListenerClass);
                //使用动态代理技术,自动生成事件监听接口的实例
                Object proxy = Proxy.newProxyInstance(setListenerClass.getClassLoader(), new Class[]{setListenerClass},
                        new InvocationHandler() {
                            /**
                             *  在执行接口中的方法的时候,会回调这个invoke方法,内执行接口中的一个方法,就会回调一次
                             * @param o 很少用
                             * @param method 要执行接口中的方法,如果接口中有多个方法,那么此次调用invoke(),method
                             *               指向的方法就不同
                             * @param objects 执行方法的参数
                             * @return 返回执行方法的结果
                             * @throws Throwable 异常
                             */
                            @Override
                            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                                //因为 监听函数只有一个方法,因此代理的invoke之会执行一次,我们不需要执行method方法
                                //因为我们要执行自己自定义的方法,下面的返回结果也就是我们自定义的返回结果
                                return listenerMethod.invoke(activityObj, null);
                            }
                        });
                //设置监听
                setListenerMethod.invoke(view, proxy);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

在Activity中使用

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    InjectListener.getInstance().init(this);
}
//点击事件
@OnClick(R.id.btn_hw)
private void btnHW() {
    Toast.makeText(getBaseContext(), "我是OnClick注解绑定的事件哦!",
            Toast.LENGTH_SHORT).show();
}
//长按事件
@OnLongClick(R.id.btn_long)
private boolean btnLong() {
    Toast.makeText(getBaseContext(), "我是OnLongClick注解绑定的事件哦!",
            Toast.LENGTH_SHORT).show();
    return true;
}

** 项目地址:Anno **

总结

上面的实现IOC注解绑定View的各种事件方式,只要是通过注解叫反射的方式实现。最后的动态代理最最重要的一点是用来创建事件接口的具体实现类,代理invoke()方法的返回函数就是Activity自动的事件监听函数的返回值,如果自定义的函数没有返回值那么就是null。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350