How to create an image/video picker with preview using the Matisse library for Android with Glide 4

How to create an image/video picker with preview using the Matisse library for Android with Glide 4

Applications that work a lot with images like Dropbox, WhatsApp, Instagram don't use normal file pickers for obvious reasons. That's why there are special pickers that allows the user to see a preview of a collection of images stored on the device. Normally, you wouldn't implement all this by yourself as there are a lot of things to consider and this may take precious time of development for your project, so a third party solution is the best way to proceed, and that's what Matisse will help you to do. Matisse is a well-designed local image and video selector for Android. With this library you can:

  • Use it in Activity or Fragment
  • Select images including JPEG, PNG, GIF and videos including MPEG, MP4
  • Apply different themes, including two built-in themes and custom themes
  • Different image loaders
  • Define custom filter rules
  • More to find out yourself

In this article, we will show you how to install and use the Matisse library in your Android project with Glide 4.

1. Configure dependencies and library

You will need to include and update all the support libraries of Android to match the compileSdkVersion of your project. In this case, we are using a new Android project with the compileSdkVersion set to 28, so our libraries will use the version 28 of all the android support libraries required by Matisse. Include as well the Glide engine and the Matisse library. We are using the latest version of every library till the date of this article:

dependencies {
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:animated-vector-drawable:28.0.0'
    implementation 'com.android.support:support-media-compat:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:support-v4:28.0.0'

    implementation 'com.github.bumptech.glide:glide:4.9.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
    implementation 'com.zhihu.android:matisse:0.5.2-beta3'
}

Save changes and synchronize the project. After the installation of the libraries we will be able to proceed with the usage of this library in your project. For more information about Matisse, please visit the official repository at Github here or for more information about Glide, visit it's repository at Github as well.

2. Prepare resources strings

In your app/src/res/values/strings.xml file append the following resource:

<resources>
    <string name="error_gif">x or y bound size should be at least %1$dpx and file size should be no more than %2$sM</string>
</resources>

Create as well inside the same directory , the dimens.xml file with the following content:

<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright 2017 Zhihu Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  -->
<resources>
    <dimen name="grid_expected_size">120dp</dimen>
    <dimen name="item_margin_horizontal">24dp</dimen>
    <dimen name="item_margin_vertical">8dp</dimen>
</resources>

3. Create GifSizeFilter class

During the initialization of the picker we will define an instance of the GifSizeFilter as the filter of images for the picker, this class will define how big the images are and os on, so proceed to create a new class in your app, namely GifSizeFilter.java with the following content:

package com.yourcompany.yourapp;

import android.content.Context;
import android.graphics.Point;

import com.zhihu.matisse.MimeType;
import com.zhihu.matisse.filter.Filter;
import com.zhihu.matisse.internal.entity.IncapableCause;
import com.zhihu.matisse.internal.entity.Item;
import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;

import java.util.HashSet;
import java.util.Set;

class GifSizeFilter extends Filter {

    private int mMinWidth;
    private int mMinHeight;
    private int mMaxSize;

    GifSizeFilter(int minWidth, int minHeight, int maxSizeInBytes) {
        mMinWidth = minWidth;
        mMinHeight = minHeight;
        mMaxSize = maxSizeInBytes;
    }

    @Override
    public Set<MimeType> constraintTypes() {
        return new HashSet<MimeType>() {{
            add(MimeType.GIF);
        }};
    }

    @Override
    public IncapableCause filter(Context context, Item item) {
        if (!needFiltering(context, item))
            return null;

        Point size = PhotoMetadataUtils.getBitmapBound(context.getContentResolver(), item.getContentUri());
        if (size.x < mMinWidth || size.y < mMinHeight || item.size > mMaxSize) {
            return new IncapableCause(
                    IncapableCause.DIALOG,
                    context.getString(R.string.error_gif, mMinWidth,
                    String.valueOf(PhotoMetadataUtils.getSizeInMB(mMaxSize))));
        }
        return null;
    }

}

4. Create Glide 4 Image Engine

The image/video picker uses under the hood the Glide library, a fast and efficient open source media management and image loading framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy to use interface. During the initialization of the picker we will define an instance of the Glide 4 engine as the preferred image engine, so proceed to create a new class in your app, namely Glide4Engine.java with the following content:

package com.yourcompany.yourapp;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.widget.ImageView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.request.RequestOptions;
import com.zhihu.matisse.engine.ImageEngine;

/**
 * {@link ImageEngine} implementation using Glide.
 */
public class Glide4Engine implements ImageEngine {

    @Override
    public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) {
        Glide.with(context)
                .asBitmap() // some .jpeg files are actually gif
                .load(uri)
                .apply(new RequestOptions()
                        .override(resize, resize)
                        .placeholder(placeholder)
                        .centerCrop())
                .into(imageView);
    }

    @Override
    public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView,
                                 Uri uri) {
        Glide.with(context)
            .asBitmap() // some .jpeg files are actually gif
            .load(uri)
            .apply(new RequestOptions()
                    .override(resize, resize)
                    .placeholder(placeholder)
                    .centerCrop())
            .into(imageView);
    }

    @Override
    public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
        Glide.with(context)
            .load(uri)
            .apply(new RequestOptions()
                    .override(resizeX, resizeY)
                    .priority(Priority.HIGH)
                    .fitCenter())
            .into(imageView);
    }

    @Override
    public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
        Glide.with(context)
            .asGif()
            .load(uri)
            .apply(new RequestOptions()
                    .override(resizeX, resizeY)
                    .priority(Priority.HIGH)
                    .fitCenter())
            .into(imageView);
    }

    @Override
    public boolean supportAnimatedGif() {
        return true;
    }
}

5. Creating application layout

In this application, our layout will be pretty simple, however it's based on the constraint layout, so you may change it as you want just adding a button with the id button on the activity_main.xml file. Our layout looks like this:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="49dp"
        android:text="Pick a file(s)"
        tools:layout_editor_absoluteX="16dp"
        tools:layout_editor_absoluteY="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
    />

</android.support.constraint.ConstraintLayout>

The main idea is to have a button that will open the image picker when it's clicked.

6. Basic example

The required logic to make this picker work, is the following one: declare 2 class accesible variables that will contain a random identification code for the activity result, permissions etc and a list variable of Uri's that will contain the selected files by the user respectively. We have a button, so we will add an onClick listener that will call statically an instance of the Matisse dialog with custom options (you can customize it as you want), for example we will allow to pick all the types of images and set a max ammount of 9 files to select. As mentioned previously, our example will use the Glide 4 Engine, so you will need to define a new instance and provide it as parameter of the imageEngine method of Matisse.

This will open the dialog and the user will be able to select the images in the dialog. Once the user clicks on Ok, it is up to you how to handle the received data on the onActivityResult callback of the main activity. In our case, we will just display the data on the logs:

Note that the example doesn't handle permissions, you will need to this by yourself. Check the permissions in the full example.

// Random code that identifies the result of the picker
public static final int PICKER_REQUEST_CODE = 1;

// List that will contain the selected files/videos
List<Uri> mSelected;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button button = findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {

            // When the button is clicked, open the Matisse dialog to pick images !
            Matisse.from(MainActivity.this)
                .choose(MimeType.ofAll())
                .countable(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.85f)
                .imageEngine(new Glide4Engine())
                .forResult(PICKER_REQUEST_CODE);
        }
    });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == PICKER_REQUEST_CODE && resultCode == RESULT_OK) {
        mSelected = Matisse.obtainResult(data);

        // Display in the logs the selected items.
        // Outputs something like:
        // D/Matisse: mSelected: [
        //    content://media/external/images/media/26263, 
        //    content://media/external/images/media/26264, 
        //    content://media/external/images/media/26261
        // ]
        Log.d("Matisse", "mSelected: " + mSelected);
    }
}

Full example

You will need to handle the permissions of READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE in your AndroidManifest.xml file:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

However that won't be enough for Android 6+ as you will need to request the permissions on Runtime, the following example describes a fully functional application with a single activity, handles the permissions and applies all the steps mentioned on this article:

package com.yourcompany.yourapp;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.zhihu.matisse.Matisse;
import com.zhihu.matisse.MimeType;
import com.zhihu.matisse.filter.Filter;

import java.util.List;

public class MainActivity extends AppCompatActivity {
    public static final int PICKER_REQUEST_CODE = 1;

    // List that will contain the selected files/videos
    List<Uri> mSelected;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                String[] PERMISSIONS = {
                    android.Manifest.permission.READ_EXTERNAL_STORAGE,
                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE
                };

                if(hasPermissions(MainActivity.this, PERMISSIONS)){
                    ShowPicker();
                }else{
                    ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS, PICKER_REQUEST_CODE);
                }
            }
        });
    }

    /**
     * Method that displays the image/video chooser.
     */
    public void ShowPicker()
    {
        Matisse.from(MainActivity.this)
            .choose(MimeType.ofAll())
            .countable(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.85f)
            .imageEngine(new Glide4Engine())
            .forResult(PICKER_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == PICKER_REQUEST_CODE && resultCode == RESULT_OK) {
            mSelected = Matisse.obtainResult(data);
            Log.d("Matisse", "mSelected: " + mSelected);
        }
    }

    /**
     * Helper method that verifies whether the permissions of a given array are granted or not.
     *
     * @param context
     * @param permissions
     * @return {Boolean}
     */
    public static boolean hasPermissions(Context context, String... permissions) {
        if (context != null && permissions != null) {
            for (String permission : permissions) {
                if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Callback that handles the status of the permissions request.
     *
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PICKER_REQUEST_CODE: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(
                            MainActivity.this,
                            "Permission granted! Please click on pick a file once again.",
                            Toast.LENGTH_SHORT
                    ).show();
                } else {
                    Toast.makeText(
                            MainActivity.this,
                            "Permission denied to read your External storage :(",
                            Toast.LENGTH_SHORT
                    ).show();
                }

                return;
            }
        }
    }
}

Happy coding !

This could interest you

Become a more social person