Android防抖动方案总结
首先讨论防抖动的必要性
一段不防抖动的代码示例:
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) {
mbRequest.deleteSingle(bean, position);
StatisticsUtilFamilyMeals.mbDelete(this, MBHomePageActivity.class.getSimpleName());
} else if (view.getId() == R.id.iv_resend) {
mbRequest.updateCacheMessage(bean, position);
}
}
测试人员频繁点击一个Delete按钮,多次发出了同一份Request,该Request中携带了相同的请求体,服务端只能处理一份,先到达的Request将被服务端处理,后到达的Request将被服务端标记为处理失败,客户端会依次收到处理成功、处理失败、处理失败......
以上就是一个典型的点击事件频繁创建的问题,为了防止事件的频繁创建、响应、发出Request、让服务端处理,我们一般会做防抖动——即将点击事件过滤调。
由此我们可以看到防抖动的必要性:
如果不防抖动,则会触发许多无用的网络请求、异步任务,浪费系统资源、后台进程资源。
如果做了抖动,我们将收获流畅的UI体现,系统稳定性更高,何乐不为?
防抖动方案按事件的产生顺序和处理顺序的方向来划分,可分为“先到达先处理型”和“后到达后处理型”
本文包括以下几种
- 传统计算时间间隔
- RxJava
- RxBinding
- 同一任务执行最后一次
前三个是先到达先处理型,第四个是后到的后处理型
传统计算时间间隔
第一次触发事件的时候记录一个时间戳,下一次触发事件时,再记录一个时间戳,在指定的时间间隔内,返回false,不去执行后续的业务逻辑
public class MBUtils {
/**
* 两次点击按钮接口之间的点击间隔不能少于1300毫秒,人为控制这个开关
*/
private static final long MIN_CLICK_DELAY_TIME = 1300;
private static long lastClickTime;
/**
* @return true:频繁点击 ;fasle 不是频繁点击
*/
public boolean isFastClick() {
boolean flag = true;
long curClickTime = System.currentTimeMillis();
if ((curClickTime - lastClickTime) >= MIN_CLICK_DELAY_TIME) {
flag = false;
}
lastClickTime = curClickTime;
return flag;
}
}
将文章开头提到的错误代码改为:
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) {
if (MBUtils.isFastClick()) { // 点击太快不予执行 return; } mbRequest.deleteSingle(bean, position); } }
}
}
RxJava
这种方案的效果是,当一个动作连续触发,则只执行第一次。主要利用了Rxjava#throttleFirst方法的特性,在指定时间间隔内,Observer不会收到subscribe发来的消息,达到防抖动的效果
- ObservableOnSubscribe用于观察View的Click事件,在接收到系统发来的click消息后,通过onNext传递给它的观察者
- Observer用于接收消息,一旦Rxjava底层发来了消息,将在onNext处理它,并通过myClickListener返回给上层,进行下一步的业务处理
public class MBUtils {
public interface ThrottleClickListener {
void onClick(View view);
}
public void throttleFirstProcess(long delay, TimeUnit unit, final View view, final ThrottleClickListener myClickListener) {
ObservableOnSubscribe<View> subscribe = new ObservableOnSubscribe<View>() {
@Override
public void subscribe(final ObservableEmitter<View> emitter) throws Exception {
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
emitter.onNext(view);
}
});
}
};
Observer<View> observer = new Observer<View>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(View view) {
myClickListener.onClick(view);
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
};
Observable.create(subscribe).throttleFirst(delay, unit).subscribe(observer);
}
}
将文章开头提到的错误代码改为:
MBUtils.throttleFirstProcess(1, TimeUnit.SECONDS, childView, new MBUtils.ThrottleClickListener() {
@Override
public void onClick(View view) {
}
});
RxBinding
RxBinding是一款非常强大组件库,用于将特定的UI组件与事件绑定起来,如将一个Button与Button的点击事件绑定。
以下是针对本文提到到频繁点击删除按钮的过滤用法,通过RxView对象,将Button对象与click事件绑定,用户点击Button后,会执行subscribe的Action#Call回调
Button btn = helper.itemView.findViewById(R.id.iv_delete);
RxView.clicks(btn)
.throttleFirst(3, TimeUnit.SECONDS)
.subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {
}
});
RxView支持哪些事件?
答:clicks longClicks draws drag layoutChange scrollChange setVisibility setClickable attaches detaches focusChanges globalLayouts hovers touches
集成方式:
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0' # 按需使用
implementation 'com.jakewharton.rxbinding4:rxbinding-core:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-appcompat:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-drawerlayout:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-leanback:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-recyclerview:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-slidingpanelayout:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-swiperefreshlayout:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-viewpager:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-viewpager2:4.0.0' # 按需使用
implementation 'com.jakewharton.rxbinding4:rxbinding-material:4.0.0'
同一任务执行最后一次
这种方案简单的说,当一个动作连续触发,则只执行最后一次。
查看代码,只有一个方法throttleFirstProcess方法会接收Runnnable对象作为value,以任务名作为key存储在一个ConcurrentHashMap里,每个Runnable的特点是将被延时1秒执行
为了实现只执行最后一次的效果,我们主要利用了ConcurrentHashMap对象put方法的特性,
举个例子,第一次执行Delete任务,ConcurrentHashMap会记下一个key为Delete,value为Rannable的任务;
指定时间间隔内,若第二次触发Delete任务,则ConcurrentHashMap会进行查找,一旦发现存在名为Delete的任务,我们将取出,并cancel掉第一次存入的Delete任务,这样保证了第一笔Delete任务在被执行之前取消掉。最终的效果就是只执行了第二次存入的Delete任务。
public class MBUtils {
private static final ScheduledExecutorService SCHEDULE = Executors.newSingleThreadScheduledExecutor();
private static final ConcurrentHashMap<Object, Future<?>> DELAYED_MAP = new ConcurrentHashMap<>();
/**
* 防抖動,只处理最后一次任务 * @param key 任务key值,同key代表过滤 * @param runnable 子线程执行任务 * @param delay 同一任务多少米内过滤 * @param unit 时间单位
*/
public static void throttleFirstProcess(final Object key, final Runnable runnable, long delay, TimeUnit unit) {
final Future<?> prev = DELAYED_MAP.put(key, SCHEDULE.schedule(() -> {
try {
runnable.run();
} finally {
DELAYED_MAP.remove(key);
}
}, delay, unit));
if (prev != null) {
prev.cancel(true);
}
}
}
使用起来也很简单:
将文章前面提到的错误代码改为:
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
MessageBean bean = mbsAdapter.getData().get(position);
if (view.getId() == R.id.iv_delete) { // 3秒内频繁点击,只处理最后一笔提交的任务 MBUtils.throttleFirstProcess("删除留言",()->{mbRequest.deleteSingle(bean, position); },1, TimeUnit.SECONDS); ... } }
}
}
1秒内点击多次,会创建n个同名为删除留言的Runnable,只有最后一个存入的Runnable才会被执行。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架