前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万万没想到,做防重复点击坑这么多

万万没想到,做防重复点击坑这么多

原创
作者头像
brzhang
修改2020-03-07 10:34:57
1.4K0
修改2020-03-07 10:34:57
举报
文章被收录于专栏:玩转全栈玩转全栈

前几天,发现App设置页中有一堆的入口,点击一些item快点会启动两个页,举个例子,就比如说微信这个发现页:

这里,点击每个入口都会进入一个新的Activity,但是,如果快速点击的话,比如快速点击附近的人,将会出现两个附近的人页。

因此,我们要如何解决这个问题呢?

  1. 将所有的Activity设置为singleTop
  2. 对附近的人这个按钮的onClick事件做一个防止重复点击

两种方式都是没问题的,但是,却都有问题,首页我们来分析第一种:

将所有的Activity设置为singleTop

为什么说这种方式有问题,首先,我们要了解singleTop启动模式是干嘛,他是说,如果当前Activity已经在栈顶了,那么,就不在启动一个新的这个Activity,只是调用它的onNewIntent,我们能排除一定不会在栈顶已经有这个Activity的时候,在开同样的页面吗?不能!业务千变万化。

那么,singleTask可以吗?抱歉更加不行,singleTask表示如果这个页面栈中有这个Activity的话,就复用它,并且干掉处在它上面的所有Activity,让自己处于栈顶,妥妥的踢人上位,谁,因此,我们更不可能将所有的Activity设置为singleTask模式了。

所以,第一种方式弊端很明显:

  • 我们不能为了方式用会多开页面,就以偏概全,将所有的Activity设置为singleTop。
  • 而且,这么做局限性很大,因为没有看到问题的本质,问题的本质是因为onClick执行两次造成的,而出现两个Activity只是结果。
  • 我们却要对这个结果进行容错,而不是针对引发这个现象的源头进行处理,就有点本末倒置了。

针对这个按钮的onClick事件做一个防止重复点击

嗯,这回看似已经找到了问题造成的根源了,如是,你这么写:

代码语言:javascript
复制
    btNeayby.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            long nowTime = System.currentTimeMillis();
            if (nowTime - mLastClickTime > TIME_INTERVAL) {
                enterActiviy()
            } 
        }
    });
}

一些变量就不在这里给出了,相信你也能看懂这个逻辑,对一处点击能起到防止重复点击的效果,那么,其他地方呢?其他地方你都要写这样一段逻辑,都要定义一个最后一次点击的时间,好麻烦~~

所以,有没有办法,不用去定义这些变量,去写包裹逻辑,回答是有的

代码语言:javascript
复制
RxView.clicks(view)
    .throttleFirst(1, TimeUnit.SECONDS)
    .subscribe(new Consumer<Object>() {
        @Override
        public void accept(Object o) throws Exception {
            enterActiviy()
        }
     });

嗯,看起来貌似是可以了,比第一版简洁不少,没有mLastClickTime变量的定义了,但是,项目中肯定有很多地方需要点击事件的,难不成,你每个地方都用RxView.clicks去包裹一遍 所以,有没有再简洁一点的呢,答案是有的

Android APT(编译时代码生成),相信对这个有所了解的小伙伴大概知道我会说什么了?如果你还不了解这个灰科技,可以看看这篇文章 Android APT(编译时代码生成)最佳实践

解决思路我帮你理一理:

  1. 定义一个注解Annotation,比如就叫做SingleClick
  2. 有了APT这个灰科技,在编译时根据这个Annotation生成了相关的代码。

相信了解过ButterKnife的同学应该知道:

代码语言:javascript
复制
 @OnClick(R.id.bt_submit)
    public void submit() {
        title.setText("hello world");
    }

这个注解,实际上他做了什么事呢?

  1. 生成代码将R.id.bt_submit 通过findViewBy()绑定到一个变量,比如mSubmit上来。
  2. mSubmit设置onClick事件。
  3. 在onClick事件的处理中,将处理权转发给submit这个被onClick注解方法处理而已
代码语言:javascript
复制
  @Override
    public void onClick(View v) {
        Method method = null;
        try {
            method = receiver.getClass().getMethod(clickMethodName);
            if (method != null) {
                method.invoke(receiver);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "未找到:" + clickMethodName + "方法");
        }
            try {
                if (method == null) {
                    method = receiver.getClass().getMethod(clickMethodName, View.class);
                    if (method != null) {
                        method.invoke(receiver, v);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "未找到带view类型参数的:" + clickMethodName + "方法");
            }
    }

只是,ButterKnife的OnClick注解并没有做防重复点击。

ButterKnife他没做防重复的事情你可不可以加,当然是可以的

加了之后,是不是可以写成这样子了?

代码语言:javascript
复制
 @SingleClick(R.id.bt_submit)
 public void submit() {
        title.setText("hello world");
 }

然后submit是被被转发过来,就看你APT的逻辑了。

眼看都到了这个份上了,就这样玩了吗?当然还没有,我们:

我们还不满足,因为,加入老子就是不喜欢用APT框架怎么办?就是不喜欢自己写view.setOnlickListenser(...)

我们最终祭出终极大杀器,AOP

可能,知道点AOP的同学就秒懂了,没错,面向切面编程,我们为什么不拦截onClick做点文章呢?

在想到这个方案之后,我就搜索了一下github,果然不出所料,有小伙伴就用这种方式处理了,GitHub - jarryleo/SingleClick: 安卓点击事件防重库

不过,我看到了我不大喜欢的地方,既然老子都用AOP了,干嘛还哟啊在定义一个注解SingleClick呢?我为什么不直接拦截所有的onClick呢?

如是,我的方案是:

代码语言:javascript
复制
@Aspect
public class OnClickAspect {
    @Pointcut("execution(* onClick(..))")
    public void onClickPointcut() {
    }

    @Around("onClickPointcut()")
    public void onClick(ProceedingJoinPoint joinPoint) throws Throwable {

        // 取出方法的参数 ,想判是不是 onClick(View viw)这种类型的方法
        View view = null;
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof View) {
                view = (View) arg;
                break;
            }
        }
        if (view == null) {
            joinPoint.proceed();
            return;
        }

        // 取出方法的注解,如果标记可以多次点击的,就直接走
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(MutilClick.class)){
            joinPoint.proceed();
            return;
        }
        if (!XClickUtil.isFastDoubleClick(joinPoint.getTarget(),view, 500)) {
            // 不是快速点击,执行原方法
            joinPoint.proceed();
        }

    }

}

 当然,我在做的过程中,也是发现了4个坑:

  1. 有些地方的点击需要多次点击怎么办?
  2. 如果在onClick事件中做了转发怎么处理?
  3. 如果出现super.onClick(v)怎么处理?
  4. 打release包就出现NPE了怎么处理?

以上的第一个问题是客观存在的,比如,我们连续点击一个按钮几次,弹出我们的后门,因此,我加了一个MutilClick的注解,来规避这种情况,这种情况极少,可能一两处而已。

然而对于

onClick事件中做了转发

代码语言:javascript
复制
view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        listener.onClick(v);
                    }
                });

哈哈,你妹啊,这不就是活生生的onClick(v)被瞬间就调了两次,妥妥的重复点击了,这肯定就造成页面上这种情况的按钮无法点了,怎么处理,别急,我们发现调用主体不同。实际上这种情况等同于:

代码语言:javascript
复制
A.click(view1)

B.click(view1)

因此,可以判断一下调用的主体是否一致,具体方法下面会给出。

super.onClick(v)

代码语言:javascript
复制
 @Override
    public void onClick(View view) {
        super.onClick(view);
        switch (view.getId()) {

尴尬了吧,这种时候调用的主体都变成了一个,其实就等于

代码语言:javascript
复制
A.click(view1)

A.click(view1)

啥都一样,不一样的就是先后各了几ms而已,等等,人的手速可能几ms吗?显然是不可能的,因此,我们似乎又找到了路子,所以总结起来,我们的防重复点击工具类可以这么写:

代码语言:javascript
复制
package com.tencent.igame.common.utils;

import android.text.TextUtils;
import android.util.Log;
import android.view.View;

public class XClickUtil {
    /**
     * 最近一次发生事件的target
     */
    private static String mLastTargetName;
    /**
     * 最近一次点击的时间
     */
    private static long mLastClickTime;
    /**
     * 最近一次点击的控件ID
     */
    private static int mLastClickViewId;

    /**
     * 是否是快速点击
     *
     * @param v              点击的控件
     * @param intervalMillis 时间间期(毫秒)
     * @return true:是,false:不是
     */
    public static boolean isFastDoubleClick(Object target, View v, long intervalMillis) {
        int viewId = v.getId();
        long time = System.currentTimeMillis();
        long timeInterval = Math.abs(time - mLastClickTime);
        //10,表示手速不可能这么快,突破ms
        if (timeInterval>10 && timeInterval < intervalMillis && viewId == mLastClickViewId && TextUtils.equals(getTargetHash(target), mLastTargetName)) {
            Log.e("XClickUtil", "重复点击 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]");
            return true;
        } else {
//         fixme   这里其实可以加一下自动埋点
            Log.e("XClickUtil", "单次点击 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]");
            mLastTargetName = getTargetHash(target);
            mLastClickTime = time;
            mLastClickViewId = viewId;
            return false;
        }
    }

    private static String getTargetHash(Object object) {
        return object.getClass().getName() + "@" + object.hashCode();
    }
}

最后一个坑,打release包直接就NPE

这种情况肯定就是混淆导致的了,一般加上混淆配置就OK了

代码语言:javascript
复制
#-------------------------注解AOP----------------------
-adaptclassstrings
-keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation*
-keepnames @org.aspectj.lang.annotation.Aspect class * {
    ajc* <methods>;
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 将所有的Activity设置为singleTop
  • 针对这个按钮的onClick事件做一个防止重复点击
    • 所以,有没有办法,不用去定义这些变量,去写包裹逻辑,回答是有的
    • 我们最终祭出终极大杀器,AOP
      •  当然,我在做的过程中,也是发现了4个坑:
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档