前几天,发现App设置页中有一堆的入口,点击一些item快点会启动两个页,举个例子,就比如说微信这个发现页:
这里,点击每个入口都会进入一个新的Activity,但是,如果快速点击的话,比如快速点击附近的人,将会出现两个附近的人页。
因此,我们要如何解决这个问题呢?
- 将所有的Activity设置为singleTop
- 对附近的人这个按钮的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(编译时代码生成)最佳实践
解决思路我帮你理一理:
- 定义一个注解Annotation,比如就叫做SingleClick
- 有了APT这个灰科技,在编译时根据这个Annotation生成了相关的代码。
相信了解过ButterKnife的同学应该知道:
代码语言:javascript复制 @OnClick(R.id.bt_submit)
public void submit() {
title.setText("hello world");
}
这个注解,实际上他做了什么事呢?
- 生成代码将R.id.bt_submit 通过findViewBy()绑定到一个变量,比如
mSubmit
上来。 - 给
mSubmit
设置onClick事件。 - 在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他没做防重复的事情你可不可以加,当然是可以的
加了之后,是不是可以写成这样子了?
代码语言: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个坑:
- 有些地方的点击需要多次点击怎么办?
- 如果在onClick事件中做了转发怎么处理?
- 如果出现super.onClick(v)怎么处理?
- 打release包就出现NPE了怎么处理?
以上的第一个问题是客观存在的,比如,我们连续点击一个按钮几次,弹出我们的后门,因此,我加了一个MutilClick的注解,来规避这种情况,这种情况极少,可能一两处而已。
然而对于
代码语言:javascript复制onClick事件中做了转发
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onClick(v);
}
});
哈哈,你妹啊,这不就是活生生的onClick(v)被瞬间就调了两次,妥妥的重复点击了,这肯定就造成页面上这种情况的按钮无法点了,怎么处理,别急,我们发现调用主体不同。实际上这种情况等同于:
代码语言:javascript复制A.click(view1)
B.click(view1)
因此,可以判断一下调用的主体是否一致,具体方法下面会给出。
代码语言:javascript复制super.onClick(v)
@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>;
}