Overscroll AppBarLayout Behavior

Unfortunately, Youtube Music app is not available in my country and I tried to get apk from various piracy sites, but still I wasn’t able to look what’s going on in this app. Thanks to this redditor, who have opened a thread in /r/materialdesign and posted videos captures of Youtube Music app on my request I was able to see the behavior.

Actual YT Music app; Probably a Behavior

From what I could see, my first guess that the album art is inside a AppBarLayout and scaled when the content below overscrolls. Let’s assume this assumption is correct and attempt to express it in terms of Behavior. IMHO, if my assumptions are true, Google should include overscroll guidelines and specs in their Scrolling Techniques section of MD guidelines.

Our goal is to keep the AppBarLayout.Behavior intact and create our extended behavior on top of it. Therefore:

public class OverscrollScalingViewAppBarLayoutBehavior extends AppBarLayout.ScrollingViewBehavior

As it is default AppBarLayout.Behavior and suggests, we need to react only when our dependency view is AppBarLayout. No biggie:

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof AppBarLayout;
}

Next we need to obtain instance of the View that we are going to scale on overscroll. Best way is to do this is in onLayoutChild() method:

@Override
public boolean onLayoutChild(CoordinatorLayout parent ....) {
boolean superLayout = super.onLayoutChild(parent, abl, layoutDirection);
if (mTargetScalingView == null) {
mTargetScalingView = parent.findViewByTag(TAG);
if(mTargetScalingView != null){
mScaleImpl.obtainInitialValues();
}
}
return superLayout;
}

But also we need to react on vertical scrolling:

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,... int nestedScrollAxes) {
return nestedScrollAxes == View.SCROLL_AXIS_VERTICAL;
}

Let’s set ViewScaler as our default Scaler, if we haven’t set it programatically, previously.

Really important question is knowing the moment when content is overscrolling. Genuine CoordinatorLayout.Behavior offers a method named onNestedScroll(). This method is called when scrolling is ongoing, but also when we have overscroll. Last two parameters dyUnconsumed and dxUnconsumed, give us the amount of pixels that weren’t consumed by the target view of the behavior.

This method is really important for us to perform the scaling. So let’s list the cases when we should scale and when we should not:

Scale when:

1. There is unconsumed pixels, i.e dyUnconsumed < 0

2. AppBarLayout is expanded, getTopAndBottomOffset() >= mScaleImpl.getInitialParentBottom()

Don’t scale when:

1. We don’t have view to scale, which is child of AppBarLayout
2. There are consumed pixels, i.e dyConsumed != 0

@Override
public void onNestedScroll(CoordinatorLayout ... int dxUnconsumed, int dyUnconsumed) {
if (mTargetScalingView == null || dyConsumed != 0) {
mScaleImpl.cancelAnimations();
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
return;
}

if (dyUnconsumed < 0 && getTopAndBottomOffset() >= mScaleImpl.getInitialParentBottom()) {
int absDyUnconsumed = Math.abs(dyUnconsumed);
mTotalDyUnconsumed += absDyUnconsumed;
mTotalDyUnconsumed = Math.min(mTotalDyUnconsumed, mTotalTargetDyUnconsumed);
mScaleImpl.updateViewScale();
} else {
mTotalDyUnconsumed = 0;
mScaleImpl.setShouldRestore(false);
if (dyConsumed != 0) {
mScaleImpl.cancelAnimations();
}
super.onNestedScroll(coordinatorLayout, .... dxUnconsumed, dyUnconsumed);
}
}

When the nested overscroll has ended, we need to reset views to their original bounds and scales:

@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
mScaleImpl.retractScale();
super.onStopNestedScroll(coordinatorLayout, child, target);

}

ViewScaler

This class implements the logic how AppBarLayout should change its bottom and how view should scale. Mostly this behavior relies on cumulative unconsumed pixels. We can set bound value for maximum cumulative value and easily find how we are going to change AppBarLayout bottom and scale the target view. ParentScaler class which is superclass of ViewScaler is doing the (almost) smooth AppBarLayout expansion and retraction. I’ll spare you by not clog the post with code. If you are interested, you can grab entire code here.

Bonus

For the geeks, there is MatrixScaler private class, which I haven’t time to finish it. This class is supposed scale the image Matrix, if the view that suppose to scale is ImageView with ScaleType.MATRIX.

Demo: