Introducción a la Nueva Activity (Actividad) Transiciones de Lollipop
Spanish (Español) translation by Javier Salesi (you can also view the original English article)
Introducción
Uno delos aspectos más interesantes de las especificaciones de Material Design es la continuación visual entre actividades. Con unas cuantas líneas de código, las nuevas APIs de Lolllipop te permiten aplicar transiciones significativamente entre dos actividades, gracias a las animaciones prolijas y continuas. Ésto rompe los límites tradicionales de las anteriores versiones de Android y permite al usuario entender como los elementos van de un punto a otro.
En éste tutorial, te mostraré como lograr éste resultado, haciendo una sencilla aplicación consistente con las pautas del Material Design de Google.
Requisitos Previos
En éste tutorial, asumiré que ya estás familiarizado con el desarrollo en Android y que utilizas Android Studio como tu IDE. Usaré extensivamente Android intents (intenciones), asumiendo un conocimiento básico del ciclo de vida de la actividad, y el nuevo widget RecyclerView
introducido con la API 21, el pasado mes de Junio. No voy a profundizar en los detalles de ésta clase, pero, si estás interesado, puedes encontrar una gran explicación en éste tutorial de Tuts+.
1. Crea la Primera Activity
La estructura básica de la aplicación es sencilla. Hay dos activities, una principal, MainActivity.java, cuya tarea es mostrar una lista de elementos, y una segunda, DetailActivity.java, que mostrará los detalles del elemento seleccionado en la lista previa.
Paso 1: El Widget RecyclerView
Para mostrar la lista de elementos, la activity principal usará el widget RecyclerView
introducido en Android Lollipop. Lo primero que necesitas hacer es, añadir la siguiente línea a la sección de dependencias en el archivo build.grade de tu proyecto para habilitar la retrocompatibilidad.
compile 'com.android.support:recyclerview-v7:+'
Paso 2: Definición de Datos
Por simpleza, no definiremos una base de datos real o una fuente de datos similar para la aplicación. En cambio, usaremos una clase personalizada, Contact
. Cada elemento tendrá un nombre, un color, información básica de contacto asociada a él. Así es como se ve la implementación de la clase Contact:
public class Contact { // The fields associated to the person private final String mName, mPhone, mEmail, mCity, mColor; Contact(String name, String color, String phone, String email, String city) { mName = name; mColor = color; mPhone = phone; mEmail = email; mCity = city; } // This method allows to get the item associated to a particular id, // uniquely generated by the method getId defined below public static Contact getItem(int id) { for (Contact item : CONTACTS) { if (item.getId() == id) { return item; } } return null; } // since mName and mPhone combined are surely unique, // we don't need to add another id field public int getId() { return mName.hashCode() + mPhone.hashCode(); } public static enum Field { NAME, COLOR, PHONE, EMAIL, CITY } public String get(Field f) { switch (f) { case COLOR: return mColor; case PHONE: return mPhone; case EMAIL: return mEmail; case CITY: return mCity; case NAME: default: return mName; } } }
Finalizarás con un agradable contenedor para la información que te interesa. Pero necesitamos llenarlo con algunos datos. En la parte superior de la clase Contact
, agrega el siguiente fragmento de código para llenar el conjunto de datos.
Al definir los datos como public
y static
, cada clase en el proyecto podrá leerlos. En un sentido, imitamos el comportamiento de una base de datos con la excepción que la estamos codificando en una clase.
public static final Contact[] CONTACTS = new Contact[] { new Contact("John", "#33b5e5", "+01 123456789", "john@example.com", "Venice"), new Contact("Valter", "#ffbb33", "+01 987654321", "valter@example.com", "Bologna"), new Contact("Eadwine", "#ff4444", "+01 123456789", "eadwin@example.com", "Verona"), new Contact("Teddy", "#99cc00", "+01 987654321", "teddy@example.com", "Rome"), new Contact("Ives", "#33b5e5", "+01 11235813", "ives@example.com", "Milan"), new Contact("Alajos", "#ffbb33", "+01 123456789", "alajos@example.com", "Bologna"), new Contact("Gianluca", "#ff4444", "+01 11235813", "me@gian.lu", "Padova"), new Contact("Fane", "#99cc00", "+01 987654321", "fane@example.com", "Venice"), };
Paso 3: Definiendo los Layouts Principales
El layot (maquetado) de la actividad principal es simple, porque la lista llenará toda la pantalla. El layout incluye un RelativeLayout
como la raíz-pero puede ser también un LinearLayout
-y un RecyclerView
como su único hijo.
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f5f5f5"> <android.support.v7.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/rv" /> </RelativeLayout>
Debido a que el widget RecyclerView
arregla subelementos y nada más, también necesitas diseñar el layout de un sólo elemento de la lista. Queremos tener un círculo de color a la izquierda de cada elemento de la lista de contactos así que primero tienes que definir el circle.xml dibujable.
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#000"/> <size android:width="32dp" android:height="32dp"/> </shape>
Ahora tienes los elementos necesarios para definir el layout del elemento de la lista.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="82dp" android:padding="@dimen/activity_horizontal_margin" android:background="?android:selectableItemBackground" android:clickable="true" android:focusable="true" android:orientation="vertical" > <View android:id="@+id/CONTACT_circle" android:layout_width="40dp" android:layout_height="40dp" android:background="@drawable/circle" android:layout_centerVertical="true" android:layout_alignParentLeft="true"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/CONTACT_circle" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:orientation="vertical"> <TextView android:id="@+id/CONTACT_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Jonh Doe" android:textColor="#000" android:textSize="18sp"/> <TextView android:id="@+id/CONTACT_phone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="+01 123456789" android:textColor="#9f9f9f" android:textSize="15sp"/> </LinearLayout> </RelativeLayout>
Paso 4: Mostrar los datos usando el RecyclerView
Casi hemos llegado al final de la primera parte del tutorial. Aún tienes que escribir el RecyclerView.ViewHolder
y el RecyclerView.Adapter
, y asignar todo a la view asociada en el método onCreate
de la actividad principal. En éste caso, el RecyclerView.ViewHolder
también debe poder manejar clicks así que necesitarás añadir una clase específica capaz de hacer eso. Comencemos definiendo la clase responsable para el manejo de los clicks.
public class RecyclerClickListener implements RecyclerView.OnItemTouchListener { private OnItemClickListener mListener; GestureDetector mGestureDetector; public interface OnItemClickListener { public void onItemClick(View view, int position); } public RecyclerClickListener(Context context, OnItemClickListener listener) { mListener = listener; mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { return true; } }); } @Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) { View childView = view.findChildViewUnder(e.getX(), e.getY()); if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) { mListener.onItemClick(childView, view.getChildPosition(childView)); return true; } return false; } @Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { } }
Es necesario especificar el RecyclerView.Adapter
, que llamaré DataManager
. Es responsable de cargar los datos e insertarlos en las views de la lista. Ésta clase de adminstrador de datos contendrá también la definición del RecyclerView.ViewHolder
.
public class DataManager extends RecyclerView.Adapter<DataManager.RecyclerViewHolder> { public static class RecyclerViewHolder extends RecyclerView.ViewHolder { TextView mName, mPhone; View mCircle; RecyclerViewHolder(View itemView) { super(itemView); mName = (TextView) itemView.findViewById(R.id.CONTACT_name); mPhone = (TextView) itemView.findViewById(R.id.CONTACT_phone); mCircle = itemView.findViewById(R.id.CONTACT_circle); } } @Override public RecyclerViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.contact_item, viewGroup, false); return new RecyclerViewHolder(v); } @Override public void onBindViewHolder(RecyclerViewHolder viewHolder, int i) { // get the single element from the main array final Contact contact = Contact.CONTACTS[i]; // Set the values viewHolder.mName.setText(contact.get(Contact.Field.NAME)); viewHolder.mPhone.setText(contact.get(Contact.Field.PHONE)); // Set the color of the shape GradientDrawable bgShape = (GradientDrawable) viewHolder.mCircle.getBackground(); bgShape.setColor(Color.parseColor(contact.get(Contact.Field.COLOR))); } @Override public int getItemCount() { return Contact.CONTACTS.length; } }
Finalmente, agregamos el siguiente código al método onCreate
, debajo setContentView
. La actividad principal está lista.
RecyclerView rv = (RecyclerView) findViewById(R.id.rv); // layout reference LinearLayoutManager llm = new LinearLayoutManager(this); rv.setLayoutManager(llm); rv.setHasFixedSize(true); // to improve performance rv.setAdapter(new DataManager()); // the data manager is assigner to the RV rv.addOnItemTouchListener( // and the click is handled new RecyclerClickListener(this, new RecyclerClickListener.OnItemClickListener() { @Override public void onItemClick(View view, int position) { // STUB: // The click on the item must be handled } }));
Así es como se verá la aplicación si la compilas y ejecutas.



2. Crea los Detalles de la Activity.
Paso 1: El layout
La segunda activity es mucho más sencilla. Toma el ID del contacto seleccionado y captura la información adicional que la primera activity no muestra.
Desde un punto de vista de diseño, el layout de ésta activity es crucial ya que es la parte más importante de la aplicación. Pero por lo que concierne a XML, es trivial. El layout es una serie de instancias de TextView
posicionadas de una forma placentera, usanod RelativeLayout
y LinearLayout
. Así es como se verá el layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:layout_width="match_parent" android:layout_height="200dp" android:scaleType="centerCrop" android:src="@mipmap/material_wallpaper"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="82dp" android:padding="@dimen/activity_vertical_margin"> <View android:id="@+id/DETAILS_circle" android:layout_width="48dp" android:layout_height="48dp" android:background="@drawable/circle" android:layout_centerVertical="true" android:layout_alignParentLeft="true"/> <TextView android:id="@+id/DETAILS_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Jonh Doe" android:layout_toRightOf="@+id/DETAILS_circle" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:layout_centerVertical="true" android:textColor="#000" android:textSize="25sp"/> </RelativeLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:padding="@dimen/activity_horizontal_margin" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/DETAILS_phone_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Phone:" android:textColor="#000" android:textSize="20sp"/> <TextView android:id="@+id/DETAILS_phone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@+id/DETAILS_phone_label" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:text="+01 123456789" android:textColor="#9f9f9f" android:textSize="20sp"/> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_vertical_margin"> <TextView android:id="@+id/DETAILS_email_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Email:" android:textColor="#000" android:textSize="20sp"/> <TextView android:id="@+id/DETAILS_email" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@+id/DETAILS_email_label" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:text="jonh.doe@example.com" android:textColor="#9f9f9f" android:textSize="20sp"/> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_vertical_margin"> <TextView android:id="@+id/DETAILS_city_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="City:" android:textColor="#000" android:textSize="20sp"/> <TextView android:id="@+id/DETAILS_city" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@+id/DETAILS_city_label" android:layout_marginLeft="@dimen/activity_horizontal_margin" android:text="Rome" android:textColor="#9f9f9f" android:textSize="20sp"/> </RelativeLayout> </LinearLayout> </LinearLayout>
Paso 2: Enviar y Recibir el ID vía Intent Extras
Ya que las dos activities están vinculadas por un intent, necesitas enviar algún fragmento de información que permita a la segunda activity entender de que contacto solicitaste los detalles.
Una opción puede ser usar la variable posición como una referencia. La posición del elemento en la lista corresponde a la posición del elemento en el arreglo así que no estará mal usar éste número como una única referencia.
Ésto funcionaría, pero si tomas este planteamiento y, por alguna razón, el conjunto de datos es modificado en el runtime, la referencia no coincidirá con el contacto en el que estás interesado. Ésta es la razón por la que es mejor usar un ID apropiado. Ésta información es el método getId
definido en la clase Contact
.
Edita el manejador onItemClick
de la lista de elementos como se muestra abajo.
@Override public void onItemClick(View view, int position) { Intent intent = new Intent(MainActivity.this, DetailsActivity.class); intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId()); startActivity(intent); }
La DetailsActivity
recibirá la información del Intent
extras y construirá el objeto correcto usando la ID como referencia. Ésto es mostrado en el siguiente bloque de código.
// Before the onCreate public final static String ID = "ID"; public Contact mContact;
// In the onCreate, after the setContentView method mContact = Contact.getItem(getIntent().getIntExtra(ID, 0));
Al igual que antes en el método onCreateViewHolder
del RecyclerView
, las views son inicializadas usando el método findViewById
y llenadas usando setText
. Por ejemplo, para configurar el campo del nombre hacemos lo siguiente:
mName = (TextView) findViewById(R.id.DETAILS_name); mName.setText(mContact.get(Contact.Field.NAME));
El proceso es el mismo para los otros campos. La segunda activity está finalmente lista.



3. Transiciones Significativas
Finalmente hemos llegado al punto central de éste tutorial, animar las dos activities usando el nuevo método de Lollipop para aplicar transiciones utilizando un elemento compartido.
Paso 1: Configura Tu Proyecto
Lo primero que necesitarás hacer es editar tu tema en el archivo style.xml en el directorio values-v21. De ésta forma, habilitas transiciones de contenido y estableces la entrada y salida de la views que no son compartidas entre las dos activities.
<style name="AppTheme" parent="AppTheme.Base"></style> <style name="AppTheme.Base" parent="android:Theme.Material.Light"> <item name="android:windowContentTransitions">true</item> <item name="android:windowEnterTransition">@android:transition/slide_bottom</item> <item name="android:windowExitTransition">@android:transition/slide_bottom</item> <item name="android:windowAllowEnterTransitionOverlap">true</item> <item name="android:windowAllowReturnTransitionOverlap">true</item> <item name="android:windowSharedElementEnterTransition">@android:transition/move</item> <item name="android:windowSharedElementExitTransition">@android:transition/move</item> </style>
Por favor ten en cuenta que tu proyecto debe estar apuntado a (y así ser compilado con) al menos Android API 21.
Las animaciones serán ignoradas en sistemas que no tienen instalado Lolllipop. Desafortunadamente, por razones de rendimiento, la librería AppCompat no proporciona completa retrocompatibilidad para éstas animaciones.
Paso 2: Asignar el Nombre de la Transición en los archivos Layout
Una vez que has editado tu archivo style.xml, tienes que definir la relación entre los dos elementos comunes de las views.
En nuestro ejemplo, las views compartidas son el campo que contiene el nombre del contacto, el del número de teléfono, y el círculo de color. Para cada uno, tienes que especificar un nombre común de transición. Por ésta razón, comienza a añadir en el archivo strings.xml los siguientes elementos:
<string name="transition_name_name">transition:NAME</string> <string name="transition_name_circle">transition:CIRCLE</string> <string name=“transition_name_phone”>transition:PHONE</string>
Entonces, para cada uno de los tres pares, en los archivos layout añade el atributo android:transitionName
con el valor correspondiente. Para el círculo de color, el código se verá así:
<!— In the single item layout: the item we are transitioning *from* —> <View android:id=“@+id/CONTACT_circle” android:transitionName=“@string/transition_name_circle” android:layout_width=“40dp” android:layout_height=“40dp” android:background=“@drawable/circle” android:layout_centerVertical=“true” android:layout_alignParentLeft=“true”/>
<!— In the details activity: the item we are transitioning *to* —> <View android:id=“@+id/DETAILS_circle” android:transitionName=“@string/transition_name_circle” android:layout_width=“48dp” android:layout_height=“48dp” android:background=“@drawable/circle” android:layout_centerVertical=“true” android:layout_alignParentLeft=“true”/>
Gracias a éste atributo, Android sabrá que views están compartidas entre las dos activities y animará correctamente la transición. Repite el mismo proceso para las otras dos views.
Paso 3: Configura el Intent
Desde un punto de vista de codificación, necesitarás adjuntar un específico paquete ActivityOptions
al intent. El método que necesitas es makeSceneTransitionAnimation
, que toma como parámetros el contexto de la aplicación y tantos elementos compartidos como necesitemos. En el método onItemClick
del RecyclerView
, edita el Intent
previamente definido así:
@Override public void onItemClick(View view, int position) { Intent intent = new Intent(MainActivity.this, DetailsActivity.class); intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId()); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation( // the context of the activity MainActivity.this, // For each shared element, add to this method a new Pair item, // which contains the reference of the view we are transitioning *from*, // and the value of the transitionName attribute new Pair<View, String>(view.findViewById(R.id.CONTACT_circle), getString(R.string.transition_name_circle)), new Pair<View, String>(view.findViewById(R.id.CONTACT_name), getString(R.string.transition_name_name)), new Pair<View, String>(view.findViewById(R.id.CONTACT_phone), getString(R.string.transition_name_phone)) ); ActivityCompat.startActivity(MainActivity.this, intent, options.toBundle()); }
Para cada elemento compartido a ser animado, tendrás que agregar al método makeSceneTransitionAnimation
un nuevo elemento Pair
. Cada Pair
tiene dos valores, el primero como una referencia a la view desde la que estás transicionando, la segunda es el valor del atributo transitionName
.
Ten cuidado cuando importes la clase Pair
. Necesitarás incluir el paquete android.support.v4.util
, no el paquete android.util
. También, recuerda usar el método ActivityCompat.startActivity
en lugar del método startActivity
, porque de otra manera no podrás ejecutar tu aplicación en entornos con API inferior a 16.
Eso es todo. Terminaste Así de simple.
Conclusión
En éste tutorial aprendiste como aplicar transiciones de manera fácil y atractiva entre dos activities que comparten uno o más elementos, permitiendo una continuidad significativa y placentera.
Comenzaste al crear la primera de las dos activities, cuyo papel es mostrar la lista de contactos. Luego completaste la segunda activity, diseñando su layout, e implementando una forma de pasar una única referencia entre las dos activities. Finalmente, viste la manera en la que funciona makeSceneTransitionAnimation
, gracias al atributo transitionName
en XML.
Consejo Extra: Aplicar Estilo a Detalles.
Para crear una verdadera aplicación Material Design, como se muestra en las capturas de pantalla anteriores, también necesitarás cambiar los colores de tu tema. Edita tu tema base en el directorio values-v21 para lograr un resultado atractivo.
<style name=“AppTheme” parent=“AppTheme.Base”> <item name=“android:windowTitleSize”>0dp</item> <item name=“android:colorPrimary”>@color/colorPrimary</item> <item name=“android:colorPrimaryDark”>@color/colorPrimaryDark</item> <item name=“android:colorAccent”>@color/colorAccent</item> <item name=“android:textColorPrimary”>#fff</item> <item name=“android:textColor”>#727272</item> <item name=“android:navigationBarColor”>#303F9F</item> </style>
¡Sé el primero en conocer las nuevas traducciones–sigue @tutsplus_es en Twitter!