Monday, November 25, 2013

Sample Android Project: Movies

There's a sample application that I worked on recently with talented designer Chris Arvin for a talk we gave at DroidCon UK 2013.  I also repurposed part of it for the talk about animations I gave at AnDevCon a few weeks later.

The app is open source and you can check it out here: https://github.com/dlew/android-movies-demo

You can get the APK here: https://github.com/dlew/android-movies-demo/releases/



The interactions required a bit of interesting engineering so I wanted to discuss some of it here.

ViewPager and Decor

One of the key interactions we wanted was the app to feel like a ViewPager so that it was an interaction users were familiar with except that the result isn't quite the same - instead of paging, content would slide out from underneath other content.  This is not something ViewPager is inherently designed to do.

There were two options open to me.  One was to rip out all of ViewPager's event handling code and make my own custom View, which was not an appealing prospect.  The other was to find some way to manipulate ViewPager itself.

It turns out there is an interface ViewPager finds special called Decor.  If a View implements Decor, then ViewPager treats it as a special View that can remain on top of it the ViewPager.  This is normally for implementing your own tabs, but in this case I imported my own copy of ViewPager (since Decor is hidden normally) and made my entire UI a Decor View.

It's a neat trick that would work for any app that wants ViewPager event handling without paging Views, though I think if you wanted a less hacky solution you'd write your own event code.

Custom Views Everywhere

I've gone from avoiding custom Views like the plague to fully embracing them.

The key realization is that custom Views do not need to handle every situation.  If you're looking at framework custom Views as reference you will be overwhelmed quickly.  For example, during measurement framework Views have to handle WRAP_CONTENT, MATCH_PARENT, and everything in between; but if you know your View is always going to be MATCH_PARENT you can greatly simplify all your code.

The movies sample app is a pile of custom Views.  First there's SlidingRevealViewGroup, which generically shows one View slide out from another.  On top of that is built MovieRowView, which has the specific Views that we want to display.  Then there's SlidingPairView, which is a set of two SlidingRevealViewGroups that creates the side-by-side effect seen in the app.

I also needed a few other custom Views to shore up some other issues.  CenteringRelativeLayout just adjusts the film cover so that, as it shrinks in size, it still looks centered.  SlidingListView was required for performance; we needed to manipulate the rows directly when sliding, instead of constantly notifying of data changed.

Performance Tricks

The coolest part of the whole app is the slide: how the cover becomes smaller and the content slides out from underneath.  All of this was achieved through translation of Views.  I took advantage of the fact that ViewGroups don't render their children outside of their own bounds.  By translating content I could hide them outside the clip bounds.

When a slide starts, it throws practically everything into hardware layers then just slides Views left/right.  Moving around pre-rendered content is fast and as a result the paging is silky smooth.

Nowadays I'm a huge fan of those basic properties of Views (translation, scale, rotation and alpha).  They're the core reason to support ICS+ only; with these properties you can take your interactions to the next level.

Drawbacks

The drawback of the solution I came up with is that it creates a ton of overdraw.  This hurts performance, especially (as far as I can tell) when rendering a new row while scrolling up/down.  I sacrificed scrolling performance for paging performance.  Perhaps there is a way to achieve both, but I'm pretty much done with this sample for now.

The rounded corners could've been implemented in a much better fashion than an overlaid Drawable, but with the limited time before the presentation I had, I could not come up with a better solution.

4 comments:

  1. this is awesome. thanks for the Decor hint :) I think I need to pay much more attention to internal views implementation..

    ReplyDelete
  2. final ViewPager.LayoutParams headerParams = new ViewPager.LayoutParams();
    headerParams.width = ViewPager.LayoutParams.MATCH_PARENT;
    headerParams.height = ViewPager.LayoutParams.WRAP_CONTENT;
    headerParams.isDecor = true;
    headerParams.gravity = Gravity.TOP;
    pagerView.addView(headerView, -1, headerParams);

    ReplyDelete
  3. To optimize overdraw you could just clip the bottom views with the bounds of the upper layers. This is the technique I used in this demo: http://www.youtube.com/watch?v=kOyOvT6Sp_o

    ReplyDelete
  4. maybe app that have some code like this post is popcorn time

    ReplyDelete