美文网首页Android 开发收集的一些东西Android开发经验谈Android开发
知乎开源matisse精读:学习如何打造精美实用的图片选择器(二

知乎开源matisse精读:学习如何打造精美实用的图片选择器(二

作者: chiyidun | 来源:发表于2017-08-28 17:34 被阅读567次

    接着上一篇 知乎开源matisse精读:学习如何打造精美实用的图片选择器(一)
    继续~

    @Override
        public void onAlbumLoad(final Cursor cursor) {
            //更新相册列表
            mAlbumsAdapter.swapCursor(cursor);
            // select default album.
            Handler handler = new Handler(Looper.getMainLooper());
            handler.post(new Runnable() {
    
                @Override
                public void run() {
                    cursor.moveToPosition(mAlbumCollection.getCurrentSelection());
                    mAlbumsSpinner.setSelection(MatisseActivity.this,
                            mAlbumCollection.getCurrentSelection());
                    //将cursor携带的数据转化为Album对象
                    Album album = Album.valueOf(cursor);
                    if (album.isAll() && SelectionSpec.getInstance().capture) {
                        album.addCaptureCount();
                    }
                    onAlbumSelected(album);
                }
            });
        }
    

    相册查找完毕以后, 默认情况下首先展示全部相册的内容。mAlbumCollection.getCurrentSelection()返回的是当前选中的相册的position。在正常情况下当前position为0,指向的就是全部相册,这里也会有非正常情况,比如acticity被回收重建,position是回收前我们在

    public void onSaveInstanceState(Bundle outState) {
            outState.putInt(STATE_CURRENT_SELECTION, mCurrentSelection);
        }
    

    方法中保存的数值。 通过cursor.moveToPosition(mAlbumCollection.getCurrentSelection());获得当前position的cursor
    然后将cursor携带的数据转化为Album对象:Album album = Album.valueOf(cursor);扫一眼这个方法:

    public static Album valueOf(Cursor cursor) {
            return new Album(
                    cursor.getString(cursor.getColumnIndex("bucket_id")),
                    cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA)),
                    cursor.getString(cursor.getColumnIndex("bucket_display_name")),
                    cursor.getLong(cursor.getColumnIndex(AlbumLoader.COLUMN_COUNT)));
        }
    

    得到这个Album 对象后,正常情况下这个对象包含的是全部相册的信息,考虑到非正常情况,需要通过album.isAll()做一下判断。怎么判断的呢?通过id来判断,还记得之前全部相册的数据实际上是通过MatrixCursor自己构造出来的,媒体库里并不存在,ID也是我们自己造的,用的是Album.ALBUM_ID_ALL这个无意义数据进行占的位。这样直接判断一下就ok了:

    public boolean isAll() {
            return ALBUM_ID_ALL.equals(mId);
    }
    

    如果判断当前是全部相册,而且用户设置了可拍照,那么就需要在全部相册中展示一个拍照选项了,
    不过这里的album.addCaptureCount()个人感觉意义并不大,先略了~
    之后调用onAlbumSelected(album);

    private void onAlbumSelected(Album album) {
            if (album.isAll() && album.isEmpty()) {
                mContainer.setVisibility(View.GONE);
                mEmptyView.setVisibility(View.VISIBLE);
            } else {
                mContainer.setVisibility(View.VISIBLE);
                mEmptyView.setVisibility(View.GONE);
                Fragment fragment = MediaSelectionFragment.newInstance(album);
                getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
                        .commitAllowingStateLoss();
            }
        }
    

    新建了一个fragment 用于展示相册内的数据。进入这个fragment,关注onActivityCreated回调

     @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            Album album = getArguments().getParcelable(EXTRA_ALBUM);
    
            mAdapter = new AlbumMediaAdapter(getContext(),
                    mSelectionProvider.provideSelectedItemCollection(), mRecyclerView);
            //注册回调
            mAdapter.registerCheckStateListener(this);
            mAdapter.registerOnMediaClickListener(this);
            //如果可以确定每个item的高度是固定的,设置这个选项可以提高性能
            mRecyclerView.setHasFixedSize(true);
    
            int spanCount;
            SelectionSpec selectionSpec = SelectionSpec.getInstance();
            //如果设置了item宽度的具体数值则计算获得列表的列数,否则使用设置的列数
            if (selectionSpec.gridExpectedSize > 0) {
                spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize);
            } else {
                spanCount = selectionSpec.spanCount;
            }
            mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
    
            int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
            mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false));
            mRecyclerView.setAdapter(mAdapter);
            mAlbumMediaCollection.onCreate(getActivity(), this);
            mAlbumMediaCollection.load(album, selectionSpec.capture);
        }
    

    注册了回调监听,设置了将要展示的RecyclerView的若干选项,这里暂时不过多关注。只看最后两句

        mAlbumMediaCollection.onCreate(getActivity(), this);
        mAlbumMediaCollection.load(album, selectionSpec.capture);
    

    指引我们来到AlbumMediaCollection 这个类:

    public class AlbumMediaCollection implements LoaderManager.LoaderCallbacks<Cursor> {
        private static final int LOADER_ID = 2;
        private static final String ARGS_ALBUM = "args_album";
        private static final String ARGS_ENABLE_CAPTURE = "args_enable_capture";
        private WeakReference<Context> mContext;
        private LoaderManager mLoaderManager;
        private AlbumMediaCallbacks mCallbacks;
    
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            Context context = mContext.get();
            if (context == null) {
                return null;
            }
            //获得传入的Album
            Album album = args.getParcelable(ARGS_ALBUM);
            if (album == null) {
                return null;
            }
            //交由CursorLoader执行查询
            return AlbumMediaLoader.newInstance(context, album,
                    album.isAll() && args.getBoolean(ARGS_ENABLE_CAPTURE, false));
        }
    
        /**
         *
         * @param loader
         * @param data 
         */
        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            Context context = mContext.get();
            if (context == null) {
                return;
            }
    
            mCallbacks.onAlbumMediaLoad(data);
        }
    
        @Override
        public void onLoaderReset(Loader<Cursor> loader) {
            Context context = mContext.get();
            if (context == null) {
                return;
            }
    
            mCallbacks.onAlbumMediaReset();
        }
    
        public void onCreate(@NonNull FragmentActivity context, @NonNull AlbumMediaCallbacks callbacks) {
            mContext = new WeakReference<Context>(context);
            mLoaderManager = context.getSupportLoaderManager();
            mCallbacks = callbacks;
        }
    
        public void onDestroy() {
            mLoaderManager.destroyLoader(LOADER_ID);
            mCallbacks = null;
        }
    
        public void load(@Nullable Album target) {
            load(target, false);
        }
    
        public void load(@Nullable Album target, boolean enableCapture) {
            Bundle args = new Bundle();
            args.putParcelable(ARGS_ALBUM, target);
            args.putBoolean(ARGS_ENABLE_CAPTURE, enableCapture);
            mLoaderManager.initLoader(LOADER_ID, args, this); //将当前相册对象Album通过Bundle传入
        }
    
        public interface AlbumMediaCallbacks {
    
            void onAlbumMediaLoad(Cursor cursor);
    
            void onAlbumMediaReset();
        }
    }
    

    是不是熟悉的套路,不错又是LoaderManager+cursorloader查询资源。是不是有人会有疑问为啥又查啊?
    不是查询过相册了吗?嗯是啊~但是相册就是相册,只是通过group by将所有资源做了一个分组,用来展示相册列表。所以每个相册内的信息还是要再查一遍的。那就看看具体的查询过程吧

    public class AlbumMediaLoader extends CursorLoader {
        private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");
        private static final String[] PROJECTION = {
                MediaStore.Files.FileColumns._ID,
                MediaStore.MediaColumns.DISPLAY_NAME,
                MediaStore.MediaColumns.MIME_TYPE,
                MediaStore.MediaColumns.SIZE,
                "duration"};
    
        // === params for album ALL && showSingleMediaType: false ===
        // 全部相册 多种资源类型
        private static final String SELECTION_ALL =
                "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                        + " OR "
                        + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
                        + " AND " + MediaStore.MediaColumns.SIZE + ">0";
        private static final String[] SELECTION_ALL_ARGS = {
                String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
                String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
        };
        // ===========================================================
    
        // === params for album ALL && showSingleMediaType: true ===
        // 全部相册 一种资源类型
        private static final String SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE =
                MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                        + " AND " + MediaStore.MediaColumns.SIZE + ">0";
    
        private static String[] getSelectionArgsForSingleMediaType(int mediaType) {
            return new String[]{String.valueOf(mediaType)};
        }
        // =========================================================
    
        // === params for ordinary album && showSingleMediaType: false ===
        // 非全部相册 多种资源类型
        private static final String SELECTION_ALBUM =
                "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                        + " OR "
                        + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
                        + " AND "
                        + " bucket_id=?"
                        + " AND " + MediaStore.MediaColumns.SIZE + ">0";
    
        private static String[] getSelectionAlbumArgs(String albumId) {
            return new String[]{
                    String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
                    String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
                    albumId
            };
        }
        // ===============================================================
    
        // === params for ordinary album && showSingleMediaType: true ===
        // 非全部相册 一种资源类型
        private static final String SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE =
                MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                        + " AND "
                        + " bucket_id=?"
                        + " AND " + MediaStore.MediaColumns.SIZE + ">0";
    
        private static String[] getSelectionAlbumArgsForSingleMediaType(int mediaType, String albumId) {
            return new String[]{String.valueOf(mediaType), albumId};
        }
        // ===============================================================
        //根据日期倒序
        private static final String ORDER_BY = MediaStore.Images.Media.DATE_TAKEN + " DESC";
        //是否显示拍照选项
        private final boolean mEnableCapture;
    
        private AlbumMediaLoader(Context context, String selection, String[] selectionArgs, boolean capture) {
            //执行查询
            super(context, QUERY_URI, PROJECTION, selection, selectionArgs, ORDER_BY);
            mEnableCapture = capture;
        }
    
        public static CursorLoader newInstance(Context context, Album album, boolean capture) {
            String selection;
            String[] selectionArgs;
            boolean enableCapture;
            //1.如果是当前相册是"全部",是否显示拍照选项由用户设置决定;否则不显示拍照选项
            //2.根据用户设置选择是否查询所有资源(图片,视频),或者只查询一类资源
            if (album.isAll()) {
                if (SelectionSpec.getInstance().onlyShowImages()) {
                    selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE;
                    selectionArgs = getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
                } else if (SelectionSpec.getInstance().onlyShowVideos()) {
                    selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE;
                    selectionArgs = getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
                } else {
                    selection = SELECTION_ALL;
                    selectionArgs = SELECTION_ALL_ARGS;
                }
                enableCapture = capture;
            } else {
                if (SelectionSpec.getInstance().onlyShowImages()) {
                    selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE;
                    selectionArgs = getSelectionAlbumArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE,
                            album.getId());
                } else if (SelectionSpec.getInstance().onlyShowVideos()) {
                    selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE;
                    selectionArgs = getSelectionAlbumArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO,
                            album.getId());
                } else {
                    selection = SELECTION_ALBUM;
                    selectionArgs = getSelectionAlbumArgs(album.getId());
                }
                enableCapture = false;
            }
            return new AlbumMediaLoader(context, selection, selectionArgs, enableCapture);
        }
    
        @Override
        public Cursor loadInBackground() {
            Cursor result = super.loadInBackground();
            //如果不满足拍照条件或者设备不支持拍照直接返回结果,否则垂直拼接一条数据在最前面用于显示拍照
            if (!mEnableCapture || !MediaStoreCompat.hasCameraFeature(getContext()) ) {
                return result;
            }
            MatrixCursor dummy = new MatrixCursor(PROJECTION);
            dummy.addRow(new Object[]{Item.ITEM_ID_CAPTURE, Item.ITEM_DISPLAY_NAME_CAPTURE, "", 0, 0});
            return new MergeCursor(new Cursor[]{dummy, result});
        }
    
        @Override
        public void onContentChanged() {
            // FIXME a dirty way to fix loading multiple times
        }
    }
    
    1. 构造查询条件取决于两个因素:一个是是否要查询全部相册内的信息,查全部不需要传bucket_id, 查单独相册需要bucket_id;一个是是否查询多种类型的资源;这样就构造出了4种查询条件。

    2. 我们发现loadInBackground又被重写了~这次重写的目的是就是为了在全部相册的资源中拼接一个用于显示拍照的占位数据,所以addRow方法中添加的数据都是无意义的,仅仅就是占一个position。

    3. 这里还重写了onContentChanged(),给了一个空实现 (AlbumLoader也有,上篇没提到)。为什么这样呢?查看源码发现Cursorloader中注册了一个ContentObserver,每隔一段时间就会触发ContentObserver的onchange方法,调用onContentChanged()。
      看一下Loader类源码:

    public final class ForceLoadContentObserver extends ContentObserver {
            public ForceLoadContentObserver() {
                super(new Handler());
            }
    
            @Override
            public boolean deliverSelfNotifications() {
                return true;
            }
    
            @Override
            public void onChange(boolean selfChange) {
                onContentChanged();
            }
        }
    public void onContentChanged() {
            if (mStarted) {
                forceLoad();
            } else {
                // This loader has been stopped, so we don't want to load
                // new data right now...  but keep track of it changing to
                // refresh later if we start again.
                mContentChanged = true;
            }
        }
    public void forceLoad() {
            onForceLoad();
        }
    

    之后由AsyncTaskLoader实现onForceLoad方法。

     @Override
        protected void onForceLoad() {
            super.onForceLoad();
            cancelLoad();
            mTask = new LoadTask();
            if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask);
            executePendingTask();
        }
    

    到了这里也就意味着线程池开启,查询操作又会被触发。
    作者复写onContentChanged()其实就是为了屏蔽这个触发机制,频繁的触发查询虽然能随时响应最新的资源变化,但实际意义不大,同时带来性能的损失,得不偿失。

    查询相册内的资源完成同样来到了LoaderManager的onLoadFinished

    @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            Context context = mContext.get();
            if (context == null) {
                return;
            }
            mCallbacks.onAlbumMediaLoad(data);
        }
    

    回调回fragment当中更新recyclerview的适配器

        @Override
        public void onAlbumMediaLoad(Cursor cursor) {
            mAdapter.swapCursor(cursor);
        }
    

    不过recyclerview 并没有提供cursoradapter,那就手动notify一下吧

     public void swapCursor(Cursor newCursor) {
            if (newCursor == mCursor) {
                return;
            }
            if (newCursor != null) {
                mCursor = newCursor;
                mRowIDColumn = mCursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID);
                // notify the observers about the new cursor
                notifyDataSetChanged();
            } else {
                notifyItemRangeRemoved(0, getItemCount());
                mCursor = null;
                mRowIDColumn = -1;
            }
        }
    

    这样相册内的资源就呈现在我们眼前了。所有的进入matisse后的初始化的流程就到此为止了。

    简单总结

    构造者模式获取用户设置参数并存储→进入首页activity并查询相册资源,获得相册列表数据→创建fragment,并通过相册id查询相册内资源,列表展示(首次默认展现全部相册的内容)。

    交互

    此时我们还没有和matisse进行任何互动。接下来就是互动环节:

    ezgif.com-resize.gif

    可以看到选中项的在随意的添加和删除操作后保持着排序。关键就在于这个类SelectedItemCollection

    public class SelectedItemCollection {
    
        public static final String STATE_SELECTION = "state_selection";
        public static final String STATE_COLLECTION_TYPE = "state_collection_type";
        /**
         * Empty collection
         */
        public static final int COLLECTION_UNDEFINED = 0x00;
        /**
         * Collection only with images
         */
        public static final int COLLECTION_IMAGE = 0x01;
        /**
         * Collection only with videos
         */
        public static final int COLLECTION_VIDEO = 0x01 << 1;
        /**
         * Collection with images and videos.
         */
        public static final int COLLECTION_MIXED = COLLECTION_IMAGE | COLLECTION_VIDEO;
        private final Context mContext;
        private Set<Item> mItems;
        private int mCollectionType = COLLECTION_UNDEFINED;
    
        public SelectedItemCollection(Context context) {
            mContext = context;
        }
    
        public void onCreate(Bundle bundle) {
            if (bundle == null) {
                mItems = new LinkedHashSet<>();
            } else {
                List<Item> saved = bundle.getParcelableArrayList(STATE_SELECTION);
                mItems = new LinkedHashSet<>(saved);
                mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED);
            }
        }
    
        public void setDefaultSelection(List<Item> uris) {
            mItems.addAll(uris);
        }
    
        public void onSaveInstanceState(Bundle outState) {
            outState.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems));
            outState.putInt(STATE_COLLECTION_TYPE, mCollectionType);
        }
    
        public Bundle getDataWithBundle() {
            Bundle bundle = new Bundle();
            bundle.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems));
            bundle.putInt(STATE_COLLECTION_TYPE, mCollectionType);
            return bundle;
        }
    
        /**
         * 将资源对象添加到已选中集合
         * @param item
         * @return
         */
        public boolean add(Item item) {
            //判断选择资源是否类型冲突
            if (typeConflict(item)) {
                throw new IllegalArgumentException("Can't select images and videos at the same time.");
            }
            boolean added = mItems.add(item);
            //如果只选中了图片Item, mCollectionType设置为COLLECTION_IMAGE
            // 如果只选中了图片影音资源,mCollectionType设置为COLLECTION_IMAGE
            // 如果两种都选择了,mCollectionType设置为COLLECTION_MIXED
            if (added) {
                if (mCollectionType == COLLECTION_UNDEFINED) {
                    if (item.isImage()) {
                        mCollectionType = COLLECTION_IMAGE;
                    } else if (item.isVideo()) {
                        mCollectionType = COLLECTION_VIDEO;
                    }
                } else if (mCollectionType == COLLECTION_IMAGE) {
                    if (item.isVideo()) {
                        mCollectionType = COLLECTION_MIXED;
                    }
                } else if (mCollectionType == COLLECTION_VIDEO) {
                    if (item.isImage()) {
                        mCollectionType = COLLECTION_MIXED;
                    }
                }
            }
            return added;
        }
        /**
         * 将资源对象从已选中集合删除
         * @param item
         * @return
         */
        public boolean remove(Item item) {
            boolean removed = mItems.remove(item);
            if (removed) {
                if (mItems.size() == 0) {
                    mCollectionType = COLLECTION_UNDEFINED;
                } else {
                    //删除前mCollectionType == COLLECTION_MIXED,需要遍历删除后的集合确定mCollectionType
                    if (mCollectionType == COLLECTION_MIXED) {
                        refineCollectionType();
                    }
                }
            }
            return removed;
        }
    
        public void overwrite(ArrayList<Item> items, int collectionType) {
            if (items.size() == 0) {
                mCollectionType = COLLECTION_UNDEFINED;
            } else {
                mCollectionType = collectionType;
            }
            mItems.clear();
            mItems.addAll(items);
        }
    
    
        public List<Item> asList() {
            return new ArrayList<>(mItems);
        }
    
        public List<Uri> asListOfUri() {
            List<Uri> uris = new ArrayList<>();
            for (Item item : mItems) {
                uris.add(item.getContentUri());
            }
            return uris;
        }
    
        public List<String> asListOfString() {
            List<String> paths = new ArrayList<>();
            for (Item item : mItems) {
                paths.add(PathUtils.getPath(mContext, item.getContentUri()));
            }
            return paths;
        }
    
        public boolean isEmpty() {
            return mItems == null || mItems.isEmpty();
        }
    
        public boolean isSelected(Item item) {
            return mItems.contains(item);
        }
    
        public IncapableCause isAcceptable(Item item) {
            //检查是否超过最大设置数量
            if (maxSelectableReached()) {
                int maxSelectable = SelectionSpec.getInstance().maxSelectable;
                String cause;
    
                try {
                    cause = mContext.getResources().getQuantityString(
                            R.plurals.error_over_count,
                            maxSelectable,
                            maxSelectable
                    );
                } catch (Resources.NotFoundException e) {
                    cause = mContext.getString(
                            R.string.error_over_count,
                            maxSelectable
                    );
                }
    
                return new IncapableCause(cause);
            }
            //检查是否有选择类型冲突
            else if (typeConflict(item)) {
                return new IncapableCause(mContext.getString(R.string.error_type_conflict));
            }
    
            return PhotoMetadataUtils.isAcceptable(mContext, item);
        }
    
        public boolean maxSelectableReached() {
            return mItems.size() == SelectionSpec.getInstance().maxSelectable;
        }
    
        public int getCollectionType() {
            return mCollectionType;
        }
        //遍历已选中项,判断资源类型种类,设置mCollectionType
        private void refineCollectionType() {
            boolean hasImage = false;
            boolean hasVideo = false;
            for (Item i : mItems) {
                if (i.isImage() && !hasImage) hasImage = true;
                if (i.isVideo() && !hasVideo) hasVideo = true;
            }
            if (hasImage && hasVideo) {
                mCollectionType = COLLECTION_MIXED;
            } else if (hasImage) {
                mCollectionType = COLLECTION_IMAGE;
            } else if (hasVideo) {
                mCollectionType = COLLECTION_VIDEO;
            }
        }
    
        /**
         * Determine whether there will be conflict media types. A user can only select images and videos at the same time
         * while {@link SelectionSpec#mediaTypeExclusive} is set to false.
         */
        public boolean typeConflict(Item item) {
            return SelectionSpec.getInstance().mediaTypeExclusive
                    && ((item.isImage() && (mCollectionType == COLLECTION_VIDEO || mCollectionType == COLLECTION_MIXED))
                    || (item.isVideo() && (mCollectionType == COLLECTION_IMAGE || mCollectionType == COLLECTION_MIXED)));
        }
    
        public int count() {
            return mItems.size();
        }
        //返回item第一次出现的索引,即选中item的数字
        public int checkedNumOf(Item item) {
            int index = new ArrayList<>(mItems).indexOf(item);
            return index == -1 ? CheckView.UNCHECKED : index + 1;
        }
    }
    

    仔细分析一下这个类的一些方法:

    1.oncreate():

    上一篇提到过,新建选中项集合,如果存在就从bundle中获取。这里使用了LinkedHashSet,特点是有序,而且因为要频繁添加和删除集合中的元素,LinkedHashSet的效率比Arraylist要高。

    2.isAcceptable():

    作用是向集合添加item之前的各种校验工作。

    1. 首先maxSelectableReached()判断是否添加的资源达到了用户设置的最大数量,如果超过了这个数量会返回IncapableCause类提示信息,方法结束。
    2. 如果没有达到最大数量限制,再去检查item是否存在选择类型冲突:typeConflict(item)方法中,SelectionSpec.getInstance().mediaTypeExclusive是用户使用choose方法的第二个参数设置的可以参看上一篇中的用法,作用是是否可以同时选中不同类型的资源(图片,视频),true表示不可以,false表示可以(有点绕~)。如果这个参数是false,则方法直接返回false,不会发生资源类型冲突。如果为true,判断一下mCollectionType 当前选中项集合的类型和要添加的item的类型是否一致,不一致就返回true,通过IncapableCause类提示信息。mCollectionType 有四种状态 COLLECTION_UNDEFINED(未定义),COLLECTION_IMAGE(图片),COLLECTION_VIDEO(视频),COLLECTION_MIXED(图片加视频)。
    3. PhotoMetadataUtils.isAcceptable方法:
    public static IncapableCause isAcceptable(Context context, Item item) {
            //判断资源类型是否已设置可选
            if (!isSelectableType(context, item)) {
                return new IncapableCause(context.getString(R.string.error_file_type));
            }
            //过滤不符合用户设定的资源 Filter提供抽象方法,由用户自行设置过滤规则
            if (SelectionSpec.getInstance().filters != null) {
                for (Filter filter : SelectionSpec.getInstance().filters) {
                    IncapableCause incapableCause = filter.filter(context, item);
                    if (incapableCause != null) {
                        return incapableCause;
                    }
                }
            }
            return null;
        }
    

    校验1:调用isSelectableType():

     private static boolean isSelectableType(Context context, Item item) {
            if (context == null) {
                return false;
            }
    
            ContentResolver resolver = context.getContentResolver();
            for (MimeType type : SelectionSpec.getInstance().mimeTypeSet) {
                if (type.checkType(resolver, item.getContentUri())) {
                    return true;
                }
            }
            return false;
        }
    

    调用checkType():

    public boolean checkType(ContentResolver resolver, Uri uri) {
            MimeTypeMap map = MimeTypeMap.getSingleton();
            if (uri == null) {
                return false;
            }
            String type = map.getExtensionFromMimeType(resolver.getType(uri));
            for (String extension : mExtensions) {
                if (extension.equals(type)) {
                    return true;
                }
                String path = PhotoMetadataUtils.getPath(resolver, uri);
                if (path != null && path.toLowerCase(Locale.US).endsWith(extension)) {
                    return true;
                }
            }
            return false;
        }
    

    这一层是用来判断待添加的资源在不在最初设置的枚举类型集合内。具体判断方法为:1通过ContentResolver .getType(uri))获得资源uri的minetype类型。2.直接通过uri获取资源绝对路径,判断后缀名。

    校验2: 过滤校验
    看一下Filter类

    public abstract class Filter {
        /**
         * Convenient constant for a minimum value.
         */
        public static final int MIN = 0;
        /**
         * Convenient constant for a maximum value.
         */
        public static final int MAX = Integer.MAX_VALUE;
        /**
         * Convenient constant for 1024.
         */
        public static final int K = 1024;
    
        /**
         * Against what mime types this filter applies.
         */
        protected abstract Set<MimeType> constraintTypes();
    
        /**
         * Invoked for filtering each item.
         *
         * @return null if selectable, {@link IncapableCause} if not selectable.
         */
        public abstract IncapableCause filter(Context context, Item item);
    
        /**
         * Whether an {@link Item} need filtering.
         */
        protected boolean needFiltering(Context context, Item item) {
            for (MimeType type : constraintTypes()) {
                if (type.checkType(context.getContentResolver(), item.getContentUri())) {
                    return true;
                }
            }
            return false;
        }
    }
    

    Filter类提供抽象方法,用户可以通过继承Filter类,实现filter方法,设置过滤规则,返回incapableCause 。
    当获取incapableCause 不为null时,即表示当前item被用户过滤,也就不能加入集合当中。

    3.add():

    作用是添加item到选中项集合。首先再次通过typeConflict(item)判断一下是否存在资源类型冲突情况。如果还有冲突,忍无可忍直接抛出异常信息。检查通过后,将item加入集合,判断当前集合的类型,同时设置mCollectionType 。

    4.remove ():

    作用是集合中删除item,集合为空时mCollectionType = COLLECTION_UNDEFINED,当mCollectionType == COLLECTION_MIXED时需要判断一下,删除后的mCollectionType ,判断方法refineCollectionType():如果集合中仍然有不同的类型,mCollectionType =COLLECTION_MIXED,如果只有一种类型mCollectionType = COLLECTION_IMAGE或者COLLECTION_VIDEO。

    5.checkedNumOf
    
    public int checkedNumOf(Item item) {
            int index = new ArrayList<>(mItems).indexOf(item);
            return index == -1 ? CheckView.UNCHECKED : index + 1;
        }
    

    ArrayList.indexOf方法获得item第一次出现的索引,返回值CheckView.UNCHECKED 表示集合中没有当前项,返回值index + 1为选中item的向用户的序列号。

    这个类的主要方法介绍完毕,看一下具体使用:
    先看一下首页AlbumMediaAdapter中的使用,找到每个item右上角的checkview的点击事件,代码如下

    @Override
        public void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder) {
            //是否需要显示数量
            if (mSelectionSpec.countable) {
                //获取当前item在选中项中的索引,如果没有索引即没有选中,就添加当前item到选中项集合,如果有就从集合中删除。
                int checkedNum = mSelectedCollection.checkedNumOf(item);
                if (checkedNum == CheckView.UNCHECKED) {
                    //验证当前item是否满足可以被选中的条件
                    if (assertAddSelection(holder.itemView.getContext(), item)) {
                        mSelectedCollection.add(item);
                        notifyCheckStateChanged();
                    }
                } else {
                    mSelectedCollection.remove(item);
                    notifyCheckStateChanged();
                }
            } else {
                if (mSelectedCollection.isSelected(item)) {
                    mSelectedCollection.remove(item);
                    notifyCheckStateChanged();
                } else {
                    if (assertAddSelection(holder.itemView.getContext(), item)) {
                        mSelectedCollection.add(item);
                        notifyCheckStateChanged();
                    }
                }
            }
        }
    
    
      private boolean assertAddSelection(Context context, Item item) {
            IncapableCause cause = mSelectedCollection.isAcceptable(item);
            IncapableCause.handleCause(context, cause);
            return cause == null;
        }
    

    比较容易理解了吧~先判断一下是否显示序列号,由用户设置而定。如果需要显示就判断当前item是否在集合中,如果不在的话就先做校验,满足条件就加入集合,如果在的话就从集合中删除。

    再看一下预览页面的中使用:

    mCheckView.setOnClickListener(new View.OnClickListener() {
    
                @Override
                public void onClick(View v) {
                    Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
                    if (mSelectedCollection.isSelected(item)) {
                        mSelectedCollection.remove(item);
                        if (mSpec.countable) {
                            mCheckView.setCheckedNum(CheckView.UNCHECKED);
                        } else {
                            mCheckView.setChecked(false);
                        }
                    } else {
                        if (assertAddSelection(item)) {
                            mSelectedCollection.add(item);
                            if (mSpec.countable) {
                                mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
                            } else {
                                mCheckView.setChecked(true);
                            }
                        }
                    }
                    updateApplyButton();
                }
            });
    

    流程基本是一样的。
    由此可见,SelectedItemCollection 类维护了一个选中项集合,允许在多个地方对集合元素添加和删除。这样只要获取到这个选中项集合,就轻而易举的可以实现选中项序列数字的UI展示。

    就先到这里吧~

    相关文章

      网友评论

        本文标题:知乎开源matisse精读:学习如何打造精美实用的图片选择器(二

        本文链接:https://www.haomeiwen.com/subject/xfhedxtx.html