A useful stack on android #3, compatibility


This is the third post of the series: 'A useful stack on Android'.

In the first part I have tried to define a modular and scalabe architecture, based on the pattern: Model View Presenter (MVP).

The second part has explained how to take small bites of Material Design on the user interface, through colors, transitions, vectors, etc.

In this third part, we will talk about compatibility , it is known that the Android fragmentation is huge, different versions, screen sizes, fetaures, etc.

For that reason we will decrease a few versions from the inital version of the project (Lollipop) and we will support diferent screen sizes.

All these examples are available on GitHub - https://github.com/saulmm/Material-Movies


Downgrading SDK Levels

I have choosen the 16 level of the Android SDK, from the Dashboards published by Google to date, the number of devices that support versions from Jelly Bean is an appealing 86,8 %.

To give support to these versions, some minor changes are necessary, it is the case of the transitions with shared elements, that were not introduced until the 21 version of the Android framework.


Shared element transitions

When you press a film in the MoviesActivity, is checked if we have a version above or equal to Lollipop, if so, we may use the new API of transitions with shared elements (in this case the poster of the film); If not, I apply an animation to achieve a similar effect.

MoviesActivity

@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);
}

Instead of having a shared element, which is moving between the two activities, in the MovieDetailActivity, I will scale the poster of the film from the last point which the user has touched.

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;
            }
        });
    }

Result:


VectorDrawables & Slide transition

Another aspect to make compatible are the VectorDrawables, they were not introduced until version 21 of the sdk, (LLolipop), instead, I show a little animation that scales and rotates the star, that is made by a ViewPropertyAnimator.

The Animation: CircularReveal, is not available until LLolipop, so in the same way that with transitions, I scale a view from the last point touched.

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));
    }
}


Result:


Supporting different screen sizes

If something is known, android is for being able to operate in a multitude of devices, the variation between the size and quality of the different screens is very significant, to make sure everything is displayed properly in a 4" phone and a tablet of 10-12" must follow a few guides.


AutofitRecyclerView

The films are arranged in the form of grid by a RecyclerView with a GridLayoutManager. Google provides a constructor which offers a `spanCount parameter to set the number of columns.

public GridLayoutManager (Context context, int spanCount)

The problem is that depending on the width of the screen, we can need showing more columns or less.

The GridView view implements an attribute android:numColumns = "auto_fit" to this end, unfortunately, it is not implemented in the RecyclerView, the idea is to reuse that attribute to achieve the same objective.

Chiu-Ki Chan, already met this problem and her solution was posted in her blog. Broadly speaking it consists of setting the parameter spanCount depending on the width of the screen.

Result:


Multiples resources

The role of resources in the android framework is undisputed, the experience that a user can have on a nexus 5 can be very different to that sits in a 10" nexus, the (MoviesDetailActivity) has the elements differently depending on the screen that is displayed.


The optimum is to get this result with the least number of modifications both in the actual activity, MovieDetailActivity as the layout activity_detail.xml

For this see the application resources:


We could divide notable distinctions between devices in the following points:

In this way, we can go configuring our layouts and dimensions in different folders to change the layout according to the screen in which we find ourselves.

Also the issue that the VectorDrawable are not available until version had been mentioned. Is not availabe until the v21 of the framework, so -v21 modifier is for that purpose.

For example, the ImageView showing VectorDrawable star, is different if it is a device with LLolipop or not, (<include>).

activity_detail.xml (all versions)

<FrameLayout>
    <!-- awesome hidden code -->

    <include
        layout="@layout/imageview_star"
    />

</FrameLayout>

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="300dp"
    android:layout_height="300dp"
    android:layout_gravity="center"
    android:src="@drawable/avd_star"
    />

imageview_star.xml (layout)

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

I have implemented this solution, but the modularity of resources offers many possible solutions, for example, the width and height could point to a dimension in the folders values/dimen.xml and _ values-v21/dimen.xml, with 150dp and 300dp; The drawable could be the vector called _drawable-v21/star. xml and in the folder drawable/start.png file with the image in .png.


Parallax landscape view

If you check the Google Books app, you will see that you can seea small view below the Toolbar, when you scroll the list of books that view moves at a different speed than the Toolbar creating an effect of Parallax.

There are some great series called How to hide/show Toolbar when list is srolling by Michal Z. that treated this topic in deeper.

For layouts '-w600dp-land` and higher:

    <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"
        />`

This view is injected by ButterKnife, being in a nexus 5, the layout you would come from 'activity_movies.xml' in the folder (layouts), where that view does not exist, so we must mark the view with the annotation: '@Optional'.

MoviesActivity.java

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

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);
        }
    }

Result: