«

»

mar 19 2015

Un ‘stack’ productivo para el desarrollador android, #3, compatibilidad

Esta es la tercera entrega se la serie: “Un ‘stack’ productivo para el desarrollador android”.

En la primera entrega tratamos de definir una arquitectura modular y escalable, basada en el patrón Model View Presenter (MVP).

La segunda entrega trató de como dar pequeños mordiscos de Material Design a la aplicación, pasando por colores, transiciones, vectores, etc.

En este tercera entrega trataremos la compatibilidad, es sabido que la fragmentación de android es enorme, pasando por versiones, tamaños de pantalla, características, etc. Por eso bajaremos unas pocas versiones de la versión mínima inicial del proyecto (LLolipop) y soportaremos diferentes tipos de pantallas.

 

Bajando niveles de SDK

Me he decantado como versión mínima el nivel 16 del SDK de android, según los Dashboards publicados por Google hasta la fecha, la cifra de dispositivos que soportan versiones a partir de Jelly Bean es de un apetecible 86,8 %.

Screen Shot 2015-03-09 at 14.48.56

Para dar compatibilidad a estas versiones, son necesarios unos pequeños cambios, es el caso de las transiciones con elementos compartidos que no fueron introducidas hasta la versión 21 del framework.

Transiciones con elementos compartidos

Cuando se presiona una película en la actividad MoviesActivity se comprueba si estamos ante una versión por encima o igual a Lollipop, si es así, podremos usar la nueva api de transiciones con elementos compartidos (en este caso el póster de la película); Si no, se aplicará una animación para conseguir un efecto similar.

MoviesActivity.java

@Override
public void onClick(View v, int position, float touchedX, 
    float touchedY) {

    if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.LOLLIPOP)
        startSharedElementPosition(touchedView, position,
            movieDetailActivityIntent);

    else
        startDetailActivityAnimation(touchedView, (int) touchedX,
            (int) touchedY, movieDetailActivityIntent);
}

En vez de haber un elemento compartido, que se desplaza entre las dos actividades, en la actividad MovieDetailActivity, se escalará el póster de la película desde el último punto en donde el usuario tocó la pantalla.

MovieDetailActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        configureEnterTransition ();
    else {
        mViewLastLocation = getIntent().getIntArrayExtra(
            "view_location");

        configureEnterAnimation ();
    }
}
private void configureEnterAnimation() {

    GUIUtils.startScaleAnimationFromPivot(
        mViewLastLocation[0], mViewLastLocation[1],

    mObservableScrollView, new AnimatorAdapter() {

        @Override
        public void onAnimationEnd(Animator animation) {

            super.onAnimationEnd(animation);
            GUIUtils.showViewByScale(mFabButton);
        }
    });

    animateElementsByScale();
}

GuiUtils.java

public static void startScaleAnimationFromPivot (
    int pivotX, int pivotY, final View v,

    final AnimatorListener animatorListener) {
    final AccelerateDecelerateInterpolator interpolator =
        new AccelerateDecelerateInterpolator();

    v.setScaleY(SCALE_START_ANCHOR);
    v.setPivotX(pivotX);
    v.setPivotY(pivotY);
    v.getViewTreeObserver().addOnPreDrawListener(
        new OnPreDrawListener() {

        @Override
        public boolean onPreDraw() {
                v.getViewTreeObserver().removeOnPreDrawListener(this);
                ViewPropertyAnimator viewPropertyAnimator =
                    v.animate().setInterpolator(interpolator)
                    .scaleY(1).setDuration(SCALE_DELAY);

            if (animatorListener != null)
                viewPropertyAnimator.setListener(animatorListener);

            viewPropertyAnimator.start();
            return true;
        }
    });
}

Resultado:

VectorDrawables & Transiciones

Otro aspecto a compatibilizar son los VectorDrawables, ya que no fueron introducidos hasta la versión 21 del SDK (LLolipop), en su lugar se muestra una pequeña animación que escala la estrella y la gira, esto se logra con varios ViewPropertyAnimator.

La animación CircularReveal tampoco está disponible hasta LLollipop, por lo que de igual forma que con las transiciones, se escala una vista desde el último punto presionado.

MovieDetailActivity.java

@Override
public void showConfirmationView() {

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)

        GUIUtils.showViewByRevealEffect(mConfirmationContainer,
            mFabButton, GUIUtils.getWindowWidth(this));

    else 
        GUIUtils.startScaleAnimationFromPivot(
           (int) mFabButton.getX(),(int) mFabButton.getY(),
           mConfirmationContainer, null);
        animateConfirmationView();
        startClosingConfirmationView();
}

MovieDetailActivity.java

@Override
public void animateConfirmationView() {

    Drawable drawable = mConfirmationView.getDrawable();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        if (drawable instanceof Animatable)
            ((Animatable) drawable).start();
        else
            mConfirmationView.startAnimation(AnimationUtils
                .loadAnimation(this,R.anim.appear_rotate));
     }
}

Resultado:

scale

Soportando diferentes pantallas

Si por algo es conocido android es por ser capaz de funcionar en multitud de dispositivos, la variación entre tamaño y calidad de las diferentes pantallas es muy significativa, para asegurarse que todo se muestre correctamente tanto en un teléfono de 4″ como en una tablet de 10-12″ hay seguir unas pequeñas directrices.

family2

AutofitRecyclerView

Las películas son dispuestas en forma de grid, para ello se utiliza un RecyclerView con un GridLayoutManager para disponer los elementos, como constructor, la documentación de Google nos ofrece un parámetro spanCount para definir el número de columnas.

public GridLayoutManager (Context context, int spanCount)

El problema, es que dependiendo del ancho de la pantalla, podemos requerir que se muestren más columnas o menos.

El GridView, implementa un atributo ‘android:numColumns=”auto_fit”‘ para este fin, por desgracia, no está implementado en el RecyclerView, la idea es reutilizar ese atributo para conseguir el mismo objetivo.

Chiu-Ki Chan, ya se encontró con este problema y su gran solución fue publicada en su blog. A grandes rasgos consiste en configurar el parámetro ‘spanCount’ dependiendo del ancho de la pantalla.

Result:

autogrid

Multiples resources

El papel de los recursos del framework de android es indiscutible, la experiencia que puede tener un usuario en un nexus 5 puede ser muy diferente a la que sienta en un nexus 10, la actividad de detalle (MoviesDetailActivity) dispone los elementos de forma diferente dependiendo de la pantalla en la que se esté mostrando.

detailFamily
Lo óptimo es conseguir éste resultado con el menor número de modificaciones tanto en la propia actividad, MovieDetailActivity como los layouts, activity_detail.xml

Para esto veamos los recursos de la aplicación:

resources
Las distinciones mas notables entre dispositivos las podríamos dividir en los siguientes puntos:

- Los dispositivos con menos de 600dp de ancho de pantalla usarán las carpetas sin el modificador -w600dp, aquí se podrían categorizar la gran mayoría de teléfonos, por ejemplo: nexus 5, nexus 4, nexus 6, etc.

- Los dispositivos con más de 600dp de ancho de pantalla se dividirán en tres tipos: -w600dp, -w600dp-land y -w600dp-port.

De esta forma, podemos ir configurando nuestros layouts y las dimensiones en las diferentes carpetas para cambiar la vista según la pantalla en la que nos encontremos.

También se había mencionado el tema de que los VectorDrawable no están disponibles hasta la versión v21 del framework, por lo que el modificador -v21 está para ese propósito.

Por ejemplo, la ImageView que muestra el VectorDrawable de la estrella, es diferente si se trata de un dispositivo con LLolipop o no, <include>.

activity_detail.xml, todas las versiones

*imageview_star.xml* (layout-v21)

<FrameLayout>
    <!-- awesome hidden code -->
    <include layout="@layout/imageview_star"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_detail_confirmation_image"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:layout_gravity="center"
    android:src="@drawable/avd_star"
/>

imageview_star.xml, layout-v21

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_detail_confirmation_image"
    android:layout_width="150dp"
    android:layout_height="150dp"
    android:layout_gravity="center"
    android:src="@drawable/star"
/>

Yo me he decantado por esta solución, pero la modularidad de los recursos ofrece muchas soluciones posibles, por ejemplo, el width y height podrían apuntar a una dimensión en las carpetas values/dimen.xml y values-v21/dimen.xml, con 150dp y 300dp; El drawable podría ser el vector llamado drawable-v21/star.xml y en la carpeta drawable un archivo con la imagen en .png (drawable/start.png).

Vista con efecto Parallax en tablets landscape

Si consultáis aplicaciones como Google Books veréis que se aprecia una pequeña vista por debajo del Toolbar que al hacer scroll se desplaza a distinta velocidad que el Toolbar creando un efecto de Parallax.

books

Hay una buena serie de artículos llamada: How to hide/show Toolbar when list is scroling por Michal Z. que tratan este tema en más profundidad.

Para los layouts -w600dp-land existe una vista:

<View
    android:id="@+id/activity_movies_background_view"
    android:layout_width="match_parent"
    android:layout_height="@dimen/
    activity_movies_background_view_height"
    android:background="@color/theme_primary"
/>

La cuál es inyectada por ButterKnife opcionalmente, estando en un nexus 5, el layout vendría de activity_movies.xml en res/layouts, en este layout la vista no existe, por eso hay que marcarla con la anotación: @Optional

MoviesActivity.java

@Optional
@InjectView(R.id.activity_movies_background_view) View mTabletBackground;

 Translación de la vista de apoyo a medida que se desplaza el Recycler:

MoviesActivity.java

private RecyclerView.OnScrollListener recyclerScrollListener =
new RecyclerView.OnScrollListener() {

    @Override
    public void onScrolled(RecyclerView recyclerView,
    int dx, int dy) {

    // awesome hidden code here
    if (mTabletBackground != null) {

        mBackgroundTranslation = mTabletBackground.getY() - (dy / 2);
        mTabletBackground.setTranslationY(mBackgroundTranslation);
    }
}

Resultado:

Acerca del autor

Saúl Molinero Malvido

Android enthusiast. GDG Vigo & GDG Santiago, en twitter @_saulmm

  • Alo

    Grandísima entrada

  • http://twitter.com/neslaram Néstor Lara (@neslaram)

    Excelente artículo!

  • pedro

    genial la serie! gracias!

  • Juan

    ¿Has puesto el código fuente en github o similar?
    ¡¡Sería muy útil!!