This article is the introduction of the method in the article + small examples, I learned the extension of Behavior, will use a variety of Behavior ability, rely on layout nesting sliding, etc., to achieve a common nesting mechanism. The target effect is as follows:

Layout Settings

First, set up the XML layout, and the interface has only two elements, TextView and RecyclerView. TextView is simple, just setting the background, nothing else is set. RecyclerView is also relatively simple, is a common list, the specific Adapter is no longer described.


      
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.TouchActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#0000FF" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00FFFF"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        tools:itemCount="10"
        tools:listitem="@layout/item_recycler" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code

If you run it directly, you don’t see the TextView. Because CoordinatorLayout is like a FrameLayout without the Behavior, the TextView is overwritten by the RecyclerView and you can’t see anything.

Behavior

To avoid overwriting, multiple behaviors will be defined to achieve the desired effect step by step.

ToBottomBehavior leaves space for the TextView

First of all, CoordinatorLayout by default has all the child views stacked in the top left corner, so if you want to display the TextView, you need to move the RecyclerView down to the height of the TextView. Therefore, you need to rewrite the Behavior’s onLayoutChild method and then redo the layout.

class ToBottomBehavior(
    context: Context,
    attr: AttributeSet? = null
    // Use RecyclerView as a generic type
) : CoordinatorLayout.Behavior<RecyclerView>(context, attr) {

    override fun onLayoutChild(
        parent: CoordinatorLayout,
        child: RecyclerView,
        layoutDirection: Int
    ): Boolean {
        // The layout is not done when it is below two child views
        if (parent.childCount < 2) {
            return false
        }
        val firstView = parent.getChildAt(0)
        // Measure height is match_parent height,
        // After the actual layout, the height becomes match_parent minus the height of firstView
        child.layout(0, firstView.measuredHeight, child.measuredWidth, child.measuredHeight)
        return true}}Copy the code

Then change the Behavior to RecyclerView:

<androidx.coordinatorlayout.widget.CoordinatorLayout...>
    
    <TextView ./>
    
    <androidx.recyclerview.widget.RecyclerView . 
      	app:layout_behavior=".behavior.ToBottomBehavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code

The TextView should now be displayed:

TouchBehavior Turns on nesting sliding

So we’ve got TextView on top and RecyclerView on top. The next thing you need to do is to make TextView roll up and out of the screen when scrolling: and then continue scrolling RecyclerView.

This task is done by RecyclerView scroll drive TextView scroll up, obviously is nested slide. First, assign slides to TextView so that TextView doesn’t consume scrolling events when it rolls off the screen, and then RecyclerView to scroll.

class TouchBehavior(
    context: Context,
    attr: AttributeSet? = null
) : CoordinatorLayout.Behavior<View>(context, attr) {

    override fun onStartNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        directTargetChild: View,
        target: View,
        axes: Int,
        type: Int
    ): Boolean {
        // Only vertical scrolling is blocked
        return axes == ViewCompat.SCROLL_AXIS_VERTICAL
    }

    override fun onNestedPreScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        target: View,
        dx: Int,
        dy: Int,
        consumed: IntArray,
        type: Int
    ) {
        // Note that dy is greater than 0 when you swipe up. Dy is less than 0 when we go down.
        val translationY = child.translationY
        if (-translationY >= child.measuredHeight || dy < 0) {
            // Child is already scrolled out of the screen, or scrolling down does not consume scrolling
            return
        }
        // The desireHeight distance will move out of the screen
        val desireHeight = translationY + child.measuredHeight
        if (dy <= desireHeight) {
            // use up all the dy
            child.translationY = translationY - dy
            consumed[1] = dy
        } else {
            // Consume part of the dy
            child.translationY = translationY - desireHeight
            consumed[1] = desireHeight.toInt()
        }
    }

}
Copy the code

Notice that the onNestedScrollAccepted method is omitted, because this is simpler and doesn’t require any initialization prefixes. Then set it to TextView, because the trigger of nested slides is RecyclerView, and the parent is TextView. So we need to set this Behavior to the TextView.

<androidx.coordinatorlayout.widget.CoordinatorLayout .>

    <TextView .
        app:layout_behavior=".behavior.TouchBehavior"/>
    
    <RecyclerView ./>
    
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code

As you can see from the figure above, nested slides are beginning to take shape. I scroll TextView, and then I scroll RecyclerView. The problem is that TextView and RecyclerView are separated when scrolling, leaving a large space in the middle. Therefore, we need to continue to optimize the RecyclerView to scroll up while TextView is scrolling.

This situation should belong to the dependent function of Behavior, that is, RecyclerView depends on TextView and is always under TextView. Therefore, this operation should be in the RecyclerView Behavior.

class ToBottomBehavior(..){
	...

    override fun layoutDependsOn(
        parent: CoordinatorLayout,
        child: RecyclerView,
        dependency: View
    ): Boolean {
        return dependency === parent.getChildAt(0)}override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: RecyclerView,
        dependency: View
    ): Boolean {
        child.translationY = dependency.translationY
        return true}}Copy the code

The layoutDependsOn method is overridden in ToBottomBehavior and only relies on the first child View. Then move the position of the child via tranlationY in the onDependentViewChanged method. Although this is also possible, there is a problem that when the RecyclerView moves up, there is a blank space at the bottom.

This is because the height of the RecyclerView changed to Match_parent minus the height of the TextView when rewriting the custom layout. So when it moves up, it doesn’t have enough height and it has a blank space at the bottom. Therefore, the onDependentViewChanged method does not only change the position, but also change the height.

class ToBottomBehavior(...) {

    ...

    override fun onDependentViewChanged(
        parent: CoordinatorLayout,
        child: RecyclerView,
        dependency: View
    ): Boolean {
// child.translationY = dependency.translationY
        child.layout(
            0,
            (dependency.bottom + dependency.translationY).toInt(),
            child.measuredWidth,
            child.measuredHeight
        )
        return true}}Copy the code

In this case, when TextView position changes, RecyclerView layout will be changed, so the height will also change, so that it can always fill the parent layout without leaving the bottom blank, the effect is as follows.

At this time, our requirements have been generally met, and the sliding is continuous. After TextView is rolled out of the screen, RecyclerView will be followed, without pause. There is a problem with RecyclerView, when the RecyclerView scrolls to the top/bottom, it should display an OverScroll (a blue arc), but it doesn’t.

This is because, in nested sliding events, the sliding event is from Child ->parent-> Child ->parent. Behavior acts as the parent. We start with a scroll event, passed from the Child to the parent, and then we handle the move up of the TextView in the parent, and then we pass the event to the Child for scrolling, and when the Child scrolls to the bottom and can’t scroll anymore, the event is passed to the parent. However, we are not handling this event (onNestedScroll) because the default implementation will be as follows:

public void onNestedScroll(
    @NonNull CoordinatorLayout coordinatorLayout, 
    @NonNull V child,
    @NonNull View target, 
    int dxConsumed, 
    int dyConsumed, 
    int dxUnconsumed,
    int dyUnconsumed, 
    @NestedScrollType int type, 
    @NonNull int[] consumed
) {
    consumed[0] += dxUnconsumed;
    consumed[1] += dyUnconsumed;
    onNestedScroll(coordinatorLayout, child, target, dxConsumed, 
        dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
Copy the code

In the default implementation, all events are consumed, and after the child can’t consume the scroll event (scroll to the bottom), the remaining events are consumed by the parent. In the case of the Child, the event stream stops when the child scrolls to the bottom, so the child doesn’t OverScroll. So, we should also rewrite onNestedScroll and give it an empty implementation to get the OverScroll effect.

Also, the TextView at the top is gone forever after it scrolls out. We now also want to roll TextView out again when the RecyclerView scrolls to the top and then continues scrolling. And that’s when you should be operating in onNestedScroll.

class TouchBehavior(...) {

    ...

    override fun onNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: View,
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int,
        consumed: IntArray
    ) {
        val translationY = child.translationY
        if (translationY >= 0 || dyUnconsumed > 0) {
            // Finger scroll up or child has already rolled off the screen
            return
        }
        if (dyUnconsumed > translationY) {
            // All consumed
            consumed[1] += dyUnconsumed
            child.translationY = translationY - dyUnconsumed
        } else {
            // Consume part of it
            consumed[1] += child.translationY.toInt()
            child.translationY = 0F}}}Copy the code

The modified picture is as follows:

The full code is here: Click the jump to Github link

conclusion

Complete touch event rewriting is cumbersome, so we rarely override such events, instead using already encapsulated sliding events. Of course, nested sliding is also more troublesome, because to implement a nested sliding parent or child first need to customize the View, and deal with the troublesome sliding relationship, also need to deal with the measurement layout of the child View, etc..

CoordinatorLayout already realizes these things that we don’t pay much attention to, and extracts all the parent functions in the nesting sliding process into the Behavior, so we don’t need to customize the View, we just need to set a Behavior for the View, You can easily let it slide as a nested parent, even if it doesn’t implement the NestedScrollingParent3 interface.

The order of nested sliding is: Child initiate -> parent first -> Child then -> parent then -> Return child

As the parent, we need to process two slide events, one is the initial preprocessing (onNestedPreScroll), and one is the child processing and then passed (onNestedScroll), so we need to determine the current stage of the operation.