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

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

前言


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


系列文章



延伸文章



目录

image.png

1. 定义需求


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

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

image.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;
    }
    @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是一个流行的JavaAOP(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'])
}
复制代码
  1. 应用插件 —— 在App Modulebuild.gradle中应用插件:


// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
复制代码
  1. 依赖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 {
    @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点。其中前两点使用目前的方案中已经能够实现,现在我们关注后面两点,即允许定制时间间隔覆盖尽可能多的点击场景

  • 需求回归 示意图:

image.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)
    @Override
    public 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...

目录
相关文章
|
XML Android开发 数据格式
android点击FrameLayout、LinearLayout等父布局没响应的原因以及解决方案
android点击FrameLayout、LinearLayout等父布局没响应的原因以及解决方案
506 2
|
测试技术 Android开发
Android按钮防抖动,避免发送多次请求
Android按钮防抖动,避免发送多次请求
301 0
|
9月前
|
存储 消息中间件 人工智能
【03】AI辅助编程完整的安卓二次商业实战-本地构建运行并且调试-二次开发改注册登陆按钮颜色以及整体资源结构熟悉-优雅草伊凡
【03】AI辅助编程完整的安卓二次商业实战-本地构建运行并且调试-二次开发改注册登陆按钮颜色以及整体资源结构熟悉-优雅草伊凡
279 3
|
9月前
|
存储 消息中间件 人工智能
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
262 11
【05】AI辅助编程完整的安卓二次商业实战-消息页面媒体对象(Media Object)布局实战调整-按钮样式调整实践-优雅草伊凡
|
XML Android开发 数据格式
Android利用selector(选择器)实现图片动态点击效果
本文介绍了Android中ImageView的`src`与`background`属性的区别及应用,重点讲解如何通过设置背景选择器实现图片点击动态效果。`src`用于显示原图大小,不拉伸;`background`可随组件尺寸拉伸。通过创建`selector_setting.xml`,结合`setting_press.xml`和`setting_normal.xml`定义按下和正常状态的背景样式,提升用户体验。示例代码展示了具体实现步骤,包括XML配置和形状定义。
565 3
Android利用selector(选择器)实现图片动态点击效果
|
10月前
|
机器学习/深度学习 Android开发 数据安全/隐私保护
手机脚本录制器, 脚本录制器安卓,识图识色屏幕点击器【autojs】
完整的UI界面,包含录制控制按钮和状态显示 屏幕点击动作录制功能,记录点击坐标和时间间隔
|
XML IDE 开发工具
【Android UI】自定义带按钮的标题栏
【Android UI】自定义带按钮的标题栏
241 7
【Android UI】自定义带按钮的标题栏
|
存储 Android开发
安卓app,MediaPlayer播放本地音频 | 按钮控制播放和停止
在Jetpack Compose中,不直接操作原生Android组件如`Button`和`MediaPlayer`,而是使用Compose UI构建器定义界面并结合ViewModel管理音频播放逻辑。以下示例展示如何播放本地音频并用按钮控制播放/停止:创建一个`AudioPlayerViewModel`管理`MediaPlayer`实例和播放状态,然后在Compose UI中使用`Button`根据`isPlaying`状态控制播放。记得在`MainActivity`设置Compose UI,并处理相关依赖和权限。
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
XML Java Android开发
15. 【Android教程】按钮 Button/ImageButton
15. 【Android教程】按钮 Button/ImageButton
629 2

热门文章

最新文章