ByPhoto-秒开的安卓图片选择库

一、背景
ByPhoto是个安卓图片选择库, 在启动渲染速度上做了很多优化; 荣耀8真机测试,图库里有3000多张图片。 冷启动图片选择页渲染完成需800ms左右, 热启动(即第二次打开Activity)渲染需要300ms。 真正实现了秒开的用户体验。

设计背景.png
结果页.png

选中页.png

二、需求
1、支持图片预加载, 即将图库的前几张图片加载到内存中; 使用了Glide的preload;
2、数据库分段回调, 即图片有几千张图片时, 每查询一定数量时(例如10条)就通知UI补充数据; RecyclerView不会刷新屏幕外的图片,只是缓存了文件路径; PS:这里还可以再优化一下,例如列表向下滑动时预加载后半段数据; 但考虑到字符串占用内存不大,几兆的样子,暂未实现;
3、数据结构, 使用适当的数据结构Map、Set降低读写时间复杂度;
4、支持手指在屏幕滑动时自动勾选经过的图片;
5、勾选图片时只刷新选中图标, 不刷新item图片;避免纵向滑动时刷新闪烁的问题;
6、支持设置单行图片数量和最多选中数量;


改造点.png

三、核心代码


类图.png

在子线程查询数据库,并分批通知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

你可能感兴趣的:(ByPhoto-秒开的安卓图片选择库)