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
网友评论
1.依赖都添加了吗?
2.@Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))")这个要修改成你自己的SingleClick类的全路径
https://blog.csdn.net/innost/article/details/49387395
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关键字?
@OnClick({R.id.button4})
public void onClickEvent() {
switch (R.id.button4) {}
加上singclick会导致onclick不响应事件,请问怎么办?
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/blob/master/aspectjx/build.gradle
* 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
可能你需要在项目build.gradle下添加:
repositories {
......
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
}
比如网易新闻:https://www.jianshu.com/p/c3c5b175a7c9
大厂App接入测试过没问题的话,我们就可以放心使用了