Android优雅地处理按钮重复点击

作者: XBaron | 来源:发表于2018-09-11 20:01 被阅读47次

    App中,有很大一部分场景是点击按钮,向服务端提交数据,由于网络请求需要时间,用户很可能会多次点击,造成数据重复提交,造成各种莫名其妙的问题。

    因此,防止按钮多次点击,是Android开发中一个很重要的技术手段。

    以前的处理方式

    网上查找到的,或者你可能会想到的方法大概有这些:

    1.每个按钮点击事件中,记录点击时间,判断是否超过点击时间间隔

    private long mLastClickTime = 0;
    public static final long TIME_INTERVAL = 1000L;
    private Button btTest;
    private void initView() {
        btTest = findViewById(R.id.bt_test);
        btTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                long nowTime = System.currentTimeMillis();
                if (nowTime - mLastClickTime > TIME_INTERVAL) {
                    // do something
                    mLastClickTime = nowTime;
                } else {
                    Toast.makeText(MainActivity.this, "不要重复点击", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
    

    这种方式,每个点击事件都需要写一个时间判断,重复代码很多。

    2.封装一个点击事件,处理点击间隔判断

    public abstract class CustomClickListener implements View.OnClickListener {
        private long mLastClickTime;
        private long timeInterval = 1000L;
    
        public CustomClickListener() {
    
        }
    
        public CustomClickListener(long interval) {
            this.timeInterval = interval;
        }
    
        @Override
        public void onClick(View v) {
            long nowTime = System.currentTimeMillis();
            if (nowTime - mLastClickTime > timeInterval) {
                // 单次点击事件
                onSingleClick();
                mLastClickTime = nowTime;
            } else {
                // 快速点击事件
                onFastClick();
            }
        }
    
        protected abstract void onSingleClick();
        protected abstract void onFastClick();
    }
    

    使用:

    btTest.setOnClickListener(new CustomClickListener() {
        @Override
        protected void onSingleClick() {
            Log.d("xxx", "onSingleClick");
        }
    
        @Override
        protected void onFastClick() {
            Log.d("xxx", "onFastClick");
        }
    });
    

    相比于第一种方式,这种方法将重复点击的判断封装在CustomClickListener内部,外部无需处理时间判断,只需要实现点击方法即可。

    3.利用RxAndroid处理重复点击

    RxView.clicks(view)
        .throttleFirst(1, TimeUnit.SECONDS)
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object o) throws Exception {
                // do something
            }
         });
    

    响应式地处理按钮点击,利用rxjava的操作符,来防止重复点击,相较于第1,2方案来说,此方法更为优雅一些。

    思考一下:

    这三种方法,不论哪一种,都对原有点击事件有很大的侵入性,要么你需要往Click事件中加方法,要么你需要替换整个Click事件,那么,有没有一种方式,可以在不改动原有逻辑的情况下,又能很好地处理按钮的重复点击呢?

    更为优雅的处理方式

    往同一类型的所有方法,都加上统一的处理逻辑,我们很快就能想到一个词:AOP,没错,面向切面编程

    如何使用AOP来解决重复点击问题?

    1.引入Aspectj

    Android 上使用AOP编程,一般使用Aspectj这个库

    站在巨人的肩膀上,沪江已经开源了Aspectj的Gradle插件,方便我们使用Aspectj

    • 在项目根目录下的build.gradle中,添加依赖:
    dependencies {
         ......
         classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
    }
    
    • 在app或其他module目录下的build.gradle中,添加:
    apply plugin: 'android-aspectjx'
    dependencies {
        ......
        implementation 'org.aspectj:aspectjrt:1.8.9'
    }
    

    2.添加一个自定义注解

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.METHOD)
    public @interface SingleClick {
        /* 点击间隔时间 */
        long value() default 1000;
    }
    

    添加自定义注解的原因是,方便管理哪些方法使用了重复点击的AOP,同时可以在注解中传入点击时间间隔,更加灵活。

    3.封装一个重复点击判断工具类

    public final class XClickUtil {
    
        /**
         * 最近一次点击的时间
         */
        private static long mLastClickTime;
        /**
         * 最近一次点击的控件ID
         */
        private static int mLastClickViewId;
    
        /**
         * 是否是快速点击
         *
         * @param v  点击的控件
         * @param intervalMillis  时间间期(毫秒)
         * @return  true:是,false:不是
         */
        public static boolean isFastDoubleClick(View v, long intervalMillis) {
            int viewId = v.getId();
            long time = System.currentTimeMillis();
            long timeInterval = Math.abs(time - mLastClickTime);
            if (timeInterval < intervalMillis && viewId == mLastClickViewId) {
                return true;
            } else {
                mLastClickTime = time;
                mLastClickViewId = viewId;
                return false;
            }
        }
    }
    

    4.编写Aspect AOP处理类

    @Aspect
    public class SingleClickAspect {
        private static final long DEFAULT_TIME_INTERVAL = 5000;
    
        /** 
         * 定义切点,标记切点为所有被@SingleClick注解的方法
         */
        @Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))")
        public void methodAnnotated() {}
    
        /** 
         * 定义一个切面方法,包裹切点方法
         */
        @Around("methodAnnotated()")
        public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
            // 取出方法的参数
            View view = null;
            for (Object arg : joinPoint.getArgs()) {
                if (arg instanceof View) {
                    view = (View) arg;
                    break;
                }
            }
            if (view == null) {
                return;
            }
            // 取出方法的注解
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            if (!method.isAnnotationPresent(SingleClick.class)) {
                return;
            }
            SingleClick singleClick = method.getAnnotation(SingleClick.class);
            // 判断是否快速点击
            if (!XClickUtil.isFastDoubleClick(view, singleClick.value())) {
                // 不是快速点击,执行原方法
                joinPoint.proceed();
            }
        }
    }
    

    使用方法

    private void initView() {
        btTest = findViewById(R.id.bt_test);
        btTest.setOnClickListener(new View.OnClickListener() {
            @SingleClick
            @Override
            public void onClick(View v) {
                // do something
            }
        });
    }
    

    只需要一个注解,即完成了按钮的防止重复点击,其他所有工作交给编译器,代码清爽了很多有木有。

    想必大家对于AOP一定产生了兴趣,关于AOP的更多用法,请关注微信公众号:Android必修课

    后面会出一个Android AOP的专题,让大家更好地学习和使用AOP

    相关文章

      网友评论

      • 程序员飞飞:不错!
      • b5bb1c8f90a2:有个问题,在recyclerview item上的点击事件,view获取到的id上一样的,就无法判断重复点击了。
        XBaron:本文重点是介绍使用AOP来去除样板代码,因此重复点击的判断只是简单使用了view的id来去重,大家可以自行扩展(例如可以添加view的tag作为重复点击判断的条件):blush:
      • 歌莫信息:我找你的代码写了,为什么没效果呀,反编译看了代码,并没有在那些有注解的方法里添加代码啊
        XBaron:@歌莫信息 嗯,我会修改一下文章,关于这个module依赖的问题的部分,以免其他人入坑
        歌莫信息:@XBaron 不是这个问题,原因已经找到了 我的SingleClickAspect写到了库项目里了,在主项目里用这个注解的话,需要在主项目的gradle文件中添加 apply plugin: 'android-aspectjx',否则无效
        XBaron:@歌莫信息 检查一下:
        1.依赖都添加了吗?
        2.@Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))")这个要修改成你自己的SingleClick类的全路径
      • XBaron:关于AOP的介绍,这篇文章写得不错,大家可以看看
        https://blog.csdn.net/innost/article/details/49387395
      • 99326d0627db:Configuration 'compile' is obsolete and has been replaced with 'implementation' and 'api'.
        It will be removed at the end of 2018. For more information see: http://d.android.com/r/tools/update-dependency-configurations.html

        sync build.gradle时提示这个 是否框架内包含compile关键字?
        99326d0627db:@alex_898e 是我自己写错了,public void onClickEvent() { 应该写成public void onClickEvent(View view) {
        99326d0627db:实测不支持butterknife.onClick注解,写法如下: @SingleClick
        @OnClick({R.id.button4})
        public void onClickEvent() {
        switch (R.id.button4) {}

        加上singclick会导致onclick不响应事件,请问怎么办?
        XBaron:@alex_898e 是aspectjx的插件build.gradle中包含了compile关键字
        https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/blob/master/aspectjx/build.gradle
      • b60bb2ced315:感谢作者提供新思路,其实AOP在Android有很多应用场景,权限检查和事件统计使用AOP都非常优雅。
        b60bb2ced315:@XBaron 感谢作者,希望作者可以多多分享下AOP的使用场景
        XBaron:@MinimaChay 是的,很多场景下都可以使用,此篇仅仅是简单介绍了一下AOP的使用,更多的场景看大家自行发现了。
      • lanyuu:Error:Connection refused (Connection refused),为什么我添加依赖之后,提示的是这个
        lanyuu:@XBaron FAILURE: Build failed with an exception.

        * What went wrong:
        A problem occurred configuring root project 'mall_android'.
        > Could not resolve all files for configuration ':classpath'.
        > Could not resolve com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0.
        Required by:
        project :
        > Could not resolve com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0.
        > Could not get resource 'https://dl.google.com/dl/android/maven2/com/hujiang/aspectjx/gradle-android-plugin-aspectjx/2.0.0/gradle-android-plugin-aspectjx-2.0.0.pom'.
        > Could not GET 'https://dl.google.com/dl/android/maven2/com/hujiang/aspectjx/gradle-android-plugin-aspectjx/2.0.0/gradle-android-plugin-aspectjx-2.0.0.pom'.
        > Connect to localhost:49329 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused (Connection refused)
        > Could not resolve com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0.
        > Could not get resource 'https://jcenter.bintray.com/com/hujiang/aspectjx/gradle-android-plugin-aspectjx/2.0.0/gradle-android-plugin-aspectjx-2.0.0.pom'.
        > Could not GET 'https://jcenter.bintray.com/com/hujiang/aspectjx/gradle-android-plugin-aspectjx/2.0.0/gradle-android-plugin-aspectjx-2.0.0.pom'.
        > Connect to localhost:49329 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] failed: Connection refused (Connection refused)

        * Try:
        Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

        * Get more help at https://help.gradle.org

        BUILD FAILED in 0s
        lanyuu:@XBaron 我添加了,还是这样,大神,可以加下你qq吗,我截图给你看:relaxed:
        XBaron:应该是依赖没找到吧
        可能你需要在项目build.gradle下添加:
        repositories {
        ......
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
        }
      • 小白象码农:怎么感觉没反应啊 是不是我哪里错了 @Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))") 是这样的吗?
        XBaron:“@me.baron.test.annotation.SingleClick”,这个要替换成你自己SingleClick这个类的全路径哦
      • 2df743050acf:看着好清爽,但是我看那个aspectjrt包有19M,感觉好大啊,不舍的用....
        2df743050acf:@XBaron 啊,那应该是我下错了,谢谢大佬给的网址 多谢
        XBaron:aspectjrt不大啊,100多KB,http://mvnrepository.com/artifact/org.aspectj/aspectjrt/1.8.9,你是不是看错了哇
      • 土拨鼠2018:听网上有人说aspectj会有兼容性问题么?会不会有失效的情况?
        XBaron:兼容性的话,由于测试机只有几台,暂时没有发现兼容性问题,关于Aspectj,很多大厂的App也都有接入
        比如网易新闻:https://www.jianshu.com/p/c3c5b175a7c9
        大厂App接入测试过没问题的话,我们就可以放心使用了:blush:
      • 大姚syf:这种方式可以和ButterKnife的点击处理配合使用吗 ?
        XBaron:完全可以,AOP会在编译期间织入代码,你可以反编译你的apk,找到你加了@SingleClick注解的方法,你会看到AOP在这个方法里加上了一段逻辑
      • JarryLeo:英雄所见略同啊:https://www.jianshu.com/p/8976e3e7695d 我的干脆把项目中的所有点击事件先防重了,要排除的再打注解:stuck_out_tongue_winking_eye:
        XBaron:@JarryLeo 原理是一样的哦,只是没加@SingleClick注解的话,如果协作开发,别人可能不知道这个方法有做去重处理,会在方法里再添加自己的去重逻辑
      • 56d0bb41d524:写的不错,期待楼主下一篇

      本文标题:Android优雅地处理按钮重复点击

      本文链接:https://www.haomeiwen.com/subject/yxqpgftx.html