一、背景
ByPhoto是个安卓图片选择库, 在启动渲染速度上做了很多优化; 荣耀8真机测试,图库里有3000多张图片。 冷启动图片选择页渲染完成需800ms左右, 热启动(即第二次打开Activity)渲染需要300ms。 真正实现了秒开的用户体验。
二、需求
1、支持图片预加载, 即将图库的前几张图片加载到内存中; 使用了Glide的preload;
2、数据库分段回调, 即图片有几千张图片时, 每查询一定数量时(例如10条)就通知UI补充数据; RecyclerView不会刷新屏幕外的图片,只是缓存了文件路径; PS:这里还可以再优化一下,例如列表向下滑动时预加载后半段数据; 但考虑到字符串占用内存不大,几兆的样子,暂未实现;
3、数据结构, 使用适当的数据结构Map、Set降低读写时间复杂度;
4、支持手指在屏幕滑动时自动勾选经过的图片;
5、勾选图片时只刷新选中图标, 不刷新item图片;避免纵向滑动时刷新闪烁的问题;
6、支持设置单行图片数量和最多选中数量;
三、核心代码
在子线程查询数据库,并分批通知UI数据变化;
@Override protected List doInBackground(String... strings) {
...
while (cursor.moveToNext()) {
...
//每隔10个图片报一次, 即分段通知UI数据变化
if (i % Constants.PHOTO_COUNT_PER_TIME == 0 && i > 0) {
ImageItem[] segData = new ImageItem[Constants.PHOTO_COUNT_PER_TIME];
for (int k = 0; k < Constants.PHOTO_COUNT_PER_TIME; k++) {
segData[k] = itemList.get(i - Constants.PHOTO_COUNT_PER_TIME + k);
}
publishProgress(segData);
}
return null;
}
预加载前几张图片到Glide缓存中, 默认加载前15张;
public static void preloadData(final Context ctx) {
...
while (cursor.moveToNext() && i < Constants.MAX_PRELOAD_PHOTO_NUMS) {
final String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
if (path != null && new File(path).exists()) {
Log.d("brycegao", "文件已存在:" + path);
}
sHandler.post(new Runnable() {
@Override public void run() {
Glide.with(ctx)
.load(new File(path))
.addListener(new RequestListener() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target target, boolean isFirstResource) {
return false;
}
@Override public boolean onResourceReady(Drawable resource, Object model,
Target target, DataSource dataSource, boolean isFirstResource) {
return false;
}
})
.preload(Constants.getScreenWidth(context), Constants.getScreenWidth(context));
}
});
i++;
}
...
}
在Activity的onCreate最开始启动线程加载数据, 注意:这里不会出现并发问题, 原因是子线程通过Handler执行UI线程的函数。 onCreate必须执行完成才可能响应子线程触发的回调;
protected void onCreate(Bundle savedInstanceState) {
//在子线程加载数据
initData();
super.onCreate(savedInstanceState);
...
}
重写RecyclerView并处理滑动事件, 逻辑是判断横向滑动时勾选经过的item, 纵向滑动时不做处理; 通过坐标(x,y)找到匹配的RecyclerView条目;
private boolean processTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
//记录点击屏幕时的初始坐标
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mDownX = x;
mDownY = y;
}
//抬起手指时重置
if (event.getAction() == MotionEvent.ACTION_UP
|| event.getAction() == MotionEvent.ACTION_CANCEL) {
mDownY = 0;
mDownX = 0;
mLastX = 0;
mLastY = 0;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
double distance = Math.sqrt(Math.abs(x - mLastX) * Math.abs(x - mLastX)
+ Math.abs(y - mLastY) * Math.abs(y - mLastY));
//如果是横向滑动且滑动距离超过阈值,则判断经过的item并勾选
if (distance > MIN_DISTANCE && Math.abs(x - mDownX) > Math.abs(y - mDownY)) {
mLastY = y;
mLastX = x;
doCheckSingleFinger(x, y);
}
}
return false;
}
控件:设置控件固定宽高, 减少测量时间;
四、总结
todo:手指滑动时取消勾选状态; 手指滑动时RecyclerView不动;
通过各种技术措施, 尽可能减少渲染时间; 目前最耗时的部分是Glide加载文件并绘制到控件;
监听DrawListener回调, 显示第一个图片(不是背景图,是目标图片)需要800ms左右(冷启动),UI体验看上去就是RecyclerView控件白了一下;
大概写了2天, 有一些收获。
欢迎技术交流~~~
https://github.com/brycegao/selectphoto