Aprende a usar la biblioteca de Matisse en su nuevo proyecto de Android usando Glide Engine 4.

Cómo crear un selector de imágenes/ videos con vista previa usando la biblioteca Matisse para Android con Glide 4

Las aplicaciones que funcionan mucho con imágenes como Dropbox, WhatsApp, Instagram no usan selectores de archivos normales por razones obvias. Es por eso que existen selectores especiales que permiten al usuario ver una vista previa de una colección de imágenes almacenadas en el dispositivo. Normalmente, no implementaría todo esto usted mismo, ya que hay muchas cosas a considerar y esto puede llevar un tiempo precioso de desarrollo para su proyecto, por lo que una solución de terceros es la mejor manera de proceder, y eso es lo que Matisse ayudará. que hagas. Matisse es un selector de imágenes y videos locales bien diseñado para Android. Con esta biblioteca puede:

  • Úselo en actividad o fragmento
  • Seleccione imágenes que incluyan JPEG, PNG, GIF y videos que incluyan MPEG, MP4
  • Aplicar diferentes temas, incluidos dos temas integrados y temas personalizados
  • Diferentes cargadores de imágenes
  • Definir reglas de filtro personalizadas
  • Más para descubrirlo usted mismo

En este artículo, le mostraremos cómo instalar y usar la biblioteca Matisse en su proyecto de Android con Glide 4.

1. Configurar dependencias y biblioteca

Deberá incluir y actualizar todas las bibliotecas de soporte de Android para que coincidan con las compileSdkVersion de su proyecto. En este caso, estamos usando un nuevo proyecto de Android con el compileSdkVersionajuste 28, por lo que nuestras bibliotecas usarán la versión 28 de todas las bibliotecas de soporte de Android requeridas por Matisse. Incluya también el motor Glide y la biblioteca Matisse. Estamos usando la última versión de cada biblioteca hasta la fecha de este artículo:

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'
}

Guarda los cambios y sincroniza el proyecto. Después de la instalación de las bibliotecas, podremos continuar con el uso de esta biblioteca en su proyecto. Para obtener más información sobre Matisse, visite el repositorio oficial en Github aquí o para obtener más información sobre Glide, visite su repositorio en Github también .

2. Preparar cadenas de recursos

En su archivo app/src/res/values/strings.xml, agregue el siguiente recurso:

<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>

Cree también dentro del mismo directorio, el archivo  dimens.xmlcon el siguiente contenido:

<?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. Cree la clase GifSizeFilter

Durante la inicialización del selector, definiremos una instancia de GifSizeFilter como el filtro de imágenes para el selector, esta clase definirá qué tan grandes son las imágenes y el sistema operativo, así que proceda a crear una nueva clase en su aplicación, es decir,  GifSizeFilter.java con el siguiente contenido:

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. Cree el motor de imágenes Glide 4

El selector de imagen / video utiliza bajo el capó la biblioteca Glide, un marco de carga de imágenes y administración de medios de código abierto rápido y eficiente para Android que envuelve la decodificación de medios, el almacenamiento en caché de disco y memoria, y la agrupación de recursos en una interfaz simple y fácil de usar. Durante la inicialización del selector, definiremos una instancia del motor Glide 4 como el motor de imágenes preferido, así que proceda a crear una nueva clase en su aplicación, es decir,  Glide4Engine.javacon el siguiente contenido:

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. Crear diseño de aplicación

En esta aplicación, nuestro diseño será bastante simple, sin embargo, se basa en el diseño de restricción, por lo que puede cambiarlo como desee simplemente agregando un botón con la identificación button en el archivo activity_main.xml. Nuestro diseño se ve así:

<?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>

La idea principal es tener un botón que abra el selector de imágenes cuando se haga clic en él.

6. Ejemplo básico

La lógica requerida para que este selector funcione, es la siguiente: declarar 2 variables de clase accesibles que contendrán un código de identificación aleatorio para el resultado de la actividad, permisos, etc. y una variable de lista de Uri's que contendrá los archivos seleccionados por el usuario respectivamente. Tenemos un botón, por lo que agregaremos un oyente onClick que llamará estáticamente a una instancia del cuadro de diálogo de Matisse con opciones personalizadas (puede personalizarlo como desee), por ejemplo, permitiremos elegir todos los tipos de imágenes y establecer un cantidad máxima de 9 archivos para seleccionar. Como se mencionó anteriormente, nuestro ejemplo usará el motor Glide 4, por lo que deberá definir una nueva instancia y proporcionarla como parámetro del método imageEngine de Matisse.

Esto abrirá el cuadro de diálogo y el usuario podrá seleccionar las imágenes en el cuadro de diálogo. Una vez que el usuario hace clic en Aceptar, depende de usted cómo manejar los datos recibidos en la devolución de llamada onActivityResult de la actividad principal. En nuestro caso, solo mostraremos los datos en los registros:

Tenga en cuenta que el ejemplo no maneja permisos, necesitará hacerlo usted mismo. Verifique los permisos en el ejemplo completo.

// Código aleatorio que identifica el resultado del selector.
public static final int PICKER_REQUEST_CODE = 1;

// Lista que contendrá los archivos / videos seleccionados
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) {

            // Cuando se hace clic en el botón, abre el cuadro de diálogo de Matisse para elegir imágenes.
            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);

        // Mostrar en los registros los elementos seleccionados.
        // Produce algo como:
        // 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);
    }
}

Ejemplo completo

Deberá manejar los permisos de READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGEen su AndroidManifest.xmlarchivo:

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

Sin embargo, eso no será suficiente para Android 6+, ya que deberá solicitar los permisos en Runtime, el siguiente ejemplo describe una aplicación completamente funcional con una sola actividad, maneja los permisos y aplica todos los pasos mencionados en este artículo:

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;

    // Lista que contendrá los archivos / videos seleccionados
    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);
                }
            }
        });
    }

    /**
     * Método que muestra el selector de imagen / video.
     */
    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);
        }
    }

    /**
     * Método auxiliar que verifica si se otorgan o no los permisos de una matriz determinada.
     *
     * @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;
    }

    /**
     * Devolución de llamada que maneja el estado de la solicitud de permisos.
     *
     * @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;
            }
        }
    }
}

Que te diviertas ❤️!


Ingeniero de Software Senior en EPAM Anywhere. Interesado en la programación desde los 14 años, Carlos es un programador autodidacta, fundador y autor de la mayoría de los artículos de Our Code World.

Conviertete en un programador más sociable

Patrocinadores