制作自己的 @OnClick、@OnLongClick(告别 setOnClickListener,使用注解、反射和动态代理)

news2024/11/28 8:49:17

前言

前面我们说过 ButterKnife 这个库,这个库实现不仅实现了 View 的绑定,而且还提供了大量的注解如 @BindView@OnClick@OnLongClick 等来简化开发过程中事件绑定。而这些功能的实现是通过 APT 也就是注解处理器,在编译期间生成 Java 代码来实现的。
关于这一部分,大家请看前面的这篇文章,这篇文章将 ButterKnife 讲解的非常好,可以跟着一步一步实现一个自己的简易版本:
制作自己的 ButterKnife(使用 AutoService 和 APT 注解处理器在编译期生成 Java 代码)

不过这一次,我们不再使用 APT,而是利用注解和反射的巧妙结合,在运行时进行事件和 View 的绑定,让我们告别 setOnClickListener 这类的方法 。这里我们拿 @OnClick@OnLongClick 这两个注解为例,自己实现这两个注解的功能。

不过想要实现自己的绑定注解还是有一些前置条件的,需要学习注解,反射和动态代理这三个方面的知识,此处推荐下面的三篇文章,建议看完,并对刚说的三个知识点有一定认识,再来看本文。

  • 由浅入深 带你了解 JAVA 注解
  • Java Reflection 反射使用 完全指南
  • 代理模式与静态代理、动态代理的实现(Proxy.newProxyInstance、InvocationHandler)

这里插播一下我的公众号,也是最近开始弄,希望多多关注,以后我会发更多的高质量内容:
微信公众号二维码

确定目标

首先我们确定要实现的是两个注解的功能:@OnClick@OnLongClick。在没有这两个注解时,我们做事件绑定的代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //为 id 为 button_1 的 View 设置 点击事件
        findViewById(R.id.button_1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击", Toast.LENGTH_SHORT).show();
            }
        });
        //为 id 为 button_2 的 View 设置 长按事件
        findViewById(R.id.button_2).setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                Toast.makeText(MainActivity.this, "长按", Toast.LENGTH_LONG).show();
                return true;
            }
        });
    }
}

而有了这两个事件绑定注解时,我们会将上面的代码改为下面这样,这也是我们最终效果的代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtils.inject(this);            //在此处处理此类中的时间绑定注解
    }

    @OnClick({R.id.button_1})                //为 id 为 button_1 的 View 设置 点击事件,
    public void click(View view) {           //当 button_1 点击时,将触发此函数
        Toast.makeText(this, "点击", Toast.LENGTH_SHORT).show();
    }

    @OnLongClick({R.id.button_2})            //为 id 为 button_2 的 View 设置 长按事件
    public boolean longClick(View view) {    当 button_2 长按时,将触发此函数
        Toast.makeText(this, "长按", Toast.LENGTH_LONG).show();
        return true;
    }
}

确定了目标之后,我们就一步一步来实现这两个功能。这里可以看到,有三个地方需要我们去实现:

  • @OnClick 注解
  • @OnLongClick 注解
  • InjectUtils.inject(this) 代码

这三个只是我们现在看到的,如果要实现这个功能,要写的代码肯定只多不少。

风景_稻田

实现 @OnClick@OnLongClick 注解

我们先拿 @OnClick 来举个例子,正常情况下,根据要求,我们会写如下的注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

这是没问题的,Target 设置为 METHOD 表明了这个注解是放到方法上的,RUNTIME 保证了我们能够在运行时拿到这个注解,里面设置了 int[] 类型的方法,使得其可以指定多个 View 的 ID。

到现在看似没什么问题,但是这里还是有问题。想象一下后续我们会使用反射来进行 View 的事件绑定,那么在事件的绑定时,需要什么信息呢?需要的信息有两个:

  • 事件绑定的方法,例如 View.setOnClickListener() 这个方法
  • 设置的事件的类型,例如 View.OnClickListener 这个类型

这里说需要者两个信息大家可能会一知半解,那么先放在这里,只知道需要这两个信息就行了。等看到后面自然就明白了。

现在知道需要这两个信息,那么我们添加这两个信息不就可以了?确实是这样的,但是问题又来了,这两个信息添加到哪里呢?如果我们直接添加到 @OnClick 这个注解的方法中,就像这样:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
    String listenerSetter() default "setOnClickListener";
    Class<?> listenerType() default View.OnClickListener.class;
}

也不是不行,但是这样外层在用的时候,还需要指定这两个参数。这会对使用方造成误解,而且对于每一个注解,这两个信息是固定的,外面再指定是多余的,即使用 default 设置默认值,外边再重新指定这两个参数,也是我们不想看到的。

那最好的办法是怎样的呢?那就是注解多态,也就是使用自定义元注解。此处我们为 @OnClick@OnLongClick 自定义一个元注解 @EventBase

自定义元注解 @EventBase

上面我们知道那两个额外的信息是跟注解相关的,也就是说相同的注解这两个值是相同的,这种情况,自定义一个元注解,然后来修饰这两个注解是最合适不过了。如下,我自定义了一个 @EventBase 的元注解,这个注解只能放到注解上,且两个属性值对应着上面的两个信息:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {
    String listenerSetter();    //绑定事件的方法,例如 "setOnClickListener"
    Class<?> listenerType();    //绑定的事件的类型,例如 View.OnClickListener.class
}

那么后面我们用这个注解来修改 @OnClick@OnLongClick 注解。

最终的 @OnClick 注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class)
public @interface OnClick {
    int[] value();
}

最终的 @OnLongClick 注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnLongClickListener", listenerType = View.OnLongClickListener.class)
public @interface OnLongClick {
    int[] value();
}

注意这两个注解虽然都使用了 @EventBase 来修改,但是传递给 @EventBase 的两个参数是不同的,都是与自己这个事件相关的方法和参数类型。

现在,这两个注解已经确定下来了,接下来我们就要去实现 InjectUtils 这个类,而且这个类也是实现这个功能的关键所在。

利用反射和动态代理绑定事件初期器

有人会问,你这个绑定的功能为什么会起个名字叫 InjectUtils,大家不要在意这个细节,只要知道这个类里的方法是实现 setOnClickListener 这个方法的就行,而且一般情况下,是在 MainActivity.onCreate 方法中调用的。

在最开始,我们确定了,这个类里面有 InjectUtils.inject(this) 方法,我们将会在 onCreate 中调用这个方法,就像调用 ButterKnife.bind(this) 一样。那么怎么写这个方法呢?我们一步一步来。
薰衣草

找到有 @OnClick@OnLongClick 注解的函数

既然我么要处理带有绑定注解的函数,那我们肯定就要找到有哪些函数被标记了自定义的 @OnClick@OnLongClick 注解。

这一步分的代码很简单,因为我们在 MainActivity.onCreate 方法中调用了 InjectUtils.inject(this),而这个 this 就表示需要进行注解解析的类,通过它的 Class对象,我们就能够找到哪些函数添加了需要处理的注解。

这部分的代码如下:

public class InjectUtils {

    public static void inject(Object context) {
        Class<?> clazz = context.getClass();
        Method[] methods = clazz.getDeclaredMethods();

        for(Method method: methods) {
            Annotation[] annotations = method.getAnnotations();
            for(Annotation annotation : annotations) {
                Class<? extends Annotation> annotationClass = annotation.annotationType();
                EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
                //判断这个注解有没有被 @EventBase 标记,如果标记了,那么就能获取到。是我们应该处理的注解
                if(eventBase == null)
                    continue;

                //走到这里就表示,这个方法是被 @OnClick 或 @OnLongClick 标记过的,应该处理绑定
                //TODO: binding
            }
        }
    }
}

这里主要有两个循环,外层循环是对这个类的所有方法循环,内层循环是对方法上的注解的循环。另外判断这个函数是否是事件处理的方法,则是判断注解是否被 @EventBase 注解标记,如果被这个注解标记,那么就是我们需要处理的方法了。

找到 View 并调用 setOnClickListener

接下来,走到上面 TODO 的地方,就是处理事件绑定。这里快到了全文的重点了,我们慢慢来看。

后面我们要做的其实就是调用 View.setOnClickListener(new OnClickListener()),那么用反射完成这个调用需要分几步呢?很简单,这就像要把大象装冰箱,总共要分三步:

  1. 首先要找到 View 对象
  2. 再找到 setOnClickListener 方法
  3. 通过反射 invoke 这个方法

是不是很简单,下面就是代码了:

try {
    Method valueMethod = annotationClass.getMethod("value");
    int[] viewIds = (int[]) valueMethod.invoke(annotation);        //获取传递给 @OnClick 的所有 View 的 ID
    if(viewIds == null)
        continue;

    for(int viewId: viewIds) {                                    // 每一个 View 都要调用 setOnClickListener
        Method findViewByIdMethod = clazz.getMethod("findViewById", int.class);
        View view = (View) findViewByIdMethod.invoke(context, viewId);         // 找到这个 viewId 对应的 View 对象
        if(view == null)
            continue;

        String listenerSetter = eventBase.listenerSetter();
        Class<?> listenerType = eventBase.listenerType();
        Method setListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);    //找到 setOnClickListener 方法
        setListenerMethod.invoke(view, /*TODO*/);            //通过反射 invoke 这个方法
    }

} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {
    exception.printStackTrace(System.err);
}

对于这一段代码,如果看了 Java Reflection 反射使用 完全指南,就很容易看懂。简单来说,就是上面的三步,这里需要注意的一点就是,获取 setOnClickListener 这个方法的地方,因为我们有两个注解,一个处理点按 @OnClick,一个处理长按 @OnLongClick,因此这里不能写死。

那么怎么获取对应的方法呢?还记得上面说的 @EventBase 这个注解么,这个注解就是提供这两个信息的。这两个信息是在写 @OnClick@OnLongClick 注解时就传递给 @EventBase 注解的,现在大家知道了为什么还要添加 @EventBase 这个注解了吧。一是能够通过它来判断哪些是我们写的注解,二是提供了这两个信息。

这样我们就拿到了对应的 setListenerMethod 方法,那么调用它即可。不过我们在调用时会发现一个很严重的问题(即代码中的 TODO 部分):传什么参数过去?第一个参数当然是 View 对象,那第二个参数呢?是不是需要实现一个 OnClickListener 的对象?答案是否定的,这个时候,我们需要动态代理!

创建动态代理处理事件

在这里我们确实需要一个 OnClickListener 实例,但在这个地方是不可能的,我们只能使用动态代理来做这个事情。

为什么要用动态代理呢?因为这里需要代理掉 OnClickListener 中的 onClick 方法。
为什么能用动态代理?因为目标对象是 OnClickListener 接口的实例。

这里我先将代码贴上,咱们再解释。另外,强烈建议在看这一部分之前看完 代理模式与静态代理、动态代理的实现(Proxy.newProxyInstance、InvocationHandler)
这篇文章,否则再怎么解释也很难理解的。

Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{listenerType}, 
    new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method proxyMethod, Object[] args) throws Throwable {
            return method.invoke(context, args);
        }
    }
);

setListenerMethod.invoke(view, proxy);

创建动态代理的三个参数,第一个选择当前 ActivityClassLoader;第二个是要设置的对应的接口,@OnClickOnClickListener 接口,@OnLongClickOnLongClickListener 接口;重点是第三个 InvocationHandler

当需要我们真的点击这个按钮的时候,点击事件会触发到 InvocationHandler.invoke 的方法,在这个方法中,我们必须通过反射调用被 @OnClick 或是 @OnLongClick 标记的函数,也就是在第一遍遍历中找到的那个 method

这个 Proxy.newProxyInstance 返回的是一个 Object 对象,但由于我们传入的是 listenerType 这个接口,因此其实际返回的就是一个 OnClickListener 或是 OnLongClickListener 接口的对象。而这个返回的对象,就是我们要设置到 setListenerMethod.invoke 中的参数。
风景_晚霞_海岸
至此,大功告成。上面是三个代码片段,下面我把 inject 整个方法的代码全部贴出来,大家看看。

public static void injectClick(Object context) {
    Class<?> clazz = context.getClass();
    Method[] methods = clazz.getDeclaredMethods();

    for(Method method: methods) {
        Annotation[] annotations = method.getAnnotations();
        for(Annotation annotation : annotations) {
            Class<? extends Annotation> annotationClass = annotation.annotationType();
            EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
            if(eventBase == null)
                continue;

            try {
                Method valueMethod = annotationClass.getMethod("value");
                int[] viewIds = (int[]) valueMethod.invoke(annotation);
                if(viewIds == null)
                    continue;

                for(int viewId: viewIds) {
                    Method findViewByIdMethod = clazz.getMethod("findViewById", int.class);
                    View view = (View) findViewByIdMethod.invoke(context, viewId);
                    if(view == null)
                        continue;

                    String listenerSetter = eventBase.listenerSetter();
                    Class<?> listenerType = eventBase.listenerType();

                    Object proxy = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{listenerType},
                            (proxy1, method1, args) -> method.invoke(context, args));

                    Method setListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                    setListenerMethod.invoke(view, proxy);
                }

            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {
                exception.printStackTrace(System.err);
            }
        }
    }
}

总结

通过自定义 @OnClick@OnLongClick 注解以及使用反射和动态代理技术,我们成功地简化了点击事件的处理代码。这样的实现不仅提高了代码的可读性和维护性,还展示了高级编程技术在实际项目中的应用。

虽然这种技术看起来很牛逼,但是里面使用了大量的反射,会慢一些。而且这种代码交接别人维护的话,肯定是一脸懵逼。所以我总结一下:

不如 setOnClickListener ,也就自己装逼用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1825413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GStreamer——教程——基础教程2:GStreamer concepts

基本教程2&#xff1a;GStreamer概念 1&#xff0c;目标 之前的教程展示了如何自动构建管道。现在我们将手动构建一条pipeline&#xff1a;初始化每一个element并将它们连接起来。在此过程中&#xff0c;我们将学习&#xff1a; 什么是GStreamer元素以及如何创建一个。 如何…

redis设计与实现(五)RDB与AOF持久化

RDB持久化 因为Redis是内存数据库&#xff0c;它将自己的数据库状态储存在内存里面&#xff0c;所以如果不想办法将储存在内存中的数据库状态保存到磁盘里面&#xff0c;那么一旦服务器进程退出&#xff0c;服务器中的数据库状态也会消失不见。 为了解决这个问题&#xff0c;…

CC2500和CC1101移植说明

主要通过如何移植、移植注意、关于芯片配置、如何生成导出配置四大步骤来说明CC2500和CC1101移植 首先通过下图1这个宏进行选择 如何移植 要移植的部分在 CC2500_hal.c 和 CC2500_hal.h中, 搜索 "//移植" 就可以定位到 库 所需的依赖, 需要根据 您的环境实现这些…

sap怎么批量给信息记录打上删除标识

1.MEMASSIN-----事务代码 2.选择完成字段 3.根据条件查询需要冻结的信息记录 4.输入查询条件 5.全部勾选完成标识&#xff0c;点击保存&#xff0c;即可冻结完成

AI Stable diffusion 报错:稳定扩散模型加载失败,退出

可能是内存不够&#xff0c;看看你最近是加了新的大的模型&#xff0c;可以把你的stable-diffusion-webui\models\Stable-diffusion目录下的某个ckpt删除掉&#xff0c;可能ckpt太大&#xff0c;无法加载成功&#xff1b; Stable diffusion model failed to load, exiting 如图…

【RAM】利用AWS Resource Access Manager服务实现与其他账户共享AWS资源

文章目录 1. 先决条件说明2. 导航至ARM控制面板3. 指定资源共享详细信息4. 关联托管式权限5. 向委托人授予访问权限6. 查看和创建7. 查看由我共享的资源8. 资源共享详细信息9. 取消关联10. 参考链接11. 生成式AI书籍推荐&#x1f4e2; 1. 先决条件说明 报错现象&#xff1a; …

工业屏:千万不要以为电脑显示啥样,工业屏就啥样,注意色差。

重要的事情说三遍&#xff1a;一定要放到实际场景下调色&#xff0c;定要放到实际场景下调色&#xff0c;定要放到实际场景下调色。 工业控制屏的色域通常比普通电脑显示器要小。工业控制屏主要用于工业环境中&#xff0c;需要满足特定行业的需求和标准。由于工业控制屏的设计…

④-2单细胞学习-cellchat单数据代码补充版(通讯网络)

目录 通讯网络系统分析 ①社会网络分析 1&#xff0c;计算每个细胞群的网络中心性指标 2&#xff0c;识别细胞的信号流模式 ②非负矩阵分解&#xff08;NMF&#xff09;识别细胞的通讯模式 1&#xff0c;信号输出细胞的模式识别 2&#xff0c;信号输入细胞的模式识别 信…

el-table表头修改文字或者背景颜色,通过header-row-style设置样式

方式一 <el-table :header-cell-style"{text-align: center}" />方式二 <template><el-table :header-cell-style"tableHeaderColor" /> </template> <script> export default {methods: {tableHeaderColor ({row, column…

驱动开发(三):内核层控制硬件层

驱动开发系列文章&#xff1a; 驱动开发&#xff08;一&#xff09;&#xff1a;驱动代码的基本框架 驱动开发&#xff08;二&#xff09;&#xff1a;创建字符设备驱动 驱动开发&#xff08;三&#xff09;&#xff1a;内核层控制硬件层​​​​​​​ ←本文 目录…

一般简单的功能使用GIS 服务器工具还需要进行扩展开发吗?

有网友困惑怎么使用了我们的 GIS 服务器工具&#xff0c;一些简单的功能需不需要对服务器进行扩展开发&#xff1f;下面我们首先看一下标准GIS WEB服务器例如GEOSERVER&#xff0c;QGISSERVER、GIS数据快捷共享发布工具&#xff08;建立自己的地图网站&#xff09;及其它一些商…

技术转管理,是灾难还是奇迹?

深耕技术or转战管理&#xff1f;this is a question! 如果你还没有想好&#xff0c;那请继续往下看&#xff01; 技术专家&#xff1a;技术前瞻者、方案构建者、难题破解者、团队聚核者 管理专家&#xff1a;战略规划者、高效组织者、变革引领者、团队建设者 特点和重心都不在…

还在用Flex布局?OUT了 快来看看grid布局吧!

还在用Flex布局&#xff1f;OUT了 快来看看grid布局吧&#xff01; 之前我在写布局的时候就喜欢用flex&#xff0c;有一次我需要实现四个方块占一行的需求 &#xff0c;But 一共有七个方块到了第二行很Egg疼 &#xff0c;第二行它直接三个方块居中排布&#xff0c; 这三兄弟直…

什么是GPT-4

什么是GPT-4 ChatGPT 可以说&#xff0c;ChatGPT的发展&#xff0c;主要的分水岭在GPT-4&#xff0c;GPT-4主要是文本对话&#xff0c;且训练度也不够完善。GPT-4之后不但训练度得到了巨大提升&#xff0c;模型支持的参数量更是预计有1万亿参数&#xff0c;在这之后出现的GPT-4…

电机控制安全:PWM 直通

在 H 桥中使用互补 PWM 时的一个主要考虑因素是短路的可能性&#xff0c;也称为“击穿”。 如图 5 所示&#xff0c;如果同一支路上的两个开关同时打开&#xff0c;H 桥配置可能会导致电源和接地之间发生直接短路。 如果同一条腿上的两个开关同时打开&#xff0c;则可能会发生…

【Kafka】Kafka Producer 分区-05

【Kafka】Kafka Producer 分区-05 1. 分区的好处2. 分区策略2.1 默认的分区器 DefaultPartitioner 3. 自定义分区器 1. 分区的好处 &#xff08;1&#xff09;便于合理使用存储资源&#xff0c;每个Partition在一个Broker上存储&#xff0c;可以把海量的数据按照分区切割成一块…

sslyze一键检查服务器检查服务器的 SSL/TLS 安全性(KALI工具系列二十五)

目录 1、KALI LINUX 简介 2、sslyze工具简介 3、信息收集 3.1 目标主机IP&#xff08;服务器&#xff09; 3.2 KALI的IP 4、操作示例 4.1 扫描主机和端口 4.2 批量扫描 4.3 插件扫描 4.4 输出结果 5、总结 1、KALI LINUX 简介 Kali Linux 是一个功能强大、多才多艺…

【会议征稿,ACM出版】2024年云计算与大数据国际学术会议(ICCBD 2024,7月26-28)

2024年云计算与大数据国际学术会议(ICCBD 2024)将于2024年7月26-28日在中国大理召开。ICCBD 2024将围绕“云计算与大数据”的最新研究领域, 旨在为从事研究的专家、学者、工程师和技术人员提供一个国际平台&#xff0c;分享科研成果和尖端技术&#xff0c;了解学术发展趋势&…

潮玩宇宙大逃杀APP系统开发成品案例分享指南

这是一款多人游戏&#xff0c;玩家需要选择一个房间躲避杀手。满足人数后&#xff0c;杀手会随机挑选一个房间杀掉里面所有的参与者&#xff0c;其他房间的幸存者将平均瓜分被杀房间的元宝。玩家在选中房间后&#xff0c;倒计时结束前可以自由切换不同房间。 软件项目开发成品…

B3981 [信息与未来 2024] 图灵完备

题目描述 &#xff08;你不需要看懂这张图片&#xff1b;但如果你看懂了&#xff0c;会觉得它很有趣。&#xff09; JavaScript 是一种功能强大且灵活的编程语言&#xff0c;也是现代 Web 开发的三大支柱之一 (另外两个是 HTML 和 CSS)。灵活的 JavaScript 包含“自动类型转换…