限制按钮点击_Android | 使用 AspectJ 限制按钮快速点击

前言

  • Android开发中,限制按钮快速点击(按钮防抖)是一个常见的需求;
  • 在这篇文章里,我将介绍一种使用AspectJ的方法,基于注解处理器 & 运行时注解反射的原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

系列文章

  • 《Android | 一文带你全面了解 AspectJ 框架》
  • 《Android | 使用 AspectJ 限制按钮快速点击》

延伸文章

  • 关于 反射,请阅读:《Java | 反射:在运行时访问类型信息(含 Kotlin)》
  • 关于 注解,请阅读:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》
  • 关于 注解处理器(APT),请阅读:《Java | 注解处理器(APT)原理解析 & 实践》

目录

0c97e8f7e43f883107f50ba43ce1be9a.png

1. 定义需求

在开始讲解之前,我们先 定义需求,具体描述如下:

  • 限制快速点击需求 示意图:

518a492f327edd121e59f8838469d036.png

2. 常规处理方法

目前比较常见的限制快速点击的处理方法有以下两种,具体如下:

2.1 封装代理类

封装一个代理类处理点击事件,代理类通过判断点击间隔决定是否拦截点击事件,具体代码如下:

// 代理类
public abstract class FastClickListener implements View.OnClickListener {private long mLastClickTime;private long interval = 1000L;public FastClickListener() {}public FastClickListener(long interval) {this.interval = interval;}@Overridepublic void onClick(View v) {long currentTime = System.currentTimeMillis();if (currentTime - mLastClickTime > interval) {// 经过了足够长的时间,允许点击onClick();mLastClickTime = nowTime;} }protected abstract void onClick();
}

在需要限制快速点击的地方使用该代理类,具体如下:

tv.setOnClickListener(new FastClickListener() {@Overrideprotected void onClick() {// 处理点击逻辑}
});

2.2 RxAndroid 过滤表达式

使用RxJava的过滤表达式throttleFirst也可以限制快速点击,具体如下:

RxView.clicks(view).throttleFirst(1, TimeUnit.SECONDS).subscribe(new Consumer<Object>() {@Overridepublic void accept(Object o) throws Exception {// 处理点击逻辑}});

2.3 小结

代理类RxAndroid过滤表达式这两种处理方法都存在两个缺点: - 1. 侵入核心业务逻辑,需要将代码替换到需要限制点击的地方; - 2. 修改工作量大,每一个增加限制点击的地方都要修改代码。

我们需要一种方案能够规避这两个缺点 —— AspectJAspectJ是一个流行的Java AOP(aspect-oriented programming)编程扩展框架,若还不了解,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》


3. 详细步骤

在下面的内容里,我们将使用AspectJ框架,把限制快速点击的逻辑作为核心关注点从业务逻辑中抽离出来,单独维护。具体步骤如下:

步骤1:添加AspectJ依赖

  • 1、依赖沪江的AspectJXGradle插件 —— 在项目build.gradle中添加插件依赖:
// 项目级build.gradle
dependencies {classpath 'com.android.tools.build:gradle:3.5.3'classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}

如果插件下载速度过慢,可以直接依赖插件 jar文件,将插件下载到项目根目录(如/plugins),然后在项目build.gradle中添加插件依赖:

// 项目级build.gradle
dependencies {classpath 'com.android.tools.build:gradle:3.5.3'classpath fileTree(dir:'plugins', include:['*.jar'])
}
  • 2、应用插件 —— 在App Modulebuild.gradle中应用插件:
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
  • 3、依赖AspectJ框架 —— 在包含AspectJ代码的Modulebuild.gradle文件中添加依赖:
// Module级build.gradle
dependencies {...api 'org.aspectj:aspectjrt:1.8.9'...
}

步骤2:实现判断快速点击的工具类

  • 我们先实现一个判断View是否快速点击的工具类;
  • 实现原理是使用Viewtag属性存储最近一次的点击时间,每次点击时判断当前时间距离存储的时间是否已经经过了足够长的时间;
  • 为了避免调用View#setTag(int key,Object tag)时传入的key与其他地方传入的key冲突而造成覆盖,务必使用在资源文件中定义的 id,资源文件中的 id 能够有效保证全局唯一性,具体如下:
// ids.xml
<resources><item type="id" name="view_click_time" />
</resources>
public class FastClickCheckUtil {/*** 判断是否属于快速点击** @param view     点击的View* @param interval 快速点击的阈值* @return true:快速点击*/public static boolean isFastClick(@NonNull View view, long interval) {int key = R.id.view_click_time;// 最近的点击时间long currentClickTime = System.currentTimeMillis();if(null == view.getTag(key)){// 1. 第一次点击// 保存最近点击时间view.setTag(key, currentClickTime);return false;}// 2. 非第一次点击// 上次点击时间long lastClickTime = (long) view.getTag(key);if(currentClickTime - lastClickTime < interval){// 未超过时间间隔,视为快速点击return true;}else{// 保存最近点击时间view.setTag(key, currentClickTime);return false;}}
}

步骤3:定义Aspect切面

使用@Aspect注解定义一个切面,使用该注解修饰的类会被AspectJ编译器识别为切面类:

@Aspect
public class FastClickCheckerAspect {// 随后填充
}

步骤4:定义PointCut切入点

使用@Pointcut注解定义一个切入点,编译期AspectJ编译器将搜索所有匹配的JoinPoint,执行织入:

@Aspect
public class FastClickAspect {// 定义一个切入点:View.OnClickListener#onClick()方法@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")public void methodViewOnClick() {}// 随后填充 Advice
}

步骤5:定义Advice增强

增强的方式有很多种,在这里我们使用@Around注解定义环绕增强,它将包装PointCut,在PointCut前后增加横切逻辑,具体如下:

@Aspect
public class FastClickAspect {// 定义切入点:View.OnClickListener#onClick()方法@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")public void methodViewOnClick() {}// 定义环绕增强,包装methodViewOnClick()切入点@Around("methodViewOnClick()")public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {// 取出目标对象View target = (View) joinPoint.getArgs()[0];// 根据点击间隔是否超过2000,判断是否为快速点击if (!FastClickCheckUtil.isFastClick(target, 2000)) {joinPoint.proceed();}}
}

步骤6:实现View.OnClickListener

在这一步我们为View设置OnClickListener,可以看到我们并没有添加限制快速点击的相关代码,增强的逻辑对原有逻辑没有侵入,具体代码如下:

// 源码:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Log.i("AspectJ","click");}});}
}

编译代码,随后反编译AspectJ编译器执行织入后的.class文件。还不了解如何查找编译后的.class文件,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》

public class MainActivity extends AppCompatActivity {protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(2131361820);findViewById(2131165349).setOnClickListener(new View.OnClickListener() {private static final JoinPoint.StaticPart ajc$tjp_0;// View.OnClickListener#onClick()public void onClick(View v) {View view = v;// 重构JoinPoint,执行环绕增强,也执行@Around修饰的方法JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this, view);onClick_aroundBody1$advice(this, view, joinPoint, FastClickAspect.aspectOf(), (ProceedingJoinPoint)joinPoint);}static {ajc$preClinit();}private static void ajc$preClinit() {Factory factory = new Factory("MainActivity.java", null.class);ajc$tjp_0 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("1", "onClick", "com.have.a.good.time.aspectj.MainActivity$1", "android.view.View", "v", "", "void"), 25);}// 原来在View.OnClickListener#onClick()中的代码,相当于核心业务逻辑private static final void onClick_aroundBody0(null ajc$this, View v, JoinPoint param1JoinPoint) {Log.i("AspectJ", "click");}// @Around方法中的代码,即源码中的aroundViewOnClick(),相当于Adviceprivate static final void onClick_aroundBody1$advice(null ajc$this, View v, JoinPoint thisJoinPoint, FastClickAspect ajc$aspectInstance, ProceedingJoinPoint joinPoint) {View target = (View)joinPoint.getArgs()[0];if (!FastClickCheckUtil.isFastClick(target, 2000)) {// 非快速点击,执行点击逻辑ProceedingJoinPoint proceedingJoinPoint = joinPoint;onClick_aroundBody0(ajc$this, v, (JoinPoint)proceedingJoinPoint);null;} }});}
}

小结

到这里,我们就讲解完使用AspectJ框架限制按钮快速点击的详细,总结如下: - 使用@Aspect注解描述一个切面,使用该注解修饰的类会被AspectJ编译器识别为切面类; - 使用@Pointcut注解定义一个切入点,编译期AspectJ编译器将搜索所有匹配的JoinPoint,执行织入; - 使用@Around注解定义一个增强,增强会被织入匹配的JoinPoint


4. 演进

现在,我们回归文章开头定义的需求,总共有4点。其中前两点使用目前的方案中已经能够实现,现在我们关注后面两点,即允许定制时间间隔覆盖尽可能多的点击场景

  • 需求回归 示意图:

82d8d973dd1e3eb98c201c874db4c4ae.png

4.1 定制时间间隔

在实际项目不同场景中的按钮,往往需要限制不同的点击时间间隔,因此我们需要有一种简便的方式用于定制不同场景的时间间隔,或者对于一些不需要限制快速点击的地方,有办法跳过快速点击判断,具体方法如下: - 定义注解

/*** 在需要定制时间间隔地方添加@FastClick注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
  • 修改切面类的Advice
@Aspect
public class SingleClickAspect {public static final long FAST_CLICK_INTERVAL_GLOBAL = 1000L;@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")public void methodViewOnClick() {}@Around("methodViewOnClick()")public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {// 取出JoinPoint的签名MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();// 取出JoinPoint的方法Method method = methodSignature.getMethod();// 1. 全局统一的时间间隔long interval = FAST_CLICK_INTERVAL_GLOBAL;if (method.isAnnotationPresent(FastClick.class)) {// 2. 如果方法使用了@FastClick修饰,取出定制的时间间隔FastClick singleClick = method.getAnnotation(FastClick.class);interval = singleClick.interval();}// 取出目标对象View target = (View) joinPoint.getArgs()[0];// 3. 根据点击间隔是否超过interval,判断是否为快速点击if (!FastClickCheckUtil.isFastClick(target, interval)) {joinPoint.proceed();}}
}
  • 使用注解
findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {@FastClick(interval = 5000L)@Overridepublic void onClick(View v) {Log.i("AspectJ","click");}
});

4.2 完整场景覆盖

ButterKnife @OnClick android:onClick OK RecyclerView / ListView Java Lambda NO Kotlin Lambda OK DataBinding OK

Editting...


推荐阅读

密码学 | Base64 是加密算法吗?​juejin.im算法面试题 | 回溯算法解题框架​juejin.im算法面试题 | 链表问题总结​juejin.imJava | 带你理解 ServiceLoader 的原理与设计思想​juejin.im计算机网络 | 图解 DNS & HTTPDNS 原理​juejin.imAndroid | 说说从 android:text 到 TextView 的过程​juejin.imAndroid | 面试必问的 Handler,你确定不看看?​www.jianshu.com
98108819ad46b3c06fdc30b05ae782c3.png
Android | 带你探究 LayoutInflater 布局解析原理​juejin.imAndroid | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?​juejin.im

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

5c43b38385b0545ee6fdc692f6d585e0.png

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

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

相关文章

svn不知道这样的主机 怎么解决_家里装修不知道怎么配置净水器,这几招教你轻松解决...

为了保障家庭饮水健康&#xff0c;很多业主都会选择在家中安装一台厨下净水器。但是&#xff0c;如若仅靠它来满足全家人日常洗漱&#xff0c;沐浴和饮用&#xff0c;这势必会极大地影响到全家人的生活品质。这个时候&#xff0c;实阳机电良心建议&#xff0c;全屋净水系统&…

redis系列:通过文章点赞排名案例学习sortedset命令

前言 这一篇文章将讲述Redis中的sortedset类型命令&#xff0c;同样也是通过demo来讲述&#xff0c;其他部分这里就不在赘述了。 项目Github地址&#xff1a;https://github.com/rainbowda/learnWay/tree/master/learnRedis/case-sortedset 案例 demo功能是文章点赞排名等等&am…

xml生成2维码_MyBatis(2)之MyBatis-Generator最佳实践

自定义注释自定义注解指定xml文件模式上一篇文章详细阐述了xml配置文件的各种标签及其含义。其实从标签开始&#xff0c;每一个标签都对应一个实体类。context.class对应标签&#xff0c;而每一个子标签都对应一个属性&#xff1b;如图&#xff1a;标签与实体类的对应关系。有了…

nginx 配置详解_Nginx 配置详解

序言Nginx是lgor Sysoev为俄罗斯访问量第二的http://rambler.ru站点设计开发的。从2004年发布至今&#xff0c;凭借开源的力量&#xff0c;已经接近成熟与完善。Nginx功能丰富&#xff0c;可作为HTTP服务器&#xff0c;也可作为反向代理服务器&#xff0c;邮件服务器。支持Fast…

推荐系统——GBDT+LR

[[逻辑回归模型]] 逻辑回归是在[[线性回归]]的基础上添加了一个Sigmoid函数&#xff08;非线形&#xff09;映射&#xff0c;从而可以使逻辑回归成为一个优秀的分类算法 逻辑回归假设数据服从[[伯努利分布]]&#xff0c;通过[[极大化似然函数]]的方法&#xff0c;运用[[梯度下降…

从燃尽图看项目管理:你的项目哪里出错了?(燃尽图类型全解析)

什么是燃尽图 燃尽图&#xff08;burn down chart&#xff09;是在项目完成之前&#xff0c;对需要完成的工作任务的一种可视化表示。理想情况下&#xff0c;该图表是一个向下的曲线&#xff0c;随着项目任务的逐渐完成“烧尽”至零。 燃尽图常常用于敏捷开发中&#xff0c;作为…

springtboot 引用子工程的文件_xmake从入门到精通11:如何组织构建大型工程

xmake是一个基于Lua的轻量级现代化c/c的项目构建工具&#xff0c;主要特点是&#xff1a;语法简单易上手&#xff0c;提供更加可读的项目维护&#xff0c;实现跨平台行为一致的构建体验。本文主要详细讲解下&#xff0c;如何通过配置子工程模块&#xff0c;来组织构建一个大规模…

依赖项出现感叹号怎么办_SpringBoot中如何对依赖进行管理?

SpringBoot中的起步依赖(starter)是一组特定功能的依赖项集合&#xff0c;SpringBoot通过starter来进行项目的依赖管理&#xff0c;而不是直接基于单独的依赖项来进行依赖管理。starter其实就是特殊的Maven依赖项或者Gradle依赖项&#xff0c;它把常用的库组合到一起构成了一个…

5g理论速度_5G是什么?5G速度有多快?

原标题&#xff1a;5G是什么&#xff1f;5G速度有多快&#xff1f;5G到底是什么东西&#xff1f;今年5G网络会普及吗&#xff1f;5G网速到底有多快&#xff0c;背后又有哪些黑科技&#xff1f;近日&#xff0c;全球首个5G火车站在上海虹桥火车站启动建设。而根据三大运营商的时…

c++ new一个结构体_「C/C++」构造类型及应用:数组、结构体、共用体、枚举类型...

3.1数组同类型、同性质、按顺序存放的一组数据集合&#xff0c;易于批量处理。3.1.1一维数组定义int 1.数组名为常量&#xff0c;指向首地址&#xff0c;由系统指定。2.数组长度为整型常量,但不能为03.上例取值image[0]-image[255]&#xff0c;取值可以修改。初始化int 输入for…

自动产生fsm代码的工具_代码自动生成工具

构建支持多种数据库类型的代码自动生成工具背景&#xff1a;一般的业务代码中写来写去&#xff0c;无外乎是先建好model&#xff0c;然后针对这个model做些CRUD的操作。(主要针对单表的业务操作)针对于数据库dao、mapper等的代码自动生成已经有了mybatisGenerator这种工具&…

gtest测试框架使用详解_测试框架TestNG使用介绍

近期接触到了一个比较全面的基于Java的接口自动化测试框架&#xff0c;作为一名Java小白&#xff0c;所以打算研究一下&#xff0c;顺带学习学习Java&#xff0c;该测试框架的逻辑控制层使用的HttpClient TestNG。在本期中&#xff0c;给大家分享一下TestNG测试框架的基础知识…

LOJ#6282. 数列分块入门 6

一个动态的插入过程&#xff0c;还需要带有查询操作。 我可以把区间先分块&#xff0c;然后每个块块用vector来维护它的插入和查询操作&#xff0c;但是如果我现在这个块里的vector太大了&#xff0c;我可能的操作会变的太大&#xff0c;所以这时候我需要把现在里面的数全部拿出…

fragment在activity中的静态和动态用法_使用Matlab修改压缩Gif动态图片制作微信表情...

脚本之家你与百万开发者在一起作者&#xff1a;theOwlAndPussyCat/焦旭光引言电脑里存了很多有意思的Gif动态图片&#xff0c;闲暇想把这些动图全导入微信表情&#xff0c;可是这些动图很多大小超过了微信表情大小1MB的限制&#xff0c;要制作成表情只能压缩图像文件大小。网上…

frontcon函数用不了_C++复制构造函数与析构函数

想用机器人赋能未来&#xff0c;少不了扎实的编程的基本功&#xff0c;让我们跟着清华大学的C语言程序设计课程一起过一遍C的语法知识吧&#xff01;当定义基本类型的变量时&#xff0c;经常会用已有的变量去初始化新定义的变量&#xff0c;当定义对象的时候也有类似的需求&…

软件项目立项书_2019年度上海市软件和集成电路产业发展专项资金项目立项

上海艾瑞德生物科技有限公司荣获2019年上海市软件和集成电路产业发展专项资金(集成电路和电子信息制造领域)项目立项&#xff01;上海艾瑞德生物科技有限公司的【医用体外诊断动态光场图像采集电子模块的研发及产业化】喜获2019年上海市软件和集成电路产业发展专项资金(集成电路…

mysql提供了表示日期和时间的数据类型_MySQL数据类型 - 日期和时间类型(1)

1.日期和时间数据类型语法用于表示时间值的日期和时间数据类型是DATE, TIME, DATETIME, TIMESTAMP和 YEAR。对于TIME, DATETIME和 TIMESTAMP值&#xff0c;MySQL支持小数秒&#xff0c;精度可达微秒(6位数)。要定义包含小数秒部分的列&#xff0c;请使用语法type_name(fsp)&…

软件质量保证计划_CMMI V2.0 精讲之“过程质量保证”

过程质量保证(PROCESS QUALITY ASSURANCE, PQA)目的&#xff1a;验证并改进已执行的过程和所产生的工作产品的质量。价值&#xff1a;增强过程使用和改进的一致性&#xff0c;以最大限度地提高业务效益和客户满意度。实践概述第1级PQA 1.1识别并解决过程和工作产品问题。第2级P…

mysql gzip_在mysql中存储GZIP:ed文本?

Is it a common thing for bigger applications and databases to GZIP text data before inserting it to the database?Ill guess that any full-text search on the actual text field will not be working before unzipping it again?解决方案Ive not seen this done muc…

html 分页_JQuery堪称完美的分页函数

演示效果&#xff1a;html部分&#xff08;引入jquery.js&#xff09;<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>jQuery简单的分页插件</title> </head><link rel"stylesheet" href"…