Original Zhanghao Baidu App technology

1. Multiple sub-Views nested application backgrounds

In the version of baidu App in 2017, two sub-views are nested to scroll, which is used for the landing page of Feed (WebView presents article details and Recycle presents Native comments). The principle is to provide a UI container in the outer layer (we call it “linkage container”) to handle the continuous nested scrolling of WebView and Recyclerview.

At that time, linkage container had relatively large restrictions on sub-views, only supporting linkage scrolling of WebView and Recyclerview, and only supporting 2 sub-views in number.

With the advancement of componentization process, in order to facilitate the decoupling of all services, higher requirements are put forward for linkage container, which needs to support any type and any number of sub-views for linkage rolling, which is the multi-sub-View nested rolling universal solution to be elaborated in this paper.

First, feel the Demo effect of nested scrolling for linkage containers:

2. Implementation principle of multi-sub-view nesting

Like most custom controls, the linkage container handles the measurement, layout, and gesture handling of child views. Measurement and layout are simple for the linkage container scenario, whereas gesture processing is more complex.

As can be seen from the demo, the linkage container needs to handle the sliding problem of nesting with child views. Nested sliding can be handled in two ways

  • NestedScrolling based on Google’s NestedScrolling mechanism;
  • Is a nested sliding logic by interlocking container internal processing and child views.

The linkage container in the early version of Baidu App adopts Scheme 2 to realize the gesture processing process of linkage container in Scheme 2 as shown below:

Github.com/baiduapp-te…

3. Core logic

3.1 Google nested sliding mechanism

Google introduced a set of NestedScrolling mechanism in Android 5.0, which breaks the traditional event processing cognition of Android and handles NestedScrolling according to the reverse event relay mechanism. For event scrolling, please refer to the following figure:

Blog.csdn.net/lmj62356579…

3.2 Interface Design

In order to ensure the arbitrariness of the sub-view in the linkage container, the linkage container should provide a perfect interface abstraction for the sub-view to implement. The following figure shows the interface class diagram exposed by the linkage container:

LinkageScrollHandler interface method linkage container will be called when necessary, to notify the child view to complete some functions, such as: obtain whether the child view can scroll, obtain the child view scroll bar related data, etc..

The ChildLinkageEvent interface defines some event information for the child view, such as the content of the child view scrolling to the top or bottom. When these events occur, the sub-view actively calls the corresponding method, so that the linkage container will make corresponding response after receiving some events of the sub-view, to ensure the normal linkage effect.

The above is only a brief description of the interface functions, for more in-depth understanding of the students please refer to: github.com/baiduapp-te…

Next, we will analyze the gesture processing details of the linkage container in detail. According to the gesture type, the nested sliding will be divided into two cases for analysis: 1. Scroll gesture; Fling 2.

3.3 scroll gestures

First give scroll gesture processing core code:

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent { @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { boolean moveUp = dy > 0; boolean moveDown = ! moveUp; int scrollY = getScrollY(); int topEdge = target.getTop(); LinkageScrollHandler targetScrollHandler = ((ILinkageScroll)target).provideScrollHandler();if(scrollY == topEdge) {// The linkage container scrollY coincides with the top coordinate of the current child viewif((moveDown && ! targetScrollHandler.canScrollVertically(-1)) || (moveUp && ! TargetScrollHandler. CanScrollVertically (1))) {/ / in the corresponding sliding direction, if the child view cannot vertical sliding, rolling distance by associative containers consumption scrollBy (0, dy); consumed[1] = dy; }}else if(scrollY > topEdge) {// The scrollY container is larger than the top coordinate of the current child view, that is, the child view head has slid out of the containerif(moveUp) {// If the finger is swiped up, the scrollBy(0, dy) is consumed by the container; consumed[1] = dy; }if(moveDown) {// If the finger slides down, the linkage container will consume part of the distance first, and the scrollY of the linkage container will decrease continuously, // Until it is equal to the top coordinate of the child view, the remaining sliding distance will be consumed by the child view. int end = scrollY + dy; int deltaY; deltaY = end > topEdge ? dy : (topEdge - scrollY); scrollBy(0, deltaY); consumed[1] = deltaY; }}else if(scrollY < topEdge) {// The linkage container scrollY is smaller than the top coordinate of the current child view, that is, the child view is not fully exposedif(moveDown) {// If the finger is down, the movement distance is scrollBy(0, dy); consumed[1] = dy; }if(moveUp) {// If the finger is swiped up, the linkage container will consume part of the distance first, and the scrollY of the linkage container will increase continuously, // Until it is equal to the top coordinate of the child view, the remaining slider distance will be consumed by the child view. int end = scrollY + dy; int deltaY; deltaY = end < topEdge ? dy : (topEdge - scrollY); scrollBy(0, deltaY); consumed[1] = deltaY; }} @override public void scrollBy(int x, int y) { int deltaY;if (y < 0) {
            deltaY = (scrollY + y) < 0 ? (-scrollY) : y;
        } else {
            deltaY = (scrollY + y) > mScrollRange ?
                    (mScrollRange - scrollY) : y;
        }
        if(deltaY ! = 0) { super.scrollBy(x, deltaY); }}}Copy the code

The onNestedPreScroll() callback is a method in the NestedScrollingParent interface of Google’s nested sliding mechanism. When the child view rolls, it first asks the parent view whether to consume the rolling distance through this method. The parent view decides whether and how much to consume according to its own situation, and puts the consuming distance into the array consumed. The child View then decides its rolling distance according to the content in the array.

By comparing the upper edge threshold of the sub-view with the scrollY of the linkage container, we deal with the scrolling situation under three cases.

In line 10, when scrollY == topEdge, as long as the child view does not scroll to the top or bottom, the child view will normally consume the scrolling distance; otherwise, the linked container will consume the scrolling distance and inform the child view of the consumed distance through the consumed variable. The child view determines its own slide distance based on the content in the Consumed variable.

In line 17, when scrollY > topEdge, that is to say, when the head of the touch sub-view has slid out of the linkage container, if the finger slides up, the sliding distance will be consumed by the linkage container. If the finger slides down, the linkage container will consume part of the distance first. When the scrollY of the linkage container reaches topEdge, The remaining sliding distance is consumed by the sub-view.

Line 32, when scrollY is less than topEdge this is similar to the last one, line 17, so I won’t explain too much here. Scroll gesture processing flow chart is as follows:

3.4 fling gestures

If the scrollY is equal to the top coordinate of the child view, the child view handles the fling. If the scrollY is not equal to the top coordinate of the child view, the child view handles the fling.

The view slides back and forth until the speed drops to zero. The view slides back and forth until the speed drops to zero. Let’s look at the detailed implementation:

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        int scrollY = getScrollY();
        int targetTop = target.getTop();
        mFlingOrientation = velocityY > 0 ? FLING_ORIENTATION_UP : FLING_ORIENTATION_DOWN;
        if(scrollY == targetTop) {// If the scrollY is equal to the top coordinate of the child view, the child view handles the velocity gesture. Keep the parent trackVelocity(velocityY);return false;
        } else{// Consume the fling gesture parentFling(velocityY);return true; }}}Copy the code

The onNestedPreFling() callback is a method in the NestedScrollingParent interface of Google’s nested sliding mechanism. The child has its own fling. If the view returns true, it wants to spend the fling. If the child does not fling, the view does not want to spend the fling.

In line 6, the fling direction is recorded according to the velocityY positive and negative values.

In the 7th line, the scrollY value is equal to the top value of the child view and the fling gesture is processed by the child view. The purpose of the fling gesture is to keep the scroll speed when the child view rolls to the top or bottom.

In line 12, the fling gesture is consumed by the linkage container. Let’s look at the details of the interlocking container and child fling.

public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {
    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()) { int y = mScroller.getCurrY(); y = y < 0 ? 0 : y; y = y > mScrollRange ? mScrollRange : y; Int edge = getNextEdge(); int edge = getNextEdge(); // boundary checkif(mFlingOrientation == FLING_ORIENTATION_UP) { y = y > edge ? edge : y; } // boundary checkif(mFlingOrientation == FLING_ORIENTATION_DOWN) { y = y < edge ? edge : y; } // View scrollTo(x, y); int scrollY = getScrollY(); // Whether the latest scrollY of the linkage container reaches the boundary valueif(scrollY = = edge) {/ / get the rest of the speed int velocity = (int) mScroller. GetCurrVelocity ();if (mFlingOrientation == FLING_ORIENTATION_UP) {
                    velocity = velocity > 0? velocity : - velocity;
                }
                if(mFlingOrientation == FLING_ORIENTATION_DOWN) { velocity = velocity < 0? velocity : - velocity; } // Get top edge child view target = getTargetByEdge(edge); // The view continues to fling ((ILinkageScroll) target).providesCrollHandler ().flingContent(target, velocity); trackVelocity(velocity); } invalidate(); }} /** * Get the next scroll boundary of the View from the direction of the scroll and determine whether the next View is isScrollablefalse, the edge of the next target will be removed. */ private intgetNextEdge() {
        int scrollY = getScrollY();
        if (mFlingOrientation == FLING_ORIENTATION_UP) {
            for (View target : mLinkageChildren) {
                LinkageScrollHandler handler
                        = ((ILinkageScroll)target).provideScrollHandler();
                int topEdge = target.getTop();
                if (topEdge > scrollY
                        && isTargetScrollable(target)
                        && handler.canScrollVertically(1)) {
                    returntopEdge; }}}else if (mFlingOrientation == FLING_ORIENTATION_DOWN) {
            for (View target : mLinkageChildren) {
                LinkageScrollHandler handler
                        = ((ILinkageScroll)target).provideScrollHandler();
                int bottomEdge = target.getBottom();
                if (bottomEdge >= scrollY
                        && isTargetScrollable(target)
                        && handler.canScrollVertically(-1)) {
                    returntarget.getTop(); }}}returnmFlingOrientation == FLING_ORIENTATION_UP ? mScrollRange : 0; } /** * Child view */ private ChildLinkageEvent mChildLinkageEvent = newChildLinkageEvent() {@override public void onContentScrollToTop(View target) {// The child View content scrolls to the top callbackif(mVelocityScroller.com puteScrollOffset ()) {/ / residual velocity obtained from the speed trackingfloatcurrVelocity = mVelocityScroller.getCurrVelocity(); currVelocity = currVelocity < 0 ? currVelocity : - currVelocity; mVelocityScroller.abortAnimation(); // Fling parentFling(currVelocity); }} @override public void onContentScrollToBottom(View target) {// The child View content scrolls to the bottom callbackif(mVelocityScroller.com puteScrollOffset ()) {/ / residual velocity obtained from the speed trackingfloatcurrVelocity = mVelocityScroller.getCurrVelocity(); currVelocity = currVelocity > 0 ? currVelocity : - currVelocity; mVelocityScroller.abortAnimation(); // Fling parentFling(currVelocity); }}}; }Copy the code

The speed of fling is divided into:

  1. Pass from the linkage container to the child view; 2. Pass from the child view to the linkage container.

Let’s look at the velocity passing from the linkage container to the child view. The core code is in the computeScroll() callback method. In line 9, get the next scroll boundary value of the linkage container. If the next scroll boundary value is reached, the linkage container needs to pass the remaining speed to the next child view so that it can continue to scroll.

In line 46, the getNextEdge() method iterates through all child Views and compares the current scrollY of the linkage container to the top/bottom of the child view to get the next sliding boundary.

Line 34 when associative container inspection to the border of the slide to the next, call the ILinkageScroll. FlingContent () to let the son view according to the residual velocity continue to roll.

Looking at speed passing from the child view to the linkage container, the core code is on line 76. When the child view scrolls to the top or bottom, the onContentScrollToTop() or onContentScrollToBottom() methods are called back. The container receives the callback and continues to scroll at lines 86 and 98. The fling gesture processing flow chart is as follows:

4. The scroll bar

4.1 ScrollBar for Android

For scrollable pages, ScrollBar is an indispensable UI component, so ScrollBar is also a necessary function of linkage container.

The good news is that Android is very friendly to scroll bar abstraction, and custom controls only need to override a few methods in View, and Android will help you draw the scroll bar correctly. Let’s first look at the related methods in View:

/**
 * <p>Compute the vertical offset of the vertical scrollbar's thumb within the horizontal range. This value is used to compute the position * of the thumb within the scrollbar's track.</p>
 *
 * <p>The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollRange()} and
 * {@link #computeVerticalScrollExtent()}.</p>
 *
 * @return the vertical offset of the scrollbar's thumb */ protected int computeVerticalScrollOffset() { return mScrollY; } /** * 

Compute the vertical extent of the vertical scrollbar'

s thumb within the vertical range. This value is used to compute the length * of the thumb within the scrollbar's track.

* *

The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollRange()} and * {@link #computeVerticalScrollOffset()}.

* * @return the vertical extent of the scrollbar'
s thumb */ protected int computeVerticalScrollExtent() { return getHeight(); } /** * <p>Compute the vertical range that the vertical scrollbar represents.</p> * * <p>The range is expressed in arbitrary units that must be the same as the units used by {@link #computeVerticalScrollExtent()} and * {@link #computeVerticalScrollOffset()}.</p> * * @return the total vertical range represented by the vertical scrollbar */ protected int computeVerticalScrollRange() { return getHeight(); } Copy the code

For the vertical Scrollbar, we only need to rewrite computeVerticalScrollOffset (), computeVerticalScrollExtent (), computeVerticalScrollRange () of the three methods. Android notes these three methods in great detail, so here’s a quick explanation:

ComputeVerticalScrollOffset () said that the current page content scrolling offset value, the value is used to control the position of the Scrollbar. The default value is the scroll value in the Y direction of the current page.

ComputeVerticalScrollExtent () said the scope of the scroll bar is a scroll bar in vertical direction can reach maximum limit, the value will be used to calculate the length of the scroll bar. The default value is the actual height of the View.

ComputeVerticalScrollRange () said the contents of the entire page scrolling numerical range, the default value is the actual height of the View.

Note that the offset, extent, and range values must be the same in units.

4.2 ScrollBar is realized by Associating containers

Linkage container is composed of scrollable sub-views in the system, these sub-views (ListView, RecyclerView, WebView) must realize the ScrollBar function, then linkage container ScrollBar is very simple, The linkage container only needs to get the offset, extent, and range values of all child views, and then convert these values of all child views to the corresponding offset, extent, and range values of the linkage container according to the sliding logic of the linkage container. The interface design is as follows:

public interface LinkageScrollHandler { // ... /** * get scrollbar extent value ** @return extent
     */
    int getVerticalScrollExtent();
    /**
     * get scrollbar offset value
     *
     * @return extent
     */
    int getVerticalScrollOffset();
    /**
     * get scrollbar range value
     *
     * @return extent
     */
    int getVerticalScrollRange();
}
Copy the code

The LinkageScrollHandler interface is explained in section 3.2 and will not be described here. The three methods are implemented by the child view, and the linkage container will get the value of the child view and the scrollbar through these three methods. Let’s look at the detailed logic of the ScrollBar in the linkage container:

Public class ELinkageScrollLayout extends ViewGroup implements NestedScrollingParent {/** constructor */ public ELinkageScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { // ... Extraneous code omitted // Make sure the linkage container calls the onDraw() methodsetWillNotDraw(false);
        // enable vertical scrollbar
        setVerticalScrollBarEnabled(true); } /** Child view scroll event */ private ChildLinkageEvent mChildLinkageEvent = newChildLinkageEvent() {/ /... @override public void contentScroll (View target) {awakenScrollBars(); } } @Override protected intcomputeVerticalScrollExtent() {// Use the default extent valuereturn super.computeVerticalScrollExtent();
    }
    @Override
    protected int computeVerticalScrollRange() { int range = 0; // Go through all the child views and get the Range of the child viewsfor (View child : mLinkageChildren) {
            ILinkageScroll linkageScroll = (ILinkageScroll) child;
            int childRange = linkageScroll.provideScrollHandler().getVerticalScrollRange();
            range += childRange;
        }
        return range;
    }
    @Override
    protected int computeVerticalScrollOffset() { int offset = 0; // Iterate over all child views to get the offset of the child viewsfor(View child : mLinkageChildren) { ILinkageScroll linkageScroll = (ILinkageScroll) child; int childOffset = linkageScroll.provideScrollHandler().getVerticalScrollOffset(); offset += childOffset; } // Offset += getScrollY();returnoffset; }}Copy the code

The above is the core code of the linkage container to implement ScrollBar. The notes are also very detailed. Here are some key points:

By default, ViewGroup does not call onDraw() to improve efficiency, so it does not follow the drawing logic of ScrollBar. So in line 6, we call setWillNotDraw(false) to open the ViewGroup drawing process;

Line 16: After receiving the scroll callback from the child view, call awakenScrollBars() to trigger the drawing of the scroll bar;

For extent, use the default extent, that is, the height of the linkage container.

For range, sum the range of all sub-views, and finally get the value is the range of linkage container;

For offset, sum the offset of all sub-views first, and then add the scrollY value of the linkage container itself. The final value is the offset of the linkage container.

You can go back to the beginning of the article to see the effect of the scrollbar in the Demo. Compared to other apps on the market that use similar linkage technology, the implementation of the scrollbar in this article is very close to native.

5. Precautions

The OverScroller tool class is used to execute the fling. The code is as follows:

private void parentFling(floatvelocityY) { // ... Mscroll. fling(0, getScrollY(), 0, (int) velocityY, 0, 0, Integer.min_value, integer.max_value); invalidate(); }Copy the code

. With the help of OverScroller fling the fling behavior of linkage container () method, this code is run on millet mobile phone linkage will appear problem, mScroller. GetCurrVelocity () is always zero.

Because the millet mobile phone Rom rewrite the OverScroller when fling () method is the third parameter transmission zero, OverScroller. Have been to NaN mCurrVelocity, unable to calculate the residual velocity right.

In order to solve the problem of Xiaomi phone, we need to pass the third parameter to a non-zero value, which is 1.

private void parentFling(floatvelocityY) { // ... Mscroll. fling(0, getScrollY(), 1, (int) velocityY, 0, 0, Integer.min_value, integer.max_value); invalidate(); }Copy the code

6. Summary

The realization principle of multi-sub-view nesting is not complicated, and the boundary conditions of gesture processing are trivial. It needs to be debuggable and perfected back and forth. Welcome friends in the industry to exchange and learn together.

Sample address: github.com/baiduapp-te…