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

微信扫一扫,分享到朋友圈

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

前言

Android
AspectJ

目录

1. 定义需求

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

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

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;
}
@Override
public void onClick(View v) {
long currentTime = System.currentTimeMillis();
if (currentTime - mLastClickTime > interval) {
// 经过了足够长的时间,允许点击
onClick();
mLastClickTime = nowTime;
}
}
protected abstract void onClick();
}
复制代码

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

tv.setOnClickListener(new FastClickListener() {
@Override
protected void onClick() {
// 处理点击逻辑
}
});
复制代码

2.2 RxAndroid 过滤表达式

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

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

2.3 小结

代理类RxAndroid过滤表达式 这两种处理方法都存在两个缺点:

  • 1. 侵入核心业务逻辑,需要将代码替换到需要限制点击的地方;
  • 2. 修改工作量大,每一个增加限制点击的地方都要修改代码。

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

3. 详细步骤

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

步骤1:添加AspectJ依赖

  • 依赖沪江的 AspectJX Gradle插件 —— 在项目 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'])
}
复制代码
  • 应用插件—— 在 App Modulebuild.gradle 中应用插件:

    // App Module的build.gradle apply plugin: ‘android-aspectjx’ …

  • 依赖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

    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 {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
@Override
public 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(),相当于Advice
private 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点。其中前两点使用目前的方案中已经能够实现,现在我们关注后面两点,即 允许定制时间间隔覆盖尽可能多的点击场景

  • 需求回归 示意图:

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) @Override public void onClick(View v) { Log.i(“AspectJ”,”click”); } });

微信扫一扫,分享到朋友圈

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

基于安卓11的ColorOS 11正式版面向OPPO Ace2系列推送:内置防卡顿引擎

上一篇

MIT6.824分布式系统课程 翻译&学习笔记(三)GFS

下一篇

你也可能喜欢

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

长按储存图像,分享给朋友