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

2020-03-07 10:34:57 浏览数 (1)

前几天,发现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>;
}

0 人点赞