All posts
Android, Blog, UI, UX

Workcation App – Part 1. Fragment custom transition

Mariusz Brona

Mariusz Brona

Welcome to the first of series of posts about my R&D (Research & Development) project I’ve made a while ago. In this blog posts, I want to share my solutions for problems I encountered during the development of an animation idea you’ll see below.

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 animation on Dribbble: https://dribbble.com/shots/2881299-Workcation-App-Map-Animation

Prelude

A few months back we’ve 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 never knew what I’m 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.

  1. 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 loaded with translation from the bottom, markers added to the map with scale/fade animation.
  2. While scrolling the items in RecyclerView, the markers are pulsing to show their position on the map.
  3. After clicking on the item, we are transferred to the next screen, the map is animated below to show the route and start/finish marker. The RecyclerView’s item is transitioned to show some description, bigger picture, trip details and button.
  4. While returning, the transition happens again back to the RecyclerView’s item, all of the markers are shown again, the route disappears.

Pretty much. That’s why I’ve decided to show you all of the things in the series of posts. In this article I will cover the enter animation of the map fragment.

The Problem

As we can see in the GIF 1 above, it looks like the map is already loaded before and just animated to the proper position. This is not happening in the real world. What it really looks like:

The Solution

  1. Preload map
  2. When map is loaded, use Google Maps API to get bitmap from it and save it in cache
  3. Create custom transition for scale and fade effect of the map when entering DetailsFragment

Let’s go!

Preloading the map

To achieve that we have to take bitmap snapshot from already loaded map. Of course we can’t do that in the DetailsFragment if we want the smooth transition between screens. What we have to do, is to get bitmap underneath the HomeFragment and save it in the cache. As you can see, the map have some margin from the bottom, so we also have to fit the “future” map size.

As you can see in the snippet above, the MapFragment is placed under the rest of the layout. It will allow us to load the map invisible for the user.

MainActivity inherits from MvpActivity, which is a class from Mosby Framework created by Hannes Dorfmann. The whole project follows MVP pattern, and the framework I mentioned before is a very nice implementation of it.

In onCreate method we have three things:

  1. We are providing LatLngBounds for map – they will be used to set the bounds of the map
  2. We are replacing the HomeFragment in activities container layout
  3. We are setting the OnMapReadyCallback on the MapFragment

After map is ready, the onMapReady() method is called, and we can do some operations to save the properly loaded map into bitmap. We are moving camera to earlier provided LatLngBounds using CameraUpdateFactory.newLatLngBounds() method. In our case we know exactly what will be the dimension of the map in the next screen, so we are able to pass width(width of screen) and height(height of screen with bottom margin) parameters to this method. We are calculating them like this:

Easy. After googleMap.moveCamera() method being called, we are setting the OnMapLoadedCallback. When camera is moved to the desired position, the onMapLoaded() method is called and we are ready to take bitmap from it.

Getting bitmap and saving in cache

The onMapLoaded() method has only one task to do – call presenter.saveBitmap() after snapshot is taken from the map. Thanks to lambda expression, we can reduce boilerplate to simply one line

The presenters code is really simple. It only saves our bitmap in the cache.

I’ve used LruCache for it, because it is the recommended way, well described here.

So we have bitmap saved in the cache, the only thing we have to do is to make custom transition for scale and fade effect of the map when entering DetailsFragment. Easy peasy lemon squeezy.

Custom enter transition for fade/scale map effect

So the most interesting part! The code is rather simple, however it allows us to do great stuff.

What we do in this transition is that we are scaling down the scaleX and scaleY properties from ImageView from scaleFactor(default is 8) to desired view scale. So in other words we are increasing the width and height by scaleFactor in the first place, and then we are scaling it down to desired size.

Creating custom transition

In order to do the custom transition, we have to inherit from Transition class. The next step is to override the captureStartValues and captureEndValues. What is happening here?

The Transition Framework is using the Property Animation API to animate between view’s start and end property value. If you are not familiar with this, you should definetely read this article.  As explained before, we want to scale down our image. So the startValue is our scaleFactor, and endValue is the desired scaleX and scaleY – normally it will be 1.

How to pass those values? As said before – easy. We have TransitionValues object passed as argument in both captureStart and captureEnd methods. It contains a reference to the view and a map in which you can store the view values – in our case the scaleX and scaleY.

With values captured, we need to override the createAnimator() method. In this method we are returning the Animator (or AnimatorSet) object which animates changes between view property values. So in our case we are returning the AnimatorSet which will animate the scale and alpha of the view. Also we want our transition to work only for ImageView, so we check if view reference from TransitionValues object passed as argument is ImageView instance.

Applying custom transition

We have bitmap stored in memory, we have transition created, so we have last step – applying the transition to our fragment. I like to create static factory method for creating the fragments and activities. It looks really nice and helps us to keep the code rather clean. It is also the nice idea to put our Transition there programmatically.

As we can see it is really easy to do. We create new instance of our transition, we add target here and also in XML of the target view, via transitionName attribute.

Next we just pass transition to fragment via setEnterTransition() method and voila! There is the effect:

Conclusion

As you can see, the final result is closer to the original from the GIF than native map loading. There is also a glitch in the final phase of the animation – this is because the snapshot from the map is different than the content of the SupportMapFragment.

Thanks for reading! Next part will be published on Tuesday 7.03. Feel free to leave a comment if you have any questions, and if you found this blog post helpful – don’t forget to share it!

Newsletter

The post is created by Droids On Roids Team Member.
We would love to take care of your business.

Leave comment

  • lastforeverzl

    Great Post! But I got NPE error in running the demo. Looks like the startValues is Null.
    java.lang.NullPointerException: Attempt to read from field ‘java.util.Map android.transition.TransitionValues.values’ on a null object reference
    at com.droidsonroids.workcation.common.transitions.ScaleDownImageTransition.createAnimator(ScaleDownImageTransition.java:66)

    • Mariusz Brona

      Please add issue to the repository and I’ll look into it as soon as possible 😉