Picture-in-picture video overlay with MotionLayout

Raf Willems
VRT Digital Products
6 min readOct 1, 2018

One of the many things at Google I/O 2018 that got me excited, is the MotionLayout.

When the opportunity came to build a YouTube-style video overlay player, me and the rest of the Android team @ VRT NWS (Belgian Public Media Service) couldn’t wait to test out Google’s new addition to the support library.

The concept of our player is to only be available during important events to broadcast live video, while still enabling the users to browse through other news articles. One of these events, for example, will be streaming the live TV-show where the results of the local elections in Belgium are communicated and debated.

Finished video overlay in VRT NWS

During the development of this neat feature, we got a first-hand experience of MotionLayout’s strengths but we also ran into challenges and pitfalls along the way.

In this post, I describe some of the things we encountered in our journey while building this new feature.

The example code used in this post can be found on Github.

MotionLayout

First things first, if you are not familiar withMotionLayout, or what purpose it serves, I suggest checking out the Introduction to MotionLayout by Google Developers @camaelon and @johnhoford and their presentation on Google I/O.

Creating a simple VideoView with Swipe behavior

In a first attempt at creating our video experience, we created a small MotionLayout that would transition to a large full screen sized layout with its elements in different positions.

Guided by the introduction, we created XML files for the small player, fullscreen player and linked both together with a third XML describing the MotionScene with OnSwipe behavior.

Avoid resizing the boundaries of the MotionLayout

When trying to animate both the content and the size of our video overlay, we ran into our first problem.

The MotionScene tried to reposition its item from the startScene to match the constraints of the endScene , in our case still wrapping its content and causing some unwanted results.

Wrapping the content of the video player caused unwanted behavior

Another option we considered, was programmatically animating the size of the motionLayout ourselves. This added an extra layer of complexity, breaking the idea of using MotionLayout in the first place to manage transitions plus messed up the animation as well. We had to look for another solution.

Transition a transparent fullscreen overlay into an overlay the same size

In order to make sure our initial VideoPlayerView had the same size as the resulting fullscreen video, we added a transparent background. The result was promising and started to show some of the power of MotionLayout.

Propagate clicks to the RecyclerView below

Since we still want to scroll through the feed behind the video, we need to find a way to pass on these click and scroll events.

For this, we extended MotionLayout to add this custom behavior.

class VideoOverlayView @JvmOverloads constructor(
context: Context,
attrs:AttributeSet? = null,
defStyleAttr: Int = 0)
: MotionLayout(context, attrs, defStyleAttr) {
...
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean =
if(touchEventInsideTargetView(touchableArea, ev)){
super.onInterceptTouchEvent(ev)
} else {
true
}
...}

The complete code can be found in our example on Github.

Now we can scroll both the list and swipe the video to full screen. Great!

The animation stops when our swipe gesture reaches the edge of the video overlays touchable area.

When swiping up the video too fast, a touch could catch up with the animating target area, resulting in the animation coming to an early stop.

Once the animation has started, we want to be able to finish the swipe up. For this, we have to add an extra check to verify the animation state in the touch interceptor.

...override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val isInProgress = (motionLayout.progress > 0.0f
&& motionLayout.progress < 1.0f)
val isInTarget = touchEventInsideTargetView(
touchableArea, ev
)
return if (isInProgress || isInTarget) {
super.onInterceptTouchEvent(ev)
} else {
true
}
}
...

So far so good! 👌

The animation runs smoothly, time to tweak it some more to our liking!

Finetuning the animation behavior

One of the things we want to fix is the video player that overlaps the title during the animation.

This is where MotionLayout really comes to shine.

As described in the introduction to MotionLayout, keyframes are used for different elements at different points in the animation.

The added keyframes make it so the video and title do not overlap.

To see the transition path, add app:showPaths=”yes” to your MotionLayout XML.

<KeyFrameSet>

<
KeyPosition
motion:framePosition="10"
motion:keyPositionType="deltaRelative"
motion:percentX="0"
motion:sizePercent="0"
motion:target="@id/video_overlay_thumbnail"/>

<
KeyPosition
motion:framePosition="10"
motion:keyPositionType="deltaRelative"
motion:percentX="0"
motion:percentY="0.4"
motion:sizePercent="0"
motion:target="@id/video_overlay_title"/>

</
KeyFrameSet>

First, we set the size of the video to stay small until 10 percent in the animation and for the video to only move up.

For the title, we do 40% of the animation distance in the Y direction in only 10% of the time.

All we had to do was adding a minimum description to the MotionScene XML and MotionLayout did the rest! Cool!

Try playing around and have some fun, I know I did, and I’m not the only one.

With only a few lines of code you can make the view overshoot, rotate 180 degrees or make any translation you can think of!

Have both OnClick and OnSwipe

Up until now, we only used the OnSwipe behavoir defined in our MotionLayout. Ideally, we want the user to be able to use both mechanisms to trigger the animation.

When adding both behaviors to the XML this did not work. Assumably this is caused by the way MotionLayout handles and consumes touch events internally.

We came up with a solution, by first defining a clickable target View in our MotionLayout that should trigger the animation both OnClick and OnSwipe. In our GitHub example, this is the video_player_thumbnail.

Before dispatching the TouchEvent to the view, we detect if the motion event received is a click inside our target view. If this is the case, we initialize the animation programmatically. If this is not the case, we dispatch the event like normal and handle it as before in onInterceptTouchEvent.

This means clicks inside our target view are captured in the dispatchTouchEvent function where they trigger an animation, while SwipeEvents and Clicks elsewhere are still dispatched to the MotionLayout to be handled as before.

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (touchEventInsideTargetView(clickableArea, ev)) {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
startX = ev.x
startY = ev.y
}
MotionEvent.ACTION_UP -> {
val endX = ev.x
val endY = ev.y
if (isAClick(startX!!, endX, startY!!, endY)) {
if (doClickTransition()) {
return true
}
}

}
}
}
return super.dispatchTouchEvent(ev)
}

Again, find the full code on GitHub.

Conclusion

MotionLayout is great, at the time of writing this article, still in alpha but already proving to be a powerful tool.

To get to the behavior we wanted, there still was a good amount of extra code to write, as well as some hiccups in certain animation patterns we tried along the way. Hopefully, Google will be able to improve on those.

With MotionLayout available, being scared by the amount of code we once had to write to come up with nice animations, is now more than ever a thing of the past.

--

--

Raf Willems
VRT Digital Products

Trying to make the world a better place, one bug at a time.