Adetunji Dahunsi

Creating an Expandable Floating Action Button in Android — Part 2

An expanded floating action button used to call out sharing

TJ Dahunsi

Dec 03 2019 · 4 min read

Part 1 of this post is linked below:

It’s been a little over a year since the post above, and a lot of things can change in a year. Specifically, I’ve since moved to Kotlin as my primary language for Android development, and I’ve found a cleaner and more aesthetically pleasing way to create an expandable floating action button in Android; let’s talk more about the latter.

I’ll start by extolling the virtues of the Jetpack [DynamicAnimations](https://developer.android.com/jetpack/androidx/releases/dynamicanimation) library; animations are inherently stateful, transforming your View in one way or the other, and to maintain visual aesthetics, it’s important that animations are:

  • Smooth

  • Continuous

  • Reentrant

Which DynamicAnimations does with aplomb. If you haven’t already, please read Nick Butcher’s post on the same topic for more detail, it’s the inspiration behind the rewriting of the expandable FAB to use DynamicAnimations: Motional Intelligence: Build smarter animations *Recently at Google I/O, I presented some techniques for writing smarter animations in your Android applications…*medium.com

One of the benefits of DynamicAnimations is that whenever an animation is running to some end value, you can change that end value to some other arbitrary value, and the implementation will take care of adjusting parameters so that this change satisfies the 3 tenets above. The implementation of DynamicAnimations for FAB expansion and collapse will be the SpringAnimation, and again I’ll refer you to to another excellent post on the library and it’s internal workings, this time by Rebecca Frank: Android: Using Physics-based Animations in Custom Views (SpringAnimation) 👩🏻‍🔬 *Learn how to use physics-based animations in a Custom View implementation for natural looking animations in your app.*medium.com

Returning to the FAB, to use a SpringAnimation, the first thing that needs to be done is to define the property of the View, in this case a MaterialButton, to animate. Since the size of the FAB regardless of the ViewGroup it’s in is dependent on it’s LayoutParams, it makes sense to choose the LayoutParams as the property to mutate with the animation; specifically, its LayoutParams.width and LayoutParams.height.

At first glance, it may appear that since both the height and the width of the FAB will change with extension and collapse, twoSpringAnimations will be needed, but we can be cleverer than that. When collapsed, the FAB is a perfect circle, and so the width and height are identical. When extended however, the height of the FAB is flattened a bit, but it’s still fully deterministic; the only real variable is the width of the FAB as it changes to match the length of the text displayed within it. Going further, it is possible to express the height of the FAB as a function of it’s width with the classic equation of a straight line:

y = f(x) = mx + c

The line must go through the points:

  • x1, y1; where x1 = y1 = collapsedFabSize

  • x2, y2; where y2 = expandedFabHeight, and x2 is TBD as it depends on the length of the text.

  • **m **is the slope of the line

  • c is the y intercept

How is x2 determined then? Fortunately, it’s as easy as setting the text in the MaterialButton and asking it to measure itself. When all the above it put together, the property for animating the collapse and expansion of the FAB takes the form:

1 2private class SpringSizeInterpolator( 3 val button: MaterialButton, 4 val collapsedFabSize: Int, 5 val expandedFabHeight: Int 6) : FloatPropertyCompat<View>("FabExtensionSpring") { 7 8 val isRunning get() = spring.isRunning 9 10 private val x1 = collapsedFabSize 11 private val y1 = collapsedFabSize 12 private var y2 = expandedFabHeight 13 private var x2 = button.height 14 15 private val slope get() = if (x2 != x1) (y2 - y1) / (x2 - x1) else 0 16 17 private val intercept get() = y2 - (slope * y1) 18 19 private val spring = SpringAnimation(button, this, x1.toFloat()) 20 21 fun run(extended: Boolean) = button.doOnLayout { 22 val widthMeasureSpec = 23 if (extended) View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) 24 else View.MeasureSpec.makeMeasureSpec(collapsedFabSize, View.MeasureSpec.EXACTLY) 25 26 val heightMeasureSpec = 27 if (extended) View.MeasureSpec.makeMeasureSpec(expandedFabHeight, View.MeasureSpec.EXACTLY) 28 else View.MeasureSpec.makeMeasureSpec(collapsedFabSize, View.MeasureSpec.EXACTLY) 29 30 button.measure(widthMeasureSpec, heightMeasureSpec) 31 32 x2 = button.measuredWidth 33 y2 = button.measuredHeight 34 35 spring.animateToFinalPosition(x2.toFloat()) 36 } 37 38 fun attachToSpring(options: (SpringAnimation.() -> Unit)?) { 39 if (!isRunning) options?.invoke(spring) 40 } 41 42 private fun f(x: Float): Float = intercept + (slope * x) 43 44 override fun getValue(button: View): Float = button.width.toFloat() 45 46 override fun setValue(button: View, x: Float) = button.run { 47 layoutParams.width = x.toInt() 48 layoutParams.height = f(x).toInt() 49 requestLayout() 50 invalidate() 51 } 52}

Using this is as simple as calling:

Animating a FAB with a spring Animating a FAB with a spring

It is also important to remove the default insets of the MaterialButton and to give it a cornerRadius equal to the collapsedFabSize. You can change the cornerRadius programatically if you wish, unfortunately however, there isn’t a way to change the insets this way.

Remember to remove the insets for the MaterialButton Remember to remove the insets for the MaterialButton

By setting different damping ratios and stiffness on the spring, it is possible to achieve results from restrained, to exaggerations of cartoonish proportions.

Effects of different Spring properties of FAB extension and collapse

Effects of different Spring properties of FAB extension and collapse

If some visual flair is desired when the FAB mutates while collapsed, a spring can be used to create a halo effect by animating the MaterialButton’s stroke width if the contents are the same, or to animate it’s scale when it changes:

Subtler animations for FAB mutations when collapsed Subtler animations for FAB mutations when collapsed

As always, full source code for the above and sample app can be found below: