This article has been published exclusively by guolin_blog, an official wechat account

directory

  • Basic introduction
  • Overall design and implementation process
  • Load and display the resource folder
  • The realization of home page picture wall
  • Preview the interface implementation
  • conclusion

1. Basic introduction


Matisse is a very beautiful local image and video selection library from Zhihu.

Matisse code written quite concise, standard, there is a lot of learning value.

Talk about some of the advantages of Matisse:

  • You can easily call an Activity or Fragment

  • Supports loading of pictures and videos in various formats

  • Support for different styles, including two built-in themes and custom themes

  • You can customize file filtering rules

We can see that Matisse is very extensible, not only can we customize the themes we need, but also can filter out the files we want according to the requirements. In addition, Matisse adopts the Builder mode, which allows us to configure various properties through chain calls. Make our picture selection more flexible.

Second, the overall design and implementation process


Before introducing the Matisse workflow, let’s take a look at some of the more important classes to help us understand what’s going on

The name of the class function
Matisse External incoming activities or fragments are saved as weak references. At the same time, SelectionCreator is returned to configure various parameters through the from() method
SelectionCreator Through the Builder mode, chain configures the various properties we need
MatisseActivity Matisse home page Activity, showing pictures and videos

Let’s start with the use of Matisse and look at the workflow of Matisse.

From (mainactivity.this).choose(mimeType.allof ())true)
        .maxSelectable(9)
        .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size)) RestrictOrientation (ActivityInfo. SCREEN_ORIENTATION_UNSPECIFIED). ThumbnailScale (0.85 f) imageEngine (new GlideEngine ()) // 2, configure various parameters. ForResult (REQUEST_CODE_CHOOSE); // open MatisseActivityCopy the code

The usage code above, taking the Activity as an example, can be divided into three parts

  • Save the incoming Activity as a weak reference, and call Choose () to get the SelectionCreator

  • The various properties of SelectionCreator, such as the number of options, the size of thumbnails, and the engine to load the image, are configured through chain calls

  • Call startActivityForResult() with the Activity passed in from the first step and pass in the request code externally so that the List of selected images is returned at that time

The specific flow chart is as follows:

This is the workflow of Matisse, and the next step is a detailed analysis of the relevant classes. It is important to note that the source code in all of the classes I post below is not complete code, but “core code” stripped of code related to performance, compatibility, and extensibility.

Matisse

public final class Matisse {

    private final WeakReference<Activity> mContext;
    private final WeakReference<Fragment> mFragment;

    private Matisse(Activity activity, Fragment fragment) {
        mContext = new WeakReference<>(activity);
        mFragment = new WeakReference<>(fragment);
    }

    public static Matisse from(Activity activity) {
        return new Matisse(activity);
    }

    public static Matisse from(Fragment fragment) {
        returnnew Matisse(fragment); } public static List<Uri> obtainResult(Intent data) {return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
    }

    public SelectionCreator choose(Set<MimeType> mimeTypes, boolean mediaTypeExclusive) {
        returnnew SelectionCreator(this, mimeTypes, mediaTypeExclusive); }}Copy the code

The code for this class is very simple. It saves external incoming activities or fragments as weak references to prevent memory leaks. SelectionCreator is then returned via the Choose () method for subsequent parameter configuration. After the image selection is complete, we can retrieve the Uri list of our selected media from obtainResult() in onActivityResult() in our Fragment or Activity.

SelectionCreator

public final class SelectionCreator {
    private final Matisse mMatisse;
    private final SelectionSpec mSelectionSpec;

    SelectionCreator(Matisse matisse, @NonNull Set<MimeType> mimeTypes) {
        mMatisse = matisse;
        mSelectionSpec = SelectionSpec.getCleanInstance();
        mSelectionSpec.mimeTypeSet = mimeTypes;
    }

    public SelectionCreator theme(@StyleRes int themeId) {
        mSelectionSpec.themeId = themeId;
        return this;
    }

    public SelectionCreator maxSelectable(int maxSelectable) {
        mSelectionSpec.maxSelectable = maxSelectable;
        returnthis; } // The rest of the methods are similar to the above two, which are not pasted public voidforResult(int requestCode) {
        Activity activity = mMatisse.getActivity();
        Intent intent = new Intent(activity, MatisseActivity.class);
        Fragment fragment = mMatisse.getFragment();
        if(fragment ! = null) { fragment.startActivityForResult(intent, requestCode); }else{ activity.startActivityForResult(intent, requestCode); }}}Copy the code

As you can see inside SelectionCreator is an instance of Matisse, which is used to retrieve activities or fragments called externally, as well as an instance of SelectionSpec, which encapsulates the parameters common to image-loading classes. Makes SelectionCreator code much cleaner. SelectionCreator uses the Builder pattern internally, allowing us to make chain calls to configure various properties. Finally, forResult() jumps to MatisseActivity and returns the list of media URIs selected by the user to the corresponding Activity or Fragment via the requestCode passed in externally.

Load and display the resource folder


The resources shown in Matisse are loaded by Loader mechanism. Loader mechanism is officially recommended after Android 3.0 as the best way to load resources in ContentProvider, which can greatly improve the speed of resource loading. But it also makes our code more concise. If you are not familiar with the Loader mechanism, you can read this article first. The Android Loader mechanism makes your data loading more efficient

Attached is the flow chart of this operation:

The AlbumLoader, which inherits the Cursor, acts as a resource loader and loads resources by configuring parameters related to the resource. AlbumCollection implements the LoaderManager LoaderCallbacks interface, AlbumLoader as loaders, its internal defines AlbumCallbacks interface, upon the completion of the loading resources, Call back the Cursor containing the data to the externally called MatisseActivity, and then display the resource folder in the MatisseActivity.

AlbumsLoader

public class AlbumLoader extends CursorLoader {

    // content://media/external/file
    private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");

    private static final String[] COLUMNS = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id"."bucket_display_name",
            MediaStore.MediaColumns.DATA,
            COLUMN_COUNT};

    private static final String[] PROJECTION = {
            MediaStore.Files.FileColumns._ID,
            "bucket_id"."bucket_display_name",
            MediaStore.MediaColumns.DATA,
            "COUNT(*) AS " + COLUMN_COUNT};

    private static final String SELECTION =
            "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
                    + " OR "
                    + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?) "
                    + " AND " + MediaStore.MediaColumns.SIZE + "> 0"
                    + ") GROUP BY (bucket_id";

    private static final String[] SELECTION_ARGS = {
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
    };

    private static final String BUCKET_ORDER_BY = "datetaken DESC";

    private AlbumLoader(Context context, String selection, String[] selectionArgs) {
        super(context, QUERY_URI, PROJECTION, selection, selectionArgs, BUCKET_ORDER_BY);
    }

    public static CursorLoader newInstance(Context context) {
        return new AlbumLoader(context, SELECTION, SELECTION_ARGS);
    }

    @Override
    public Cursor loadInBackground() {
       returnsuper.loadInBackground(); }}Copy the code

Because Matisse only needs to fetch images and videos from the phone, you configure the necessary parameters directly in AlbumLoader and then provide newInstance() to an external call to fetch an instance of AlbumLoader.

AlbumCollection

public class AlbumCollection implements LoaderManager.LoaderCallbacks<Cursor> {
    private static final int LOADER_ID = 1;
    private static final String STATE_CURRENT_SELECTION = "state_current_selection";
    private WeakReference<Context> mContext;
    private LoaderManager mLoaderManager;
    private AlbumCallbacks mCallbacks;
    private int mCurrentSelection;

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Context context = mContext.get();
        return AlbumLoader.newInstance(context);
    }

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

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Context context = mContext.get();
        mCallbacks.onAlbumReset();
    }

    public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
        mContext = new WeakReference<Context>(activity);
        mLoaderManager = activity.getSupportLoaderManager();
        mCallbacks = callbacks;
    }

    public void loadAlbums() { mLoaderManager.initLoader(LOADER_ID, null, this); } public interface AlbumCallbacks { void onAlbumLoad(Cursor cursor); void onAlbumReset(); }}Copy the code

To decouple the code, Matisse encapsulates in an AlbumCollection some of the operations that clients interact with the LoaderManager. In onCreate(), the Activity is passed in to get the LoaderManager. After loading the resource, in the onLoadFinished() method, Returns “Cursor containing data” to the externally called MatisseActivity through the onAlbumLoad(Cursor Cursor) method of AlbumCallbacks.

AlbumsSpinner

The AlbumsSpinner wraps a set of controls in the upper-left corner of the MatisseActivity, including a TextView that displays the folder name and a ListPopupWindow that displays the list of folders, extracting a relatively complete set of functions. Write the logical operations inside and use it as a control in an Activity, similar to a custom View.

public class AlbumsSpinner {

    private static final int MAX_SHOWN_COUNT = 6;
    private CursorAdapter mAdapter;
    private TextView mSelected;
    private ListPopupWindow mListPopupWindow;
    private AdapterView.OnItemSelectedListener mOnItemSelectedListener;
Copy the code

The Cursor returned in the AlbumCollection as the data source for the AlbumsSpinner, which then displays the resource folder through the AlbumsAdapter. When the folder is selected, call back the position of the clicked folder to the onItemSelected() method in MatisseActivity.

@Override public void onItemSelected(AdapterView<? > parent, View view, int position, long id) { mAlbumCollection.setStateCurrentSelection(position); mAlbumsAdapter.getCursor().moveToPosition(position); Album = albu.valueof (malbumsAdapter.getCursor ()); onAlbumSelected(album); }Copy the code

Use the position called back from the AlbumsSpinner to get the information for the corresponding folder, and then refresh the current interface to display the image of the selected folder.

    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); // MediaSelectionFragment contains a RecyclerView, Used to display all images in the folder fragments fragments. = MediaSelectionFragment newInstance (album); getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName()) .commitAllowingStateLoss(); }}Copy the code

Fourth, the realization of the home page picture wall


The photo wall on the home page is arguably the most interesting module in Matisse, and the most valuable to learn. The data source of the picture wall is also loaded by the Loader mechanism, and the implementation idea is similar to the “loading and display of the resource folder” described in the previous section. It is good to briefly talk about it here.

The photo wall of the home page will display different pictures by selecting different resource folders. Therefore, when we select the resource folder, we will send the ID of the resource folder to the corresponding Loader to load the corresponding resource files.

Matisse encapsulates image and audio information into entity classes, and implements the Parcelable interface to serialize them. It obtains the Uri, media type, file size and, if it is a video, the playing time of the video through the external Cursor.

/** * public class Item implements Parcelable {public final long; public final String mimeType; public final Uri uri; public final long size; public final long duration; // onlyfor video, in ms

    private Item(long id, String mimeType, long size, long duration) {
        this.id = id;
        this.mimeType = mimeType;
        Uri contentUri;
        if (isImage()) {
            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        } else if (isVideo()) {
            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        } else{/ / if not pictures or audio directly when the file storage contentUri = MediaStore Files. GetContentUri ("external");
        }
        this.uri = ContentUris.withAppendedId(contentUri, id);
        this.size = size;
        this.duration = duration;
    }

    public static Item valueOf(Cursor cursor) {
        return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
                cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),
                cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)),
                cursor.getLong(cursor.getColumnIndex("duration"))); }}Copy the code

Picture wall is directly used a RecyclerView to display, Item is an inheritance of SquareFrameLayout (SquareFrameLayout) custom control, mainly contains three parts

  • CheckView in the upper right corner

  • The ImageView that displays the image

  • TextView that displays the length of the video

The CheckView is the small white circle in the upper right corner of the CheckBox. I said in the previous article that Matisse has high learning value, one of the important reasons is that there are many custom views in Matisse, which can let us learn some good ideas and practices of custom View while learning the image selection library.

Let’s take a look at how CheckView is implemented.

First, CheckView rewrites the onMeasure() method to set the width and height to 48, and for screen fit, multiplies 48dp by density, converting dp units to pixel units.

    private static final int SIZE = 48; // dp

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
        super.onMeasure(sizeSpec, sizeSpec);
    }
Copy the code

Next comes the onDraw() method, which is the highlight

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); InitShadowPaint (); canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint); // 2, draw a white empty circle canvas. DrawCircle ((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, STROKE_RADIUS * mDensity, mStrokePaint); // 3, Draw the contents of the circleif (mCountable) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
                        BG_RADIUS * mDensity, mBackgroundPaint);
                initTextPaint();
                String text = String.valueOf(mCheckedNum);
                int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2;
                int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2;
                canvas.drawText(text, baseX, baseY, mTextPaint);
        } else {
            if (mChecked) {
                initBackgroundPaint();
                canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, BG_RADIUS * mDensity, mBackgroundPaint); mCheckDrawable.setBounds(getCheckRect()); mCheckDrawable.draw(canvas); }}}Copy the code

The onDraw() method is divided into three main parts

  • I have to say that Matisse has done a really good job of detailing the inside and outside of the hollow circle by adding a shade of radiation gradient to make the image selection library look nice

  • Drawing white hollow circles is really not a big deal, right

  • If the value of mCountable is true, a background of theme colors and a number representing the number of images selected will be drawn internally. If mCount is false, draw the background and fill in a white ✓

This part is about Paint, and the math. If you’re not familiar with Paint, check out this article: Customize View 1-2 Paint tutorial, HenCoder tutorial, HenCoder tutorial, HenCoder tutorial.

MediaGrid is a custom control that inherits the SquareFrameLayout (SquareFrameLayout). Can be understood as an extension of the CheckView and display video duration (TextView) function of ImageView.

Let’s start with the use of MediaGrid in Adapter and take a closer look at the code implementation of MediaGrid

mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
        getImageResize(mediaViewHolder.mMediaGrid.getContext()),
        mPlaceholder,
        mSelectionSpec.countable,
        holder
        ));
       mediaViewHolder.mMediaGrid.bindMedia(item);
Copy the code

You can see that there are two main steps to using MediaGrid

  • Initialize the public properties of the image (mediagrid.prebindMedia (new Mediagrid.prebindinfo ()))

  • Bind the information corresponding to the image (mediagrid.bindMedia (Item))

PreBindInfo is a static inner class of MediaGrid that encapsulates the common attributes of some images

public static class PreBindInfo { int mResize; Drawable mPlaceholder; // ImageView placeholder Boolean mCheckViewCountable; // √ recyclerView. ViewHolder mViewHolder; // ViewHolder public PreBindInfo(int resize, Drawable placeholder, Boolean checkViewCountable, RecyclerView.ViewHolder viewHolder) { mResize = resize; mPlaceholder = placeholder; mCheckViewCountable = checkViewCountable; mViewHolder = viewHolder; }}Copy the code

Item, described above, is an entity class for pictures or audio. The second step is to pass an Item containing the image information to MediaGrid and set it accordingly.

MediaGrid provides a custom interface for callbacks

    public interface OnMediaGridClickListener {

        void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);

        void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
    }
Copy the code

When the user clicks on an image, the click event is called back to Adapter, then to MediaSelectionFragment, then to MatisseActivity, and then opens the large image preview screen of the image. You are right, it really calls back three layers. Once that happened, I found EventBus to be pretty handy.

When you click the CheckView in the upper right corner, the click event is called back to the Adapter, and the corresponding setting is performed according to the countable value (display the number or display √). The corresponding Item information is then stored in the SelectedItemCollection (container of items).

Five, the implementation of preview interface


There are two ways to open the preview screen

  • Click on an image on the home page

  • After selecting the image, click the Preview button in the lower left corner of the home page

These two methods may appear to open the same interface, but in fact their implementation logic is very different, so they use two different activities.

Clicking on an image on the home page leads to a screen that contains a ViewPager. Since there may be a lot of images in the resource folder, it is not practical to pass all the images in that folder directly to the Activity in the preview screen. A better way to achieve this is to upload the “Album containing the information of the corresponding folder” to the interface, and then use the Loader mechanism to load it.

After selecting the home page image, click the preview button in the lower left corner to jump to the preview interface. Since there are usually few images to select, it is ok to directly send the “List containing all selected images” to the preview interface.

Although the implementation logic of the two activities is different, they have much in common because they are preview screens. Therefore, Matisse implements a BasePreviewActivity to reduce the redundancy of the code.

The BasePreviewActivity layout consists of three main parts

  • CheckView in the upper right corner

  • Custom ViewPager

  • Bottom bar (including Preview and Apply)

The main code logic is basically built around these three parts.

When clicking CheckView, set the CheckView and update the bottom bar according to whether the image has been selected and the image type.

        mCheckView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Item item = mAdapter.getMediaItem(mPager.getCurrentItem()); // If the current image is already selectedif (mSelectedCollection.isSelected(item)) {
                    mSelectedCollection.remove(item);
                    if (mSpec.countable) {
                        mCheckView.setCheckedNum(CheckView.UNCHECKED);
                    } else {
                        mCheckView.setChecked(false); }}else{// Determine whether the image can be addedif (assertAddSelection(item)) {
                        mSelectedCollection.add(item);
                        if (mSpec.countable) {
                            mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
                        } else {
                            mCheckView.setChecked(true); }} // Update bottom bar updateApplyButton(); }});Copy the code

When the user slides the ViewPager left or right, the user will get the corresponding Item information according to the current position, and then set the corresponding CheckView and switch pictures.

    @Override
    public void onPageSelected(int position) {
        PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
        if(mPreviousPos ! = -1 && mPreviousPos ! = position) { ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView(); Item = adapter.getMediaItem(position);if (mSpec.countable) {
                int checkedNum = mSelectedCollection.checkedNumOf(item);
                mCheckView.setCheckedNum(checkedNum);
                if (checkedNum > 0) {
                    mCheckView.setEnabled(true);
                } else {
                    mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
                }
            } else {
                boolean checked = mSelectedCollection.isSelected(item);
                mCheckView.setChecked(checked);
                if (checked) {
                    mCheckView.setEnabled(true);
                } else{ mCheckView.setEnabled(! mSelectedCollection.maxSelectableReached()); } } updateSize(item); } mPreviousPos = position; }Copy the code

That’s the BasePreviewActivity implementation logic. The subclasses AlbumPreviewActivity (a preview interface for all images) and SelectedPreviewActivity (a preview interface for selected images) are simple. You can understand the source code.

conclusion


Matisse is probably the first open source project THAT I have fully chewed through, and was shocked at the number of interfaces implemented by MatisseActivity from the beginning. I gradually understood the code design and implementation ideas from each function point. After watching the whole project, I was deeply impressed by the architecture design and code quality of Matisse.

When reading a large open source project, which is completely new to you and usually involves a large amount of code, it’s easy to get bogged down in the details of the code when reading the source code. It will be easier and more productive to read if we start with the function points and analyze step by step how the function points are implemented and the logic of the subject.


Guess you like

  • Android a very simple, elegant diary APP
  • Android rolls up its sleeves and encapsulates dialog Fragments by itself
  • Teach you how to make a beautiful APP from scratch
  • Android can save you a lot of detours