Previously in this series we’ve looked at a number of different things that we can do to begin applying some principles of material design to our apps. In this concluding article in this series we’ll turn our attention to Activity transitions which are an important part of material design as they are designed to provide a smooth visual transition between different parts of the app which (if we do it right!) will provide the user with clear visual cues as to how they navigate through the app in response to their interactions.
We only have two Activities in our RSS app, so we only have a single transition point that we need to worry about! If we consider the two Activities, one provides a lists of articles which may be viewed and the other provides a detail view of a single article. So what the obvious transition would be when the user clicks on an article in the list it should expand in to the detail view.
This may sound quite daunting, but Activity transitions which were added in Lollipop actually make this pretty easy, and like the list item re-ordering that we looked at in the last article, the default behaviours are pretty good!
The key thing to understand is that common elements in both Activities need to be associated in order for the Activity transitions framework to make the transition for us. There are three items of information that we display for each item: the article title, the publication date, and the article body (although in the list view this is a shorter truncated description). So we clearly have the same items displayed in both Activities – so we need to tie them together in order to perform the expansion.
We start by giving the Views in the detail layout a transition name which is used to identify them when generating the transition animation. While it is possible to do this in the XML layout, we’d need to create a separate layout in res/layout-v21 with the additional attributes in order to avoid lint warnings. However we can also do this programmatically, in a much more backwardly compatible way using ViewCompat:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.feed_detail); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } Item item = (Item) getIntent().getSerializableExtra(ARG_ITEM); TextView title = (TextView) findViewById(R.id.feed_detail_title); TextView date = (TextView) findViewById(R.id.feed_detail_date); WebView webView = (WebView) findViewById(R.id.feed_detail_body); ViewCompat.setTransitionName(title, getString(R.string.transition_title)); ViewCompat.setTransitionName(date, getString(R.string.transition_date)); ViewCompat.setTransitionName(webView, getString(R.string.transition_body)); title.setText(item.getTitle()); date.setText(dateFormat.format(new Date(item.getPubDate()))); String html = item.getContent(); html = html.replaceAll(NEWLINE, BR); webView.loadData(html, HTML_MIME_TYPE, null); }
ViewCompat does not necessarily provide the features themselves, it merely provides us with a backwardly compatible way of making method calls which may not be supported on older version without having to add a load of version checking code (as we’ll see later on when we use ActivityOptionsCompat to avoid the boilerplate checking code, and explore how it does things).
So this is fine for the detail Activity which only has one of each View, but what about in the list – each of the list items will have a set of controls corresponding to these, but we want to transition the ones in the list item which the user has clicked. Using ListView this would have been a bit trickier but, once again, RecyclerView makes it very easy to get to precisely what we need!
We need to change our onItemClicked()
method to provide different Activity change behaviours depending on whether the Activity transitions framework is available:
@Override public void itemClicked(Item item) { Intent detailIntent = new Intent(FeedListActivity.this, FeedDetailActivity.class); detailIntent.putExtra(FeedDetailActivity.ARG_ITEM, item); FeedAdapter.ViewHolder viewHolder = (FeedAdapter.ViewHolder) recyclerView.findViewHolderForItemId(item.getPubDate()); String titleName = getString(R.string.transition_title); String dateName = getString(R.string.transition_date); String bodyName = getString(R.string.transition_body); PairtitlePair = Pair.create(viewHolder.getTitleView(), titleName); Pair datePair = Pair.create(viewHolder.getDateView(), dateName); Pair bodyPair = Pair.create(viewHolder.getBodyView(), bodyName); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, titlePair, datePair, bodyPair); ActivityCompat.startActivity(this, detailIntent, options.toBundle()); }
The third line of this method shows how RecyclerView makes our life much easier – we are able to get the ViewHolder corresponding to the list item which has been clicked (remember how we added stable IDs to our adapter based upon the publication date previously?). We then load our three transition name strings which correspond to the transitionName
attributes we set in FeedDetailActivity. Next we create three Pair objects which will map the relevant View objects from the ViewHolder (i.e. the View in the list item that the user has just clicked) to the Views with the transition name corresponding to the String value. Then we use ActivityOptionsCompat.makeSceneTransitionAnimation()
to generate an ActivityOptionsCompat object containing these mappings. There are some additional getters which have been added to the ViewHolder to allow us access to the relevant View objects which I won’t bother listing here (they’re in the source if you need to see them).
We discussed ViewCompat earlier and here, again, we’re using a compat wrapper for ActivityOptions and Activity. These are merely to enable us to use newer API calls whose behaviour will degrade gracefully on older version of Android which don’t support the method in question. For example ActivityOptionsCompat provides a makeSceneTransitionAnimation()
method which is backwardly compatible to API 4. However a quick look at the source of this show that it does nothing pre-Lollipop.
Finally we call startActivity()
and pass in our ActivityOptionsCompat converted to a Bundle. That’s it. Now the Activity transitions framework will do the rest:
The transition from the list view to the detail view is quite nice, but when clicking back, we get just a standard Activity change. The reason for that is the finish() call in the onOptionsItemSelected()
method in FeedDetailActivity. We simply need to change this to finishAfterTransition()
in order to reverse the transition:
@Override public boolean onOptionsItemSelected(MenuItem menuItem) { int id = menuItem.getItemId(); if (id == android.R.id.home) { ActivityCompat.finishAfterTransition(this); return true; } return super.onOptionsItemSelected(menuItem); }
Now we get a smooth return transition, as well:
Of course, it is possible to customise the transitions that are used if you require something different to the default behaviours, but this is perfectly adequate for our needs here. If I were polishing this for a commercial app, I’d definitely look at doing something a bit smoother with the background elements, but this code is designed to support a blog post and be simple to understand so I’ll happily sacrifice a polished app for simpler code, in this case.
With that we’ll finish this series on applying material design principles to apps, not because we’ve covered everything – far from it, we’ve barely scratched the surface. But more to provide a little bit of variety. We’ll certainly be covering more material-related topics in the future.
The source code for this article can be found here.
© 2014, Mark Allison. All rights reserved.
Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Great post! Looks like the two videos need to be swapped though 😉
Well spotted, and thanks for letting me know. It should now be fixed.
Great series, but currently your rss parser dosent set pubDate on item (maybe due to some change of rss structure) and because of that swaping items dosent work. Same problem with animation, it dosent start from proper place because items on list dont have unique id.
It’s working fine for me.