Udacity Android Developer Nanodegree: Week 2

This is a series of posts about my experience leveling up my Android development skills with Udacity's Nanodegree program. The entire series can be found under the udacity tag here.


Progress This Week

I'm happy to report I've completed Project 1 - Popular Movies Stage 1 today! (bonus: also added some tasteful animations ;). Here it is in action:

Along the way I employed a number of APIs for achieving (almost) complete Material Design spec compatibility, ranging all the way from Meaningful Transitions to bold, vibrant colors to the Typography guidelines. It was fun to create, and I want to talk a bit about the implementation here. There are several subtle details involved in the Grid => Details activity transition. Let's watch it in slow motion:

Slow-motion GIF of movie details transition

Material Design: Meaningful Transitions

First, the movie poster we click expands into the details screen, taking into account the exact bounds of the poster's View. This helps preserve the user's context of how they landed on a particular screen and makes the app feel like a real, physical space. This is from the Navigational transitions section of the Material Design spec. It is done using ActivityOptions.makeScaleUpAnimation():

// In MoviesGridFragment#onPosterClick(View poster):
Intent intent = new Intent(getActivity(), MovieDetailsActivity.class);
ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(
        // the source view from which to animate the new Activity
        // defines the co-ordinate space for initial (x, y) location
        poster,
        // starting (x, y) position for animation
        // NOTE: these co-ordinates are relative to the source view above
        0, 0,
        // initial width and height of the new Activity
        poster.getWidth(), poster.getHeight());
getActivity().startActivity(intent, opts.toBundle());

See here for the full expand transition code on Github.

Second, the text content (movie title, rating, synopsis, etc) fades in with a small slide-up effect, and the important part is, all those TextViews don't animate together, but rather with a little delay, one after another. This is called a staggered animation, and it's described in the Material Design spec here. The importance of staggering is that it guides the user's eye to the most important elements first: the title enters first and the eye is drawn to it, then the release date, the rating, and finally the synopsis. For this I used ViewPropertyAnimator. Let's look at the code (links are included below):

View[] animatedViews = new View[] {
        mTitle, mReleaseDate, mRatingContainer, mSynopsis
};

// see here for why using the right interpolator is important:
// http://www.google.com/design/spec/animation/authentic-motion.html#authentic-motion-mass-weight
// and here for how to use them:
// http://developer.android.com/guide/topics/graphics/prop-animation.html#interpolators
Interpolator interpolator = new DecelerateInterpolator();

for (int i = 0; i < animatedViews.length; ++i) {
    View v = animatedViews[i];
    
    // initial state: hide the view and move it down slightly
    v.setAlpha(0f);
    v.setTranslationY(75);

    v.animate()
            // let's enable hardware acceleration for better performance
            // http://blog.danlew.net/2015/10/20/using-hardware-layers-to-improve-animation-performance/
            .withLayer()
            .alpha(1.0f)
            .translationY(0)
            .setInterpolator(interpolator)
            // this little calculation here produces the staggered effect we 
            // saw, so each animation starts a bit after the previous one
            .setStartDelay(100 + 75 * i)
            .start();
}

One important thing here is, if you just use the above code as it is, you will notice that the Views that are sliding upwards are clipped from below. That's because ViewGroups automatically clip their children when drawing. This needs to be disabled by setting android:clipChildren to false on each ViewGroup in your XML or code. The code for this is here and here on Github.

And lastly, ...

Material Design: Bold, Vibrant Colors

... the Toolbar on the top changes color smoothly to a vibrant hue taken from the backdrop image. This is courtesy the amazing Palette API introduced in the v7 support library. Code walkthrough:

// `bitmap` is the image from which to pick colors
Palette.Builder(bitmap)
        .generate(new Palette.PaletteAsyncListener() {
            @Override
            public void onGenerated(Palette palette) {
                setPalette(palette);
            }
        });

// inside setPalette():
Palette.Swatch vibrant = palette.getVibrantSwatch();

// vibrant background color for toolbar
int newPrimaryColor = vibrant.getRgb();

// appropriate text color for toolbar, with sufficient
// contrast to the background color
final int newTitleTextColor = vibrant.getTitleTextColor();

// dark variation of vibrant color, for the status bar
int newPrimaryDarkColor = Util.multiplyColor(newPrimaryColor, 0.8f);

// current toolbar background color
int currentPrimaryColor = getResources().getColor(R.color.colorPrimary);

// credits: http://stackoverflow.com/a/14467625/504611
// ValueAnimator generates a smooth color transition for us
// ArgbEvaluator is responsible for figuring out how
// different 2 colors are, and how to generate a new color
// "in between" them
ValueAnimator colorAnimation = ValueAnimator
        .ofObject(new ArgbEvaluator(), currentPrimaryColor, newPrimaryColor)
        .setDuration(500);
colorAnimation.setInterpolator(new AccelerateInterpolator());
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
            mToolbar.setBackgroundColor((int) animator.getAnimatedValue());
        }
    });
colorAnimation.start();

This code can be found here, here and here on Github. NOTE: if you use Picasso, you'll have to write some extra boilerplate code so that Palette can access the bitmap after it's been downloaded and decoded. See Jake Wharton's blog post and this gist.

Material Design: Typography

The Material Design spec lays out clear guidelines for typography:

Too many type sizes and styles at once can wreck any layout. A typographic scale has a limited set of type sizes that work well together along with the layout grid. These sizes and styles were developed to balance content density and reading comfort under typical usage conditions.

Material Design Typographic scale

The sweet thing is, these styles are already bundled nicely with the AppCompat Material theme! To use these, simply specify a style in XML like this:

<TextView
    android:id="@+id/movie_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Interstellar"
    android:textAppearance="@style/TextAppearance.AppCompat.Title"
    />

You can use any of the styles from the guidelines: Title, Subheading, Body1, Caption, Display3, etc. You can see the styles I'm using here on Github.

Another issue is line spacing. It is crucial to give your text some space to breathe else it becomes difficult to read. See for yourself:

line spacing comparison

To increase line spacing all you need to do is set android:lineSpacingMultiplier to a factor slightly > 1 like 1.15 which I use here.


That's all for week 2! Feel free to leave a comment below if you have anything to add!