RxCursorSample是我在一个用Rx特性实现的图片多选/单选选择器
特性:
1.使用RxJava操作ContentProvider的Cursor,并且在图片详情页面,使用RxJava操作符实现了一个RecyclerView的简单动画,提升了用户体验
2.使用RxBus实现组件通信
3.适配了Android 6.0的权限请求
其他几个特性不分析了,主要分析下项目中的Rx:(源码在文章结尾)
** RxBus(这里是我的一篇关于RxBus的简书)负责了Fragment和Activity通信**
Activity接收RxBus的事件,然后通过FragmentManager切换Fragment。
下面是在ImagePickerActivity的主要代码:
private void initRxBus() {
// 接收 切换相册事件
rxSubscriptions.add(RxBus.getDefault().toObserverable(AddDetailEvent.class)
.map(addDetailEvent -> addDetailEvent.getBucketName())
.subscribe(bucketName -> {
// add DetailExploreFragment
addFragment(DetailExploreFragment.newInstance(bucketName, isMultiplePick));
}, throwable -> {
throwable.printStackTrace();
Toast.makeText(this, R.string.yo_switch_bucket_exception,Toast.LENGTH_SHORT).show();
}));
// 接收 切换预览事件
rxSubscriptions.add(RxBus.getDefault().toObserverable(AddPreviewEvent.class)
.map(addPreviewEvent -> addPreviewEvent.getImgs())
.subscribe(imgs -> {
toolbar.setTitle(R.string.yo_preview);
// add PreviewFragment
addFragment(PreviewFragment.newInstance(imgs, isMultiplePick));
}, throwable -> {
throwable.printStackTrace();
Toast.makeText(this, R.string.yo_switch_preview_exception, Toast.LENGTH_SHORT).show();
}));
}
上面的代码中,比如点击了“预览”,由DetailExploreFragment跳转至PreviewFragment,通过RxBus将事件传递到Activity,Activity再切换PreviewFragment
下面是DetailExploreFragment 中的代码,点击“预览”,发送事件
btnPreview.setOnClickListener((v) -> {
// addFragment
RxBus.getDefault().post(new AddPreviewEvent(imgs));
});
** RxJava操作ContentProvider的Cursor**
创建Observable<Cursor>的代码:
private Observable<Cursor> cursorObservable() {
return Observable.create(new Observable.OnSubscribe<Cursor>() {
@Override
public void call(Subscriber<? super Cursor> subscriber) {
if (!subscriber.isUnsubscribed()) {
try{
Cursor cursor = getCursor();
// 判断!subscriber.isUnsubscribed() 是为了在取消订阅时,保证cursor可以及时关闭
while (cursor.moveToNext() && !subscriber.isUnsubscribed()) {
subscriber.onNext(cursor);
}
subscriber.onCompleted();
}catch(Exeception e){
subscriber.onError(e);
}finally {
assert cursor != null;
cursor.close();
}
}
}
});
}
private Cursor getCursor() {
String[] mediaColumns = new String[]{
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATA,
"COUNT(*) AS " + COLUMN_NAME_COUNT
};
// SELECT _data, COUNT(*) AS v_count FROM video WHERE ( GROUP BY bucket_display_name)
String selection = " 1=1 ) GROUP BY (" + MediaStore.Images.Media.BUCKET_DISPLAY_NAME;
return _activity.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaColumns, selection, null, null);
}
Cursor是个比较特殊的数据库操作类,在该例中,在Cursor的游标移动到结尾之前,会一直发射Observable<Cursor>的数据源。
代码中while里有判断!subscriber.isUnsubscribed(),是为了在取消订阅时而cursor还没有执行完的情况下,保证cursor可以及时关闭。
下面在IO线程中将Observable<Cursor>转换成List<BucketEntity>,最终在主线程中的RecyclerView中绑定数据显示。
private void initData() {
subscription = cursorObservable()
.subscribeOn(Schedulers.io())
.filter(cursor -> {
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
return !path.endsWith(".gif");
})
.map(cursor -> {
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
String bucket_name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME));
int count = cursor.getInt(cursor.getColumnIndex(COLUMN_NAME_COUNT));
return new BucketEntity(bucket_name, count, path);
})
.toList()
// 如果不用toList()转成List 一定要调用onBackpressureBuffer()方法,防止数据源发射过快,导致异常MissingBackpressureException
// .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
bucketEntity -> {
adapter.setDatas(bucketEntity);
}, throwable -> {
throwable.printStackTrace();
Toast.makeText(_activity, R.string.yo_find_exception, Toast.LENGTH_SHORT).show();
}
);
}
filter操作符过滤掉的gif图片,map操作符将Observable<Cursor>对象转换成Obser<BucketEntity>,这里有2种绑定数据到RecyclerView的方法:
1. 数据源集合绑定:
使用toList操作符,它将一连串的Observable<T>类型数据源集合转换成一个Observable<List<T>>返回,最后一次性绑定数据到RecyclerView上
2. 单数据源依次绑定:
即通过Adapter.addData(bean),一个一个数据逐个绑定到RecyclerView上,但是如果直接这样操作,则会报预测MissingBackpressureException的异常,该异常是由于数据源发射速度过快导致的。
可以使用onBackpressureBuffer操作符解决,它可以缓冲发射速度过快的数据源,直到所有数据源全部发射出去。
这个例子中,使用的是第一种方法
下面这段代码使用了第二种,同时加入了一个操作符,视觉上形成一个逐条加入的动画
private void initData() {
subscription = cursorObservable()
.subscribeOn(Schedulers.io())
// 延迟60ms发射数据 形成动画, delay默认在computation线程 要主动切换到当前的线程
.delay(60, TimeUnit.MILLISECONDS, Schedulers.immediate())
.filter(cursor -> {
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
return !path.endsWith(".gif");
})
.map(cursor -> {
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
return new File(path);
})
// onBackpressureBuffer()方法,防止数据源发射过快引起的MissingBackpressureException
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
file -> {
adapter.addData(file);
}, throwable -> {
throwable.printStackTrace();
Toast.makeText(_activity, R.string.yo_find_exception, Toast.LENGTH_SHORT).show();
}
);
}
delay操作符可以延迟发射数据源。
因为该操作符默认在computation线程中运行,我们需要延迟的时间在数据源处理的IO线程上,所以主动指定在Schedules.immediate()上,即当前的IO线程上。(关于RxJava的线程高效使用,可以参考小鄧子的这篇译文)
这样的话,每隔60ms观察者就会收到一个经过加工的File数据源,然后将其绑定到RecyclerView上。
在视觉上,每个RecyclerView的Item都会在上个Item显示后的60ms后显示,仅仅通过一个操作符完成了一个Item显示动画 :)
更多详情可以查看源码,这里是完整的代码。