【安卓开发小经验】Android防抖动四种方案实践

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体现,系统稳定性更高,何乐不为?

防抖动方案按事件的产生顺序和处理顺序的方向来划分,可分为“先到达先处理型”和“后到达后处理型”
本文包括以下几种

  1. 传统计算时间间隔
  2. RxJava
  3. RxBinding
  4. 同一任务执行最后一次
    前三个是先到达先处理型,第四个是后到的后处理型

传统计算时间间隔

第一次触发事件的时候记录一个时间戳,下一次触发事件时,再记录一个时间戳,在指定的时间间隔内,返回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才会被执行。

总结

在这里插入图片描述

以下是一个简单的 Android UVCCamera 实现视频防抖动的示例代码: ```java public class UvcCameraPreview implements SurfaceHolder.Callback, Camera.PreviewCallback { private static final String TAG = "UvcCameraPreview"; private SurfaceHolder mHolder; private Camera mCamera; private ByteBuffer mPreviewBuffer; private byte[] mPreviewFrame; private int mPreviewWidth; private int mPreviewHeight; private int mFrameSize; private boolean mIsPreviewing = false; private Handler mHandler = new Handler(); private Runnable mStabilizationRunnable = new Runnable() { @Override public void run() { stabilizeFrame(); } }; public UvcCameraPreview(SurfaceView surfaceView) { mHolder = surfaceView.getHolder(); mHolder.addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { mCamera = Camera.open(); mCamera.setPreviewCallbackWithBuffer(this); Camera.Parameters params = mCamera.getParameters(); List<Camera.Size> sizes = params.getSupportedPreviewSizes(); Camera.Size size = sizes.get(0); params.setPreviewSize(size.width, size.height); mCamera.setParameters(params); try { mCamera.setPreviewDisplay(holder); } catch (IOException e) { e.printStackTrace(); } mPreviewWidth = size.width; mPreviewHeight = size.height; mFrameSize = mPreviewWidth * mPreviewHeight * ImageFormat.getBitsPerPixel(params.getPreviewFormat()) / 8; mPreviewBuffer = ByteBuffer.allocate(mFrameSize); mCamera.addCallbackBuffer(mPreviewBuffer.array()); mCamera.startPreview(); mIsPreviewing = true; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mIsPreviewing = false; mCamera.stopPreview(); mCamera.setPreviewCallback(null); mCamera.release(); mCamera = null; } @Override public void onPreviewFrame(byte[] data, Camera camera) { mPreviewFrame = data; mHandler.post(mStabilizationRunnable); if (mCamera != null && mIsPreviewing) { mCamera.addCallbackBuffer(mPreviewBuffer.array()); } } private void stabilizeFrame() { // 实现视频防抖动的代码 // 将 mPreviewFrame 中的数据进行防抖动处理 // 处理后的数据可以进行显示或者存储 // 处理完成后,可以继续进行下一帧的处理 mHandler.post(mStabilizationRunnable); } } ``` 在这个示例中,我们使用了 `SurfaceView` 来显示预览画面,并通过 `SurfaceHolder.Callback` 监听 `SurfaceView` 的生命周期。在 `surfaceCreated` 方法中,我们打开了相机,设置了预览的参数和大小,并将预览的数据回调设置为 `this`,即当前类。我们还创建了一个 `ByteBuffer` 来存储预览数据,并将其添加到相机的回调缓冲区中。 在 `onPreviewFrame` 方法中,我们将相机返回的预览数据存储到 `mPreviewFrame` 数组中,并通过 `Handler` 在另一个线程中调用 `stabilizeFrame` 方法对预览数据进行防抖动处理。在 `stabilizeFrame` 中,我们可以对 `mPreviewFrame` 数组中的数据进行防抖动处理,并将处理后的数据显示或者保存。最后,我们通过 `Handler` 的方式在处理完成后继续对下一帧数据进行处理,从而实现视频防抖动的效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值