Usar Transiciones En Android Con Material Design

Las transiciones en Android son animaciones que se manifiestan cuando las actividades son iniciadas o cerradas dentro de una aplicación.

Y es que estos efectos muestran en el Material Design un estilo visual tan poderoso que deleita a la vista.

Además aplican los conceptos de movimiento autentico, donde cada elemento proyecta sus condiciones físicas con tan solo verlo.

Pero… ¿Ya sabes usar este tipo de elementos visuales?

¿No?,

pues bien… en este artículo veremos de qué forma incorporar estas características. Estudiaremos distintos tipos de transiciones entre actividades, como explode, fade y slide.

También verás cómo compartir elementos en común de una actividad a otra, para que se ajusten sus dimensiones de un espacio a otro.

Descargar Proyecto En Android Studio De Cursos Online

Al final crearemos una aplicación que contiene Cursos Online como síntesis de este artículo.

Si deseas desbloquear el link de descarga del código Android, entonces sigue estas instrucciones:

1. Transiciones Entre Actividades

¿Qué es una transición?

Google dice:

“Las transiciones en aplicaciones con Material Design proveen conexiones visuales entre diferentes estados, a través del movimiento y transformación de elementos en común.”

Puedes mostrar transiciones al inicio o terminación (Enter-Exit) de una actividad. Donde los elementos de las actividades saldrán o entrarán con un patrón determinado.

Estados Enter-Exit En Transiciones Android

Dicho patrón puede ser definido por tí a través de la clase Transition. O puedes usar aquellos que ya están prefabricados por el framework de Android.

Aunque para nosotros a simple vista hay transiciones de entrada y salida, Android comprende otros dos estados llamados Retorno y Reingreso (Reenter – Return) cuando el back button es presionado.

Estados Reenter-Return En Transiciones Android

Por otro lado, dos actividades pueden compartir uno o más views que se mantengan a través de la transición.

Esta convención visual permite mantener una retroalimentación de las acciones del usuario. Lo que incrementa la experiencia de usuario y el significado de las acciones dentro de la app.

La siguiente aplicación muestra como la miniatura de la galería es redimensionada a medida que aparece la actividad de detalle.

Transición En Android Con View Compartido

Existen tres animaciones predefinidas que puedes usar en tus aplicaciones:

  • Explode: Expulsa o Atrae los views desde o hacia el centro de la actividad.
    Transición En Android Tipo Explode
  • Slide: Mueve los views en conjunto hacia algún borde de la actividad (superior, inferior, derecho, izquierdo, etc).
    Transición De Tipo Slide
  • Fade: Desvanece o resalta poco a poco los views dentro de la actividad.
    Transición Tipo Fade En Android

Crear Transiciones En Android

Antes de crear transiciones debes habilitarlas para que funcionen.

Una de las formas de hacerlo es crear un archivo values-v21/styles.xml y añadir el atributo android:windowContentTransitions con el valor de true como muestra el código de abajo.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="AppTheme" parent="android:Theme.Material">
        <!--Habilitar animaciones en las transiciones -->
        <item name="android:windowContentTransitions">true</item>

        <!-- Más definiciones... -->

    </style>
</resources>

Análogamente puedes activar las transiciones de forma programática con el método Window.requestFeature() a través de la bandera Window.FEATURE_CONTENT_TRANSITIONS.

// Dentro de tu actividad
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);

Las transiciones pueden ser definiciones xml almacenadas en la carpeta /res/transition. Dentro los archivos de transición podemos incluir un nodo del tipo <transitionSet> para combinar un conjunto de transiciones. O simplemente declarar una transición individual.

Por ejemplo…

El código siguiente declara una transición del tipo Explode con 2 segundos de duración.

explode_transition.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <explode
        android:duration="2000"/>
</transitionSet>

Así mismo puedes usar las clases Slide y Fade para representar los otros tipos de transición.

Luego asignas la animación en los estados correspondientes:

<!-- Estados Entrada-Salida  -->
<item name="android:windowEnterTransition">@transition/explode_transition</item>
<item name="android:windowExitTransition">@transition/explode_transition</item>

<!-- Estados Reingreso-Retorno  -->
<item name="android:windowReenterTransition">@transition/fade_transition</item>
<item name="android:windowReturnTransition">@transition/fade_transition</item>

Como ya lo supones, los elementos anteriores se refieren a cada una de las interacciones que vimos al inicio del artículo.

Si no seteas transiciones para el reingreso y retorno, el framework de android reutilizará las de entrada-salida, solo que las reproducirá desde el final hacia el inicio.

Para las transiciones con elementos compartidos es el mismo proceso. Usamos los atributos android:windowSharedElement*.

<!-- Estados entrada-salida -->
<item name="android:windowSharedElementEnterTransition">@transition/shared_explode</item>
<item name="android:windowSharedElementExitTransition">@transition/shared_explode</item>

<!-- Estados ingreso-retorno -->
<item name="android:windowSharedElementReenterTransition">@transition/shared_fade</item>
<item name="android:windowSharedElementReturnTransition">@transition/shared_fade</item>

Poner transiciones en el código: A través de los métodos set*Transition() que proporciona el componente Window puedes cumplir el mismo cometido anterior.

Por ejemplo…

Si quieres añadir una transición Explode para la salida de la actividad actual usa el método setExitTransition().

Explode explode = new Explode();
explode.setDuration(500); // Duración en milisegundos
getWindow().setExitTransition(explode);

Si deseas la entrada, entonces usa setEnterTransition(). Si deseas establecer el retorno, usa setReturnTransition() y así sucesivamente.

Ahora, para las transiciones compartidas, usa los métodos setSharedElement*Transition() de la misma forma que los anteriores.

Iniciar una Activity con transiciones: Como ya sabes, una actividad se inicia con el método startActivity(). No obstante debemos indicar el inicio de la transición con el resultado del método ActivityOptions.makeSceneTransitionAnimation() en el segundo parámetro.

Intent startIntent = new Intent(this, OtraActividad.class);
startActivity(startIntent,
              ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

Para terminar manualmente una actividad y que esta reproduzca su transición de salida usa el método Activity.finishAfterTransition() y no el método finish().

Superponer las transiciones: Por defecto las transiciones entre actividades se superponen. Esto quiere decir que en medio de la animación, las transiciones de entrada-salida o reingreso-retorno se combinan para producir un efecto de continuidad natural.

Usa el los atributos android:windowAllowEnterTransitionOverlap, android:windowAllowReturnTransitionOverlap con el valor de false, para que una transición no empiece si la otra no ha acabado de reproducirse.

<!-- Deshabilitar sobreposición entre animaciones-->
<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowAllowReturnTransitionOverlap">false</item>

Para transiciones de elementos compartidos también existe un atributo similar. Pero esta vez, cumple la función de superponer los views compartidos sobre todas las transiciones. Esto evita que no desaparezcan de la escena.

<!-- Habilitar superposición de los elementos compartidos entre transiciones -->
<item name="android:windowSharedElementsUseOverlay">true</item>

Usar transiciones definidas en xml desde el código: En ocasiones necesitarás usar dinámicamente una transición personalizada que se encuentra en un archivo xml.

La solución a este problema es usar la clase TransitionInflater. Este elemento es el encargado de transformar las definiciones de una transición en objetos java.

Veamos un ejemplo…

Transition boomExplode = TransitionInflater.from(this)
        .inflateTransition(R.transition.boom_explode);
window.setEnterTransition(boomExplode);

Obtén su instancia a través del método estático TransitionInflater.from() y luego infla el recurso xml con TransitionInflater.inflateTransition.

En seguida puedes setear el resultado en cualquier estado de las transiciones.

Usar Transiciones Con Elementos Compartidos

Ahora verás la lógica para compartir un elemento entre dos actividades de forma que varíe sus características y se ajuste a lo largo de la transición de forma coordinada.

Para ello haz lo siguiente:

  1. Marca el view en ambas actividades con el atributo android:transitionName. Este será un nombre único que mantendrá la relación del elemento.
  2. Setea una escucha OnClickListener al view que recibe el evento para activar la transición. El mejor caso, es que el view que reciba el click sea el que se compartirá. Por otro lado, puede que el elemento esté contenido dentro de otro view ó que sea el elemento de un ViewGroup como una lista.
  3. Inicia la transición con el método makeSceneTransitionAnimation() enviando como segundo parámetro el view compartido y la marca en el tercer parámetro.

Por ejemplo… el siguiente código obtiene un ImageView que al ser presionado es compartido entre dos actividades:

userImage = findViewById(R.id.user_avatar);
userImage.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent i = new Intent(MainActivity.this, DetailActivity.class);

        ActivityOptions options = ActivityOptions
                .makeSceneTransitionAnimation(MainActivity.this, v, v.getTransitionName());
        startActivity(i, options.toBundle());
    }
});

Como ves, se usa como segundo parámetro el view que ingresa en onClick(), ya que se refiere a sí mismo. Y el nombre de transición puede obtenerse a través del método getTransitionName().

¿Cómo empezar una actividad con múltiples views compartidos?

Luego de marcar cada view en su layout, define pares de elementos view-nombreDeTransición a través de la clase Pair  y setealos secuencialmente dentro de makeSceneTransitionAnimation().

ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
        Pair.create(view1, view1.getTransitionName()),
        Pair.create(view2, view2.getTransitionNane());

Puedes usar la cantidad de elementos que necesites, solo crea pares nuevos con Pair.create(). El primer parámetro será el view a compartir y el segundo su nombre de transición.

Manejar Eventos De Una Transición

Gestiona los eventos de una transición con la escucha OnTransitionListener. Esta clase proporciona controladores para dictaminar acciones en caso de que una transición inicie, termine, se cancele, etc.

Un buen ejemplo de utilización de la escucha OnTransitionListener se ve en el repositorio Material Animations. Donde luego de terminar una transición de elementos compartidos, se inicia un efecto de revelación cicular.

Ejemplo De Circular Reveal En Una Transición

 

Añade la escucha a una transición a través del método Transition.addListener() y luego crea una clase anónima para sobrescribir los controladores.

Fade fade = new Fade();
fade.addListener(
        new Transition.TransitionListener() {
            @Override
            public void onTransitionStart(Transition transition) {

            }

            @Override
            public void onTransitionEnd(Transition transition) {
                
            }

            @Override
            public void onTransitionCancel(Transition transition) {

            }

            @Override
            public void onTransitionPause(Transition transition) {

            }

            @Override
            public void onTransitionResume(Transition transition) {

            }
        }
);

Con ello tendrás la capacidad de dirigir el flujo de la transición y mejorar tus efectos visuales.

Ejemplo De Transiciones En Android

La aplicación a desarrollar como ejemplo se llama “Cursos Point”. Su objetivo es mostrar una lista de cursos online en una actividad principal, los cuales al ser presionados abrirán una actividad de detalle.

La idea es que al pasar entre actividades, se activen diferentes transiciones que te permitan conocer su comportamiento. Esto nos dará la oportunidad de aplicar los temas que vimos en el artículo.

¡Empezemos!

Paso 1. Abre Android Studio y crea un nuevo proyecto llamado “Cursos Point”. Cuando se te pregunte por qué tipo de actividad deseas agregar para comenzar, selecciona Blank Activity.

Usa el nombre de CourseActivity.java para la clase java y activity_course.xml para el layout.

Paso 2. Ve al archivo build.gradle y añade las dependencias del código de abajo.

build.gradle

dependencies {
    ...
    compile 'com.android.support:appcompat-v7:22.2.0'
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.android.support:design:22.2.0'
    compile 'com.github.bumptech.glide:glide:3.6.1'
    compile 'com.android.support:support-v4:22.2.0'
    compile 'com.android.support:cardview-v7:22.2.0'
}

Las librerías reflejan varios aspectos importantes de nuestro proyecto.

Paso 3. Crea el archivo /res/values/colors.xml para especificar la paleta de colores que usaremos en el estilo Material Design.

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="window_background">#EEEEEE</color>

    <color name="primaryDarkColor">#00796B</color>
    <color name="primaryColor">#009688</color>
    <color name="accentColor">#FFC107</color>
</resources>

Paso 4. Lo siguiente es modificar el archivo /res/values/styles.xml. Dentro de él, asigna los colores a un tema base llamado “CursosPoint.Base”. Luego aplica herencia de estilos para definir el tema de la aplicación.

<resources>

    <style name="CursosPoint" parent="CursosPoint.Base"/>
    <style name="CursosPoint.Base" parent="Theme.AppCompat.Light.NoActionBar">

        <item name="android:windowBackground">@color/window_background</item>

        <item name="colorPrimaryDark">@color/primaryDarkColor</item>
        <item name="colorPrimary">@color/primaryColor</item>
        <item name="colorAccent">@color/accentColor</item>
    </style>

</resources>

Este recurso aplicará para versiones anteriores de android.

Para las versiones mayores a 21, crea una nueva carpeta llamada /res/values-21. Luego crea un archivo styles.xml y habilita las transiciones.

values-21/styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="CursosPoint" parent="CursosPoint.Base">
        <!--Habilitar animaciones en las transiciones -->
        <item name="android:windowContentTransitions">true</item>

        <!-- Deshabilitar sobreposición entre animaciones-->
        <item name="android:windowAllowEnterTransitionOverlap">false</item>
        <item name="android:windowAllowReturnTransitionOverlap">false</item>


        <!-- Transiciones para entrada, salida y elementos compartidos...-->

        <!-- Habilitar superposición de los elementos compartidos entre transiciones -->
        <item name="android:windowSharedElementsUseOverlay">true</item>


    </style>
</resources>

Como ves, el estilo hereda del tema base para mantener los colores que declaramos, solo que esta vez añadimos las características de transiciones que vimos en los apartado anteriores.

Paso 5. Declara las siguientes dimensiones dentro del archivo /res/values/dimens.xml.

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>

    <dimen name="size_fab">56dp</dimen>
    <dimen name="fab_margin">16dp</dimen>

    <dimen name="card_margin">16dp</dimen>
</resources>

Paso 6. Descarga los drawables que usaremos dentro de la aplicación. En este caso usaremos imágenes para los elementos de la lista y para el detalle. La imagen será el elemento que compartamos entre las transiciones.

Crear Una Lista Con RecyclerView

Paso 7. Crea una nueva clase java con el nombre de Course.java. Este archivo representa la fuente de datos que usaremos como unidad de la lista. Dentro de esta clase añadiremos campos populares referentes a un curso online.

Course.java

/**
 * POJO para los cursos
 */
public class Course {
    private String name;
    private String description;
    private String author;
    private float rating;
    private int students;
    private float price;
    private int idImage;

    public Course(String name,
                  String description,
                  String author,
                  float rating,
                  int students,
                  float price,
                  int idImage) {
        this.name = name;
        this.description = description;
        this.author = author;
        this.rating = rating;
        this.students = students;
        this.price = price;
        this.idImage = idImage;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public String getAuthor() {
        return author;
    }

    public float getRating() {
        return rating;
    }

    public int getStudents() {
        return students;
    }

    public float getPrice() {
        return price;
    }

    public int getIdImage() {
        return idImage;
    }
}

Paso 8. Lo siguiente es crear datos de prueba para poblar la lista. Esto requiere que crees una nueva clase llamada Courses.java que represente el modelo de datos de la aplicación.

import java.util.Arrays;
import java.util.List;

/**
 * Creado por Hermosa Programación.
 */
public class Courses {
    private static Course[] courses = {
            new Course("Curso Online De Seo Para Principiantes",
                    "Aprende prácticas para optimizar tanto internos como externos de tu sitio " +
                            "web con el fin de recibir mas tráfico desde los motores de búsqueda\n" +
                            "\n" +
                            "Más de 30 clases \n" +
                            "12 horas de contenido", "Carlos Villa",
                    3f, 4340, 22, R.drawable.curso_online_seo),
            new Course("La ciencia del Marketing Online",
                    "Obtén este curso y aprende paso a paso como crear un negocio que genere" +
                            " ingresos constantes lo más pronto posible.\n" +
                            "\n" +
                            "20 excelentes clases\n" +
                            "\n" +
                            "Plantillas para inexpertos\n", "Elena Maiguel",
                    5f, 220, 24, R.drawable.curso_online_marketing),
            new Course("Publicidad Rápida y Furiosa",
                    "Con este curso obtendrás todos los secretos para generar campañas " +
                            "publicitarias que tus clientes no puedan resistirse.\n" +
                            "\n" +
                            "45 clases didácticas\n" +
                            "\n" +
                            "Desarrolla tu creatividad y asertividad comercial\n", "PricePart",
                    4.4f, 34235, 32, R.drawable.curso_online_publicidad),
            new Course("Aumentando el control de mis finanzas",
                    "Curso introductorio sobre finanzas personales. " +
                            "Aprende a gestionar tus recursos financieros a " +
                            "través de una estrategia de planificación sencilla y probada.\n" +
                            "\n" +
                            "¡Más de 13 clases y 20 horas de contenido!\n" +
                            "\n" +
                            "Satisfacción garantizada", "Academia Money",
                    3.4f, 11245, 35, R.drawable.curson_online_finanzas),
            new Course("Coaching Extremo",
                    "Aprende a conseguir resultados, alcanzar metas, cooperar " +
                            "con otras personas y a motivar su entorno.\n" +
                            "\n" +
                            "23 clases dividas en 10 horas \n" +
                            "Ejemplo prácticos", "Internaut Perri",
                    4.0f, 122, 45, R.drawable.curso_online_coaching),
            new Course("¿Cómo sacar máximo provecho a las redes sociales?",
                    "Aprende a gestionar y manejar eficazmente las comunidades " +
                            "sociales. Automatiza tareas, crea contenidos " +
                            "interesantes y saca el mejor provecho de las análiticas.\n" +
                            "\n" +
                            "Plantillas descargables para planificación.\n" +
                            "21 Infografías potentes para simplificar tu acción",
                    "Milo Alino", 3.2f, 2503, 34, R.drawable.curso_online_community_manager),
    };

    /**
     * Obtiene como lista todos los cursos de prueba
     *
     * @return Lista de cursos
     */
    public static List<Course> getCourses() {
        return Arrays.asList(courses);
    }

    /**
     * Obtiene un curso basado en la posición del array
     *
     * @param position Posición en el array
     * @return Curso seleccioando
     */
    public static Course getCourseByPosition(int position) {
        return courses[position];
    }

}

Paso 9. Ve a /res/layout y añade un nuevo layout llamado list_item.xml. El propósito de este archivo es proveer el diseño para los ítems de la lista.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foreground="?attr/selectableItemBackground"
    android:minHeight="?android:attr/listPreferredItemHeight">

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="16dp"
        android:layout_toRightOf="@+id/image"
        android:text="Nombre"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="@android:color/black"
        android:textSize="15sp" />

    <TextView
        android:id="@+id/author"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/name"
        android:layout_below="@+id/name"
        android:text="Autor" />

    <ImageView
        android:id="@+id/image"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/curso_online_seo"
        android:transitionName="shared_image" />

    <TextView
        android:id="@+id/price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:padding="16dp"
        android:text="Precio"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:textColor="?colorPrimary" />

    <TextView
        android:id="@+id/students"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/image"
        android:layout_toRightOf="@+id/image"
        android:padding="16dp"
        android:text="Estudiantes"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <RatingBar
        android:id="@+id/rating"
        style="?android:attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/author"
        android:layout_below="@+id/author"
        android:layout_toRightOf="@+id/image"
        android:isIndicator="true"
        android:paddingBottom="8dp"
        android:paddingTop="8dp"
        android:progressTint="#FDDB39"
        android:rating="3"
        android:secondaryProgressTint="#FDDB39" />

</RelativeLayout>

¿Has visto el atributo android:transitionName en la image view?

Usamos el identificador “shared_image” como marca.

Paso 10. Ahora es el turno de crear el RecyclerView.Adapter personalizado para inflar la lista.

CourseAdapter.java

import android.app.Activity;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;

import com.bumptech.glide.Glide;

import java.util.List;

/**
 * {@link android.support.v7.widget.RecyclerView.Adapter} para la lista de elementos
 */
public class CourseAdapter extends RecyclerView.Adapter<CourseAdapter.CourseViewHolder>
        implements ItemClickListener {
    private final Context context;
    private List<Course> items;


    public CourseAdapter(Context context, List<Course> items) {
        this.context = context;
        this.items = items;
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    @Override
    public CourseViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.list_item, viewGroup, false);
        return new CourseViewHolder(v, this);
    }

    @Override
    public void onBindViewHolder(CourseViewHolder viewHolder, int i) {
        // Item procesado actualmente
        Course currentItem = items.get(i);

        viewHolder.name.setText(currentItem.getName());
        viewHolder.author.setText(currentItem.getAuthor());
        viewHolder.price.setText("$" + currentItem.getPrice());
        viewHolder.rating.setRating(currentItem.getRating());
        viewHolder.students.setText(currentItem.getStudents() + " Estudiantes");
        // Cargar imagen
        Glide.with(context)
                .load(currentItem.getIdImage())
                .into(viewHolder.image);
    }

    @Override
    public void onItemClick(View view, int position) {
        // Imagen a compartir entre transiciones
        View sharedImage = view.findViewById(R.id.image);
        DetailActivity.launch(
                (Activity) context, position, sharedImage);
    }

    /**
     * View holder para reciclar elementos
     */
    public static class CourseViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
        // Views para un curso
        public final TextView name;
        public final TextView author;
        public final TextView price;
        public final RatingBar rating;
        public final TextView students;
        public final ImageView image;

        // Interfaz de comunicación
        public ItemClickListener listener;

        public CourseViewHolder(View v, ItemClickListener listener) {
            super(v);
            name = (TextView) v.findViewById(R.id.name);
            author = (TextView) v.findViewById(R.id.author);
            price = (TextView) v.findViewById(R.id.price);
            rating = (RatingBar) v.findViewById(R.id.rating);
            students = (TextView) v.findViewById(R.id.students);
            image = (ImageView) v.findViewById(R.id.image);
            v.setOnClickListener(this);

            this.listener = listener;
        }

        @Override
        public void onClick(View v) {
            listener.onItemClick(v, getAdapterPosition());
        }
    }
}

interface ItemClickListener {
    void onItemClick(View view, int position);
}

Algo importante a resaltar en el código anterior es la creación de una interfaz de comunicación entre el adaptador y el view holder llamada ItemClickListener.

Esta clase permite que se añadan escuchas OnClickListener a los elementos del view holder, de tal forma que se relacionen con la posición dentro del adaptador.

El adaptador debe implementar esta interfaz y sobrescribir el controlador onItemClick(). Si te fijas bien, dentro de este se llama al método estático DetailActivity.launch().

El tercer parámetro recibe la imagen que compartiremos entre actividades.

Crear Actividad De Detalle

Paso 11. Ve a File > New > Activity > Blank Activity para crear una nueva actividad. Ponle el nombre de DetailActivity.java y conviértela en hija de CourseActivity.java.

Paso 12. Revisa que el archivo AndroidManifest.xml tenga la dependencia entre ambas actividades como se ve en el siguiente código.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.herprogramacion.cursospoint" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/CursosPoint" >
        <activity
            android:name=".CourseActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".DetailActivity"
            android:label="@string/title_activity_detail"
            android:parentActivityName=".CourseActivity" >
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.herprogramacion.cursospoint.CourseActivity" />
        </activity>
    </application>

</manifest>

Paso 13. Modifica el layout de la actividad de detalle con el siguiente diseño:

activity_detail.xml

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/coordinator"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingBottom="@dimen/activity_vertical_margin"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin">


            <TextView
                android:id="@+id/detail_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Nombre"
                android:textAppearance="?android:attr/textAppearanceMedium" />

            <TextView
                android:id="@+id/detail_price"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Precio"
                android:textColor="?colorPrimary"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/tag_description"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingTop="16dp"
                android:text="DESCRIPCIÓN"
                android:textColor="?colorPrimary" />

            <TextView
                android:id="@+id/detail_description"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Descripción" />

            <RatingBar
                android:id="@+id/detail_rating"
                style="?android:attr/ratingBarStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:clickable="true"
                android:isIndicator="true"
                android:paddingTop="8dp"
                android:progressTint="#FDDB39"
                android:rating="3"
                android:secondaryProgressTint="#FDDB39" />

            <TextView
                android:id="@+id/detail_author"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Autor" />
        </LinearLayout>


    </android.support.v4.widget.NestedScrollView>
    <!-- App Bar -->
    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="256dp"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <!-- Toolbar -->
        <android.support.v7.widget.Toolbar xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
        <!-- Imagen del detalle -->
        <ImageView
            android:id="@+id/detail_image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:transitionName="shared_image" />


    </android.support.design.widget.AppBarLayout>
    <!-- FAB -->
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="@dimen/size_fab"
        android:layout_height="@dimen/size_fab"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_basket"
        app:borderWidth="0dp"
        app:elevation="@dimen/fab_elevation"
        app:layout_anchor="@id/app_bar"
        app:layout_anchorGravity="bottom|right|end" />


</android.support.design.widget.CoordinatorLayout>

El diseño requiere del uso de la App Bar para añadir la imagen de cabecera. Además debes recubrir los componentes con un CoordinatorLayout para permitir la ubicación del floating action button.

Importante… ¿viste la marca de la imagen de cabecera?

Tiene el mismo valor: "shared_image". Esto asegura que la imagen del item sea redimensionada hasta la posición de la imagen de cabecera.

Paso 14. Crea la carpeta /res/transition-v21 e incluye una nueva transición llamada detail_enter_transition.xml. Esta transición será inflada en la actividad de detalle para una de las muestras que usaremos.

detail_enter_transition.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:transitionOrdering="together">
    <slide android:slideEdge="right">
        <targets>
            <target android:targetId="@android:id/navigationBarBackground" />
        </targets>
    </slide>
    <slide android:slideEdge="left">
        <targets>
            <target android:targetId="@android:id/statusBarBackground" />
        </targets>
    </slide>
    <slide android:slideEdge="top">
        <targets>
            <target android:targetId="@+id/fab" />
        </targets>
    </slide>


</transitionSet>

Dentro de este archivo es posible definir como se comportarán los elementos de la interfaz. Para referirte a un elemento específico usa la etiqueta <targets> y dentro de ella usa <target>.

<target> tiene un atributo llamado android:targetId, que te permite referenciar por el indentificador al view que desees.

En nuestro caso, se aplica un slide a la barra de navegación. Con el atributo android:slideEdge="right" indicamos que sería a la derecha.

Luego usamos un slide hacia el borde izquierdo para la status bar.

Y por último un slide hacia el borde superior para el floating action button.

Transición Personalizada En Aplicación Android

Adicionalmente se indicó al elemento <transitionSet> una duración de 500ms (android:duration) y que las transiciones se reproduzcan al mismo tiempo(android:transitionOrdering="together").

Paso 15. Abre la actividad DetailActivity.java y añade la siguiente implementación.

import android.app.Activity;
import android.app.ActivityOptions;
import android.app.TaskStackBuilder;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.transition.Explode;
import android.transition.Fade;
import android.transition.Slide;
import android.transition.Transition;
import android.transition.TransitionInflater;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;

import com.bumptech.glide.Glide;

public class DetailActivity extends AppCompatActivity {
    private static final String EXTRA_POSITION = "com.herprogramacion.cursospoint.extra.POSITION";

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

        setToolbar(); // Reemplazar la action bar

        // Se obtiene la posición del item seleccionado
        int position = getIntent().getIntExtra(EXTRA_POSITION, -1);

        // Carga los datos en la vista
        setupViews(position);

        Window window = getWindow();

        // Elegir transiciones
        switch (position) {
            // EXPLODE
            case 0:
                Explode t0 = new Explode();
                window.setEnterTransition(t0);
                break;
            // SLIDE
            case 1:
                Slide t1 = new Slide();
                t1.setSlideEdge(Gravity.END);
                window.setEnterTransition(t1);
                break;
            // FADE
            case 2:
                Fade t2 = new Fade();
                window.setEnterTransition(t2);
                break;
            // PERSONALIZADA
            case 3:
                Transition t3 = TransitionInflater.from(this)
                        .inflateTransition(R.transition.detail_enter_trasition);
                window.setEnterTransition(t3);
                break;
            // EVENTOS DE TRANSICIÓN
            case 4:
                Fade t4 = new Fade();
                t4.addListener(
                        new Transition.TransitionListener() {
                            @Override
                            public void onTransitionStart(Transition transition) {

                            }

                            @Override
                            public void onTransitionEnd(Transition transition) {
                                Snackbar.make(
                                        findViewById(R.id.coordinator),
                                        "Terminó la transición",
                                        Snackbar.LENGTH_SHORT)
                                        .show();
                            }

                            @Override
                            public void onTransitionCancel(Transition transition) {

                            }

                            @Override
                            public void onTransitionPause(Transition transition) {

                            }

                            @Override
                            public void onTransitionResume(Transition transition) {

                            }
                        }
                );
                window.setEnterTransition(t4);
                break;
            // POR DEFECTO
            case 5:
                window.setEnterTransition(null);
                break;

        }
    }

    private void setupViews(int position) {
        TextView name = (TextView) findViewById(R.id.detail_name);
        TextView description = (TextView) findViewById(R.id.detail_description);
        TextView author = (TextView) findViewById(R.id.detail_author);
        TextView price = (TextView) findViewById(R.id.detail_price);
        RatingBar rating = (RatingBar) findViewById(R.id.detail_rating);
        ImageView image = (ImageView) findViewById(R.id.detail_image);

        Course detailCourse = Courses.getCourseByPosition(position);
        name.setText(detailCourse.getName());
        description.setText(detailCourse.getDescription());
        author.setText("Creado Por:" + detailCourse.getAuthor());
        price.setText("$" + detailCourse.getPrice());
        rating.setRating(detailCourse.getRating());
        Glide.with(this).load(detailCourse.getIdImage()).into(image);
    }

    private void setToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        if (getSupportActionBar() != null)// Habilitar Up Button
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            default:
                return super.onOptionsItemSelected(item);
            case android.R.id.home:
                // Obtener intent de la actividad padre
                Intent upIntent = NavUtils.getParentActivityIntent(this);
                upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

                // Comprobar si DetailActivity no se creó desde CourseActivity
                if (NavUtils.shouldUpRecreateTask(this, upIntent)
                        || this.isTaskRoot()) {

                    // Construir de nuevo la tarea para ligar ambas actividades
                    TaskStackBuilder.create(this)
                            .addNextIntentWithParentStack(upIntent)
                            .startActivities();
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    // Terminar con el método correspondiente para Android 5.x
                    this.finishAfterTransition();
                    return true;
                }

                // Dejar que el sistema maneje el comportamiento del up button
                return false;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        return super.onCreateOptionsMenu(menu);
    }

    public static void launch(Activity context, int position, View sharedView) {
        Intent intent = new Intent(context, DetailActivity.class);
        intent.putExtra(EXTRA_POSITION, position);

        // Los elementos 4, 5 y 6 usan elementos compartidos,
        if (position >= 3) {
            ActivityOptions options0 = ActivityOptions
                    .makeSceneTransitionAnimation(context, sharedView, sharedView.getTransitionName());
            context.startActivity(intent, options0.toBundle());
        } else {
            ActivityOptions options0 = ActivityOptions.makeSceneTransitionAnimation(context);
            context.startActivity(intent, options0.toBundle());
        }
    }
}

Veamos un poco de que van las piezas de código que tenemos.

  • setToolbar(): Reemplaza la action bar por la toolbar que tenemos definida en el layout. Además habilita el up button para la navegación hacia atrás.
  • setupViews(): Obtiene las instancias de los views del layout y luego setea los valores correspondientes según el curso obtenido con la posición que entró como parámetro.
  • launch(): Inicia una instancia prefabricada de DetailActivity. Recibe como parámetros la actividad desde donde se inicia, la posición del ítem de la lista y el view que se compartirá en la transición. Para propósitos educativos, solo los ítems 4, 5 y 6 podrán compartir la imagen

Superimportante resaltar que estamos usando un switch en onCreate(), para determinar que transiciones usaremos de acuerdo a la posición obtenida desde el valor extra del intent comunicado con launch().

El número de posición reproduce las siguientes variaciones:

  • Cero: Transición tipo Explode.
  • Uno: Transición tipo Slide con movimiento de izquierda a derecha (Gravitiy.END).
  • Dos: Transición tipo Fade.
  • Tres: Transición personalizada.
  • Cuatro: Uso de eventos de transición para desplegar una SnackBar al terminar la animación.
  • Cinco: Transición por defecto del sistema.

Paso 16. Abre el layout de la actividad principal para añadir la toolbar y el recycler view que la compondrán.

activity_course.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
        app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/reciclador"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />


</LinearLayout>

Paso 17. Finalmente modifica la actividad principal para que relacione el recycler view con un linear layout.

CourseActivity.java

import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.transition.Explode;
import android.view.Menu;
import android.view.MenuItem;

public class CourseActivity extends AppCompatActivity {
    public RecyclerView recyclerView;
    public LinearLayoutManager linearLayout;

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_course);

        setToolbar(); // Reemplazar toolbar

        setupRecyclerView(); // Preparar recycler view

        setupWindowAnimations(); // Añadir animaciones

    }

    private void setupRecyclerView() {
        linearLayout = new LinearLayoutManager(this);
        recyclerView = (RecyclerView) findViewById(R.id.reciclador);
        recyclerView.setLayoutManager(linearLayout);
        CourseAdapter adapter = new CourseAdapter(this, Courses.getCourses());
        recyclerView.setAdapter(adapter);
    }

    private void setToolbar() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    }

    private void setupWindowAnimations() {
        getWindow().setReenterTransition(new Explode());
        getWindow().setExitTransition(new Explode().setDuration(500));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_course, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Paso 18. Corre la aplicación en Android Studio y prueba cada curso online para ver su transición.

Actividad De Detalle En Android

Conclusión

Las transiciones son elementos visuales que comunican retroalimentación a los usuarios, con el fin de mantener congruencia e integridad entre las acciones que realizan dentro de la aplicación.

El Material Design ha establecido una gran cantidad de conceptos muy cercanos a la realidad física, dándonos otra perspectiva sobre lo que es la interfaz de un software.

Ya sabiendo cómo implementar transiciones, ahora puedes experimentar creando coreografías atractivas que potencialicen tus aplicaciones.

Te recomiendo visites el proyecto Material Up. Este sitio web contiene una gran cantidad de conceptos, aplicaciones, desarrollos e ideas sobre Material Design. Encontrarás mucha inspiración.

También puedes combinar las transiciones con los nuevos elementos de la librería de diseño, como lo son el Navigation View, el Tab Layout y la App Bar.

Lista de créditos para las imágenes que usamos en la aplicación. ¡Gracias Freepick!: