Workcation App – Part 4. Shared Element Transition with RecyclerView and Scenes
Discover how to show details layout with Shared Element Transition with Scene Framework!
Welcome to the fourth and last part of my post series about the R&D (Research & Development) project, which I made a while ago. I want to share with you my solutions for problems I encountered during the development of an animation idea, shown below. In this blogpost, I will cover how to show details layout with Shared Element Transition with Scene Framework!
Part 1: Fragment’s custom transition
Part 2: Animating Markers with MapOverlayLayout
Part 3: RecyclerView interaction with Animated Markers
Part 4: Shared element transition with RecyclerView and Scenes
Link for the project on Github: Workcation App
Link for the animation on Dribbble:
https://dribbble.com/shots/2881299-Workcation-App-Map-Animation
PRELUDE
A few months back, we had a company meeting, where my friend Paweł Szymankiewicz showed the animation he’d done during his Research & Development. And I loved it. After the meeting, I decided that I will code it. I didn’t know what I was going to struggle with…
GIF 1 “The animation”
LET’S START!
As we can see, in the GIF above, there is a lot of going on.
- After clicking on the bottom menu item, we are moving to the next screen, where we can see the map being loaded with some scale/fade animation from the top, RecyclerView items are loaded with translation from the bottom, while markers are added to the map with a scale/fade animation.
- While scrolling the items in RecyclerView, the markers are pulsing to show their position on the map.
- After clicking on an item, we are transferred to the next screen, while the map is animated below to show the route and the start/finish marker. The RecyclerView’s item is transitioned to show some description, a bigger picture, trip details and button.
- While returning, the transition happens again back to the RecyclerView’s item, showing all of the markers again, as the route disappears.
That’s pretty much it. This is also why I’ve decided to show you all of these things in a series of posts. As I have mentioned above, in this article, I will cover how to show a details layout with Shared Element Transition with Scene and Transition Framework!
THE PROBLEM
Okay, so as we can see in the GIF above, after clicking on the item in RecyclerView, we are entering the details layout with some information about our trip destination. There is definitely a Shared Element Transition, where some of the views are changing their bounds, while TextView is also changing its size and details with red buttons are sliding in from the bottom. Thanks to the Transition Framework, we are able to create this amazing animation.
My first guess was to create just like it appears in 90% of materials on the internet – via the Shared Element Transition between two Activities. However let’s look on the map. There is also an animation “underneath” the details layout – the route is being drawn and the map zooms to the fixed position. So, I guess we don’t want to create another activity with a transparent background and try to animate the map on the “background” activity.
My second guess was to create the Shared Element Transition between two fragments – add the DetailsFragment on the top of the stack and create a transition between views in both layouts – RecyclerView’s item and DetailsFragment’s layout. A better solution, but still – for me, it looks like the same screen and the same fragment, only with another layout added on top. So, do we have a solution that fits my needs?
Yes, we have! From API 19 (the Workcation App is 21 and above) we have an option – Scenes! When used with Transition Framework, they’re really powerful. We can manage our UI in a very sophisticated way. And the most important thing – it totally fits our needs! So, let’s look at the implementation!
SHARED ELEMENT TRANSITION WITH RECYCLER VIEW
Let’s start from clicking on the RecyclerView’s item. Our DetailsFragment (the one with Map and RecyclerView) implements OnPlaceClickedListener. We pass the OnPlaceClickListener interface implementation in the constructor like this:
1 2 3 4 | BaliPlacesAdapter(OnPlaceClickListener listener, Context context) { this.listener = listener; this.context = context; } |
Next, in the onBindViewHolder method, we trigger the onPlaceClicked method after clicking on the RecyclerView’s item. We do this simply by passing it to onClickListener:
1 2 3 4 5 | @Override public void onBindViewHolder(final BaliViewHolder holder, final int position) { [...] holder.root.setOnClickListener(view -> listener.onPlaceClicked(holder.root, TransitionUtils.getRecyclerViewTransitionName(position), position)); } |
As you can see above, we set the onClickListener on a root element – in our case, it is a CardView. We also pass it as the first argument in the onPlaceClicked method, while the second one is fixed TransitionName – we are simply adding a position to the transition name. We do this because we have to distinguish which child of RecyclerView needs to be transitioned. If every item has the same:
1 2 3 | public static String getRecyclerViewTransitionName(final int position) { return DEFAULT_TRANSITION_NAME + position; } |
For the last parameter, we pass the position of the clicked item. We are using the same collection of data to fill RecyclerView item and DetailsLayout, so we just want to get the specific item by the position. Below we can see the OnPlaceClickListener and BaliViewHolder
1 2 3 | interface OnPlaceClickListener { void onPlaceClicked(View sharedView, String transitionName, final int position); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | static class BaliViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.title) TextView title; @BindView(R.id.price) TextView price; @BindView(R.id.opening_hours) TextView openingHours; @BindView(R.id.root) CardView root; @BindView(R.id.headerImage) ImageView placePhoto; BaliViewHolder(final View itemView) { super(itemView); ButterKnife.bind(this, itemView); } } |
The OnPlaceClickListener is implemented in DetailsFragment – the one that has RecyclerView and Map. Let’s look at the onPlaceClicked method:
1 2 3 4 5 6 7 | @Override public void onPlaceClicked(final View sharedView, final String transitionName, final int position) { currentTransitionName = transitionName; detailsScene = DetailsLayout.showScene(getActivity(), containerLayout, sharedView, transitionName, baliPlaces.get(position)); drawRoute(position); hideAllMarkers(); } |
In the first place, we save currentTransitionName as a global variable – we will need it when we will hide scene with DetailsLayout. We also assign the Scene object to the detailsScene variable – it is needed to handle the onBackPressed method properly. In the next step, we are drawing a route between our position and the destination position; parallelly, we are hiding all markers.
The part that we are mostly interested in is the one showing the scene. Let’s move to DetailsLayout.
USING SCENE FRAMEWORK TO CREATE SHARED ELEMENT TRANSITION
Below we have our custom CoordinatorLayout. It looks ordinary in the first part, but we have two additional two static methods – showScene and hideScene. Let’s look at them more closely:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class DetailsLayout extends CoordinatorLayout { @BindView(R.id.cardview) CardView cardViewContainer; @BindView(R.id.headerImage) ImageView imageViewPlaceDetails; @BindView(R.id.title) TextView textViewTitle; @BindView(R.id.description) TextView textViewDescription; public DetailsLayout(final Context context) { this(context, null); } public DetailsLayout(final Context context, final AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); ButterKnife.bind(this); } private void setData(Place place) { textViewTitle.setText(place.getName()); textViewDescription.setText(place.getDescription()); } public static Scene showScene(Activity activity, final ViewGroup container, final View sharedView, final String transitionName, final Place data) { DetailsLayout detailsLayout = (DetailsLayout) activity.getLayoutInflater().inflate(R.layout.item_place, container, false); detailsLayout.setData(data); TransitionSet set = new ShowDetailsTransitionSet(activity, transitionName, sharedView, detailsLayout); Scene scene = new Scene(container, (View) detailsLayout); TransitionManager.go(scene, set); return scene; } public static Scene hideScene(Activity activity, final ViewGroup container, final View sharedView, final String transitionName) { DetailsLayout detailsLayout = (DetailsLayout) container.findViewById(R.id.bali_details_container); TransitionSet set = new HideDetailsTransitionSet(activity, transitionName, sharedView, detailsLayout); Scene scene = new Scene(container, (View) detailsLayout); TransitionManager.go(scene, set); return scene; } } |
In the first place, we are inflating the DetailsLayout. Next, we are setting the data (just the title and description for DetailsLayout). Next, we are creating TransitionSet – for our purpose, I’ve created a separate class to keep the codebase clean. The third step is to create the scene object – we just pass our inflated detailsLayout and containerView (the main ViewGroup of DetailsFragment – in our case, it is the FrameLayout that matches the whole screen and has RecyclerView as a child). To create our amazing animation, we just need to call one method – TransitionManager.go(scene, transitionSet). Et voila! This is our effect:
A little bit of magic happens here. TransitionManager is a class which fires transitions when the scene change occurs. With a simple call like TransitionManager.go(scene, transitionSet), we are able to go to the specific scene, where some kind of transition happens. In our case, we are showing DetailsLayout with some data about a trip destination but, when using a TransitionManager, we are able to make this kind of transition which you can see above. Now, let’s see how the ShowDetailsTransitionSet is implemented.
CREATING CUSTOM TransitionSet WITH TransitionBuiler
For keeping the code clean, I’ve created a TransitionBuilder – a simple class made using the Builder Pattern, which allows us to write less code to make a Transition, especially the Shared Element Transition. This is how it looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | public class TransitionBuilder { private Transition transition; public TransitionBuilder(final Transition transition) { this.transition = transition; } public TransitionBuilder duration(long duration) { transition.setDuration(duration); return this; } public TransitionBuilder target(View view) { transition.addTarget(view); return this; } public TransitionBuilder target(Class clazz) { transition.addTarget(clazz); return this; } public TransitionBuilder target(String target) { transition.addTarget(target); return this; } public TransitionBuilder target(int targetId) { transition.addTarget(targetId); return this; } public TransitionBuilder delay(long delay) { transition.setStartDelay(delay); return this; } public TransitionBuilder pathMotion(PathMotion motion) { transition.setPathMotion(motion); return this; } public TransitionBuilder propagation(TransitionPropagation propagation) { transition.setPropagation(propagation); return this; } public TransitionBuilder pair(Pair<View, String> pair) { pair.first.setTransitionName(pair.second); transition.addTarget(pair.second); return this; } public TransitionBuilder excludeTarget(final View view, final boolean exclude){ transition.excludeTarget(view, exclude); return this; } public TransitionBuilder excludeTarget(final String targetName, final boolean exclude) { transition.excludeTarget(targetName, exclude); return this; } public TransitionBuilder link(final View from, final View to, final String transitionName) { from.setTransitionName(transitionName); to.setTransitionName(transitionName); transition.addTarget(transitionName); return this; } public Transition build() { return transition; } } |
Okay, now let’s go to the ShowDetailsTransitionSet, which creates this amazing transition. In the constructor, we pass the Context object, transitionName – this is the one that is created with the position in RecyclerView – the View object that we are transitioning from and the DetailsLayout that we are transitioning to. We also call the addTransition method, where we pass the Transition built with TransitionBuilder and returned from a specific method – textResize(), slide() and shared().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class ShowDetailsTransitionSet extends TransitionSet { private static final String TITLE_TEXT_VIEW_TRANSITION_NAME = "titleTextView"; private static final String CARD_VIEW_TRANSITION_NAME = "cardView"; private final String transitionName; private final View from; private final DetailsLayout to; private final Context context; ShowDetailsTransitionSet(final Context ctx, final String transitionName, final View from, final DetailsLayout to) { context = ctx; this.transitionName = transitionName; this.from = from; this.to = to; addTransition(textResize()); addTransition(slide()); addTransition(shared()); } private String titleTransitionName() { return transitionName + TITLE_TEXT_VIEW_TRANSITION_NAME; } private String cardViewTransitionName() { return transitionName + CARD_VIEW_TRANSITION_NAME; } private Transition textResize() { return new TransitionBuilder(new TextResizeTransition()) .link(from.findViewById(R.id.title), to.textViewTitle, titleTransitionName()) .build(); } private Transition slide() { return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(R.transition.bali_details_enter_transition)) .excludeTarget(transitionName, true) .excludeTarget(to.textViewTitle, true) .excludeTarget(to.cardViewContainer, true) .build(); } private Transition shared() { return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move)) .link(from.findViewById(R.id.headerImage), to.imageViewPlaceDetails, transitionName) .link(from, to.cardViewContainer, cardViewTransitionName()) .build(); } } |
So, recapping and describing everything above:
- RecyclerView’s item’s title is animated as a SharedElementTransition with TextResize transition (it is a specific case, which is well described in this video).
- The whole layout has a custom slide transition, implemented with some of kind start delay for a specific child.
- The RecyclerView’s item’s header and container have SharedElementTransition implemented with a Move transition – the default transition for Android’s framework.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="together" android:duration="500"> <slide android:slideEdge="bottom" android:interpolator="@android:interpolator/decelerate_cubic"> <targets> <target android:targetId="@id/descriptionLayout" /> </targets> </slide> <slide android:slideEdge="bottom" android:interpolator="@android:interpolator/decelerate_cubic" android:startDelay="100"> <targets> <target android:targetId="@id/description" /> </targets> </slide> <fade android:interpolator="@android:interpolator/decelerate_cubic" android:startDelay="100"> <targets> <target android:targetId="@id/description" /> </targets> </fade> <slide android:slideEdge="bottom" android:interpolator="@android:interpolator/decelerate_cubic" android:startDelay="200"> <targets> <target android:targetId="@id/takeMe" /> </targets> </slide> </transitionSet> |
With this set of different transitions we are able to create this “enter transition” for our layout:
Looks awesome to me! But what about a returning scene? Look below!
RETURNING TO THE PREVIOUS Scene AND HANDLING onBackPress
As you can remember, we have two methods in DetailsLayout – showScene and hideScene. We’ve just covered the first method, but what’s with the second method? Let’s cover it too!
1 2 3 4 5 6 7 8 | public static Scene hideScene(Activity activity, final ViewGroup container, final View sharedView, final String transitionName) { DetailsLayout detailsLayout = (DetailsLayout) container.findViewById(R.id.bali_details_container); TransitionSet set = new HideDetailsTransitionSet(activity, transitionName, sharedView, detailsLayout); Scene scene = new Scene(container, (View) detailsLayout); TransitionManager.go(scene, set); return scene; } |
Now, there are few changes. We’ve added the DetailsLayout to DetailsFragment’s container (the FrameLayout mentioned before). So, to obtain DetailsLayout, we have to call findViewById on this container. Then, we have to create the TransitionSet with specific targets and specified settings. For this purpose, I’ve also written another class which inherits from TransitionSet – HideDetailsTransitionSet. This is how it looks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class HideDetailsTransitionSet extends TransitionSet { private static final String TITLE_TEXT_VIEW_TRANSITION_NAME = "titleTextView"; private static final String CARD_VIEW_TRANSITION_NAME = "cardView"; private final String transitionName; private final View from; private final DetailsLayout to; private final Context context; HideDetailsTransitionSet(final Context ctx, final String transitionName, final View from, final DetailsLayout to) { context = ctx; this.transitionName = transitionName; this.from = from; this.to = to; addTransition(textResize()); addTransition(shared()); } private String titleTransitionName() { return transitionName + TITLE_TEXT_VIEW_TRANSITION_NAME; } private String cardViewTransitionName() { return transitionName + CARD_VIEW_TRANSITION_NAME; } private Transition textResize() { return new TransitionBuilder(new TextResizeTransition()) .link(from.findViewById(R.id.title), to.textViewTitle, titleTransitionName()) .build(); } private Transition shared() { return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move)) .link(from.findViewById(R.id.headerImage), to.imageViewPlaceDetails, transitionName) .link(from, to.cardViewContainer, cardViewTransitionName()) .build(); } } |
In this case, we have textResize() and shared() transitions once again. If you look closely at both methods, you will see that this TranstionBuilder has method link(). This method takes three parameters – sourceView, targetView and transitionName. It simply puts transitionName on sourceView and targetView, as well as placing it as a target on Transition object. So it’s used to “link” two views for Shared Element Transition.
The rest looks the same. We create the Scene object, call TransitionManager.go() and voila! We have returned to the previous state!
CONCLUSION
As we can see – the sky’s the limit! We are able to create meaningful transitions with activities, fragments and even the layouts! Scenes and Transitions are really powerful and can really improve the UI and UX. What are the benefits from this solution? First of all, we don’t have another lifecycle to care about. Secondly, there are some libraries that help us to create “fragmentless” UI. Applied with Scenes and Transitions, we can develop a pretty nice app. Thirdly, this approach is really rare, however, I think it’s giving us more control.
That’s all folks! Thank you very much for reading my series of posts, I hope you liked it!
See you soon!
Mariusz Brona aka panwrona
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!
Great article. The only question I have is if any smoothing can be done for the text transition. When you tap the card, there is sort of a cropping that happens before the transition takes place. Any way to avoid that?
I think it’s problem with paddings. Transition doesn’t set paddings automatically, you need to set paddings to view manually in transition callback. Or just replace it with margins (if applicable)
public void onBindViewHolder(final BaliViewHolder holder, final int position) – https://youtu.be/LqBlYJTfLP4?t=2589
Bad article. No sample source code, hard to read code from end to start. Why need to create custom class for TransitionSet if you can just define it inside R.transition.your_transition. And last. TextResize will not just work if you not get source of sample project “android-unsplash”, where you will see how it reallly works (it works bad, cause there too many hardcode, like putting manually text sizes to intent to set it for exit transition)
Hi, sorry to hear that it’s a bad article, however, I think you’ve missed some things while reading it. First of all, in the top of the article there is a link for github repository with all of the code – https://github.com/panwrona/Workcation . Also there is about 300 lines of code in the article. I think it’s enough to understand how it’s been made.
Second of all, we have to set transition name dynamically – we have to distinguish which item from RecyclerView needs to be transitioned. That’s why I’ve used custom class for TransitionSet. Of course we can do for example Factory class for Transitions and TransitionSets, but with TransitionBuilder class we have clean and easy to read custom TransitionSets classes. I think it’s better approach.
Third of all, I’ve used TextResize transition after watching this video: https://www.youtube.com/watch?v=4L4fLrWDvAU . It’s a second comment about problems with that class – I will look closely into it. You can always create an issue in the repository or pull request if you have some solutions for that 😉
Cheers!
Well done, Mariusz!!!!
This project is one of a kind! It’s just superb!
I’ll get down to work and implement your techniques in my projects straight away!
Thanks a lot!
I have tried the github example on Samsung Galaxy s7, the app doesn’t ask for permissions, the map doesn’t work and the app also crashes. I think you should do a little more work on this. It’s an awesome concept however it’s a little too complicated. I guess you should have tried doing it with less classes. This way it looks like one of those old telephone centrals where there are wires leading to everywhere just to achieve a couple of things.