万万没想到,做防重复点击都会被坑4下

  • 2020 年 2 月 23 日
  • 筆記

前几天,发现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事件做一个防止重复点击

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

    btNeayby.setOnClickListener(new View.OnClickListener() {          @Override          public void onClick(View v) {              long nowTime = System.currentTimeMillis();              if (nowTime - mLastClickTime > TIME_INTERVAL) {                  enterActiviy()              }          }      });  }

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

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

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的同学应该知道:

 @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注解方法处理而已
  @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他没做防重复的事情你可不可以加,当然是可以的

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

 @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呢?

如是,我的方案是:

@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事件中做了转发

view.setOnClickListener(new View.OnClickListener() {                      @Override                      public void onClick(View v) {                          listener.onClick(v);                      }                  });

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

A.click(view1)    B.click(view1)

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

super.onClick(v)

 @Override      public void onClick(View view) {          super.onClick(view);          switch (view.getId()) {

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

A.click(view1)    A.click(view1)

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

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了

#-------------------------注解AOP----------------------  -adaptclassstrings  -keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation*  -keepnames @org.aspectj.lang.annotation.Aspect class * {      ajc* <methods>;  }