ConsecutiveScrollerLayout is my a in making open source Android custom slide layout, it can make multiple slide layout and normal controls on the interface as a whole continuous sliding.

Imagine that we have such a requirement, on an interface, there is a rotation chart, a classified layout like a nine-grid, several lists of different styles, and in the middle are mixed with various advertising pictures and the layout of displaying various activities, such a design is very common on the home page of large apps. Another example is a WebView with a native review list, recommendation list and advertising space, such as an article details page for consulting or a product details page for e-commerce. This kind of complex layout is often difficult to implement, and it requires high sliding smoothness of the page and display efficiency of the layout. Before I met this kind of complex layout, can I use in making open source project GroupedRecyclerViewAdapter implementation. GroupedRecyclerViewAdapter was designed, which is in order to make secondary RecyclerView easily lists, lists and displayed on a RecyclerView different list. GroupedRecyclerViewAdapter support set different item types of the head, tail, and children, all it can on a RecyclerView display a variety of different layout and list, also accord with the demand of complex layout. But because GroupedRecyclerViewAdapter is not designed for the complex layout, use it to achieve this layout, need to users on the subclass of GroupedRecyclerViewAdapter manage data and various types of layout of the page display logic, heavy look trouble again. If it is not integrated in a RecyclerView, but the nesting implementation of the layout, not only seriously affect the performance of the layout, but also to solve the sliding conflict is a headache. Although Google introduced NestedScrolling in Android 5.0 to better handle scrolling conflicts, it’s still not easy to handle scrolling on your own.

No matter how complex a page is, it is made up of small controls. If we can have a layout container that handles the sliding of all the child views in the layout, so that both the normal controls and the sliding layout can be slid in the container as a whole, just like sliding a normal ScrollView. So we don’t have to worry about layout sliding conflicts and sliding performance anymore. No matter how complex the layout, we only need to think about the small parts of the layout and use what controls, any complex layout will not be complicated. ConsecutiveScrollerLayout designed based on this demand.

Design ideas

ConsecutiveScrollerLayout in conception, I was considering using NestedScrolling mechanism to realize, but later I gave up the plan, there are two main reasons:

1. NestedScrolling coordinates parent and child scroll conflicts, distributes scroll events, and allows them to scroll independently. This is not what I want to put ConsecutiveScrollerLayout slide of all child View as a whole idea, I hope the content of the traverse to the child View as part of ConsecutiveScrollerLayout content, Both in ConsecutiveScrollerLayout itself and its View, unified handling by ConsecutiveScrollerLayout sliding event.

2. NestedScrolling requires the parent to implement NestedScrollingParent, and all sliding child views to implement NestedScrollingChild. And I hope ConsecutiveScrollerLayout on using as much as possible without limitation, any View in it can be a very good job, and the child View do not need to care about how it is sliding.

After rejecting the NestedScrolling mechanism, I tried to find a breakthrough point by sliding the content of the View. I’ve noticed that almost all Android views slide their contents through the scrollBy() -> scrollTo() method, and most sliding layouts call this method either directly or indirectly. So these two methods are the entry point for handling layout sliding, and you can rewrite them to redefine the layout sliding logic.

Specific ideas by intercepting can slide as part of the view of sliding, prevented it from sliding, and the event was a ConsecutiveScrollerLayout processing, ConsecutiveScrollerLayout rewrite scrollBy (), scrollTo () method, the scrollTo () method of distribution of sliding offset by calculation, decisions are made by itself or specific consumption View of sliding distance, Call its super.scrollto () and its child’s scrollBy() to slide the contents of its and child views.

Say so many, let us through code, analyze ConsecutiveScrollerLayout is how to implement. The code given below is some of the main snippet of the source code, deleted some of the processing details unrelated to the design ideas and the main process, for better understanding of its design and implementation principles.

rendering

Before beginning, let everybody see ConsecutiveScrollerLayout implementation effect.

OnMeasure, onLayout

ConsecutiveScrollerLayout inherited from ViewGroup, a custom layout always be rewritten onMeasure, onLayout to measure and locate the View.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); }}@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mScrollRange = 0;
        int childTop = t + getPaddingTop();
        int left = l + getPaddingLeft();

        List<View> children = getNonGoneChildren();
        int count = children.size();
        for (int i = 0; i < count; i++) {
            View child = children.get(i);
            int bottom = childTop + child.getMeasuredHeight();
            child.layout(left, childTop, left + child.getMeasuredWidth(), bottom);
            childTop = bottom;
            // Maximum rolling distance of linkage container
            mScrollRange += child.getHeight();
        }
        // The linkage container can scroll range
        mScrollRange -= getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    }

		/** * returns all non-gone child views */
    private List<View> getNonGoneChildren(a) {
        List<View> children = new ArrayList<>();
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                children.add(child);
            }
        }
        return children;
    }
Copy the code

The logic of onMeasured is very simple, traversing the measurement sub VEW. OnLayout is an arrangement of child views from top to bottom, just like a vertical LinearLayout. The getNonGoneChildren() method filters out hidden child views that do not participate in the layout. The mScrollRange variable above is the slider range of the layout itself, which is equal to the height of all child Views minus the height of the layout’s own content display. Later, it will be used to calculate slide offsets and margin limits for the layout.

Intercept slide event

Said earlier ConsecutiveScrollerLayout will intercept it slide of the view of sliding, sliding to handle all by itself. Here is an implementation of its interception of events.

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            // Need to intercept events
            if (isIntercept(ev)) {
                return true; }}return super.onInterceptTouchEvent(ev);
    }
Copy the code

If it is sliding event (ACTION_MOVE), determine whether need to intercept the events, intercept directly returns true, let events by ConsecutiveScrollerLayout onTouchEvent treatments. The key to determining whether an intercept is needed is the isIntercept(EV) method.

    /** * Determine whether the event needs to be intercepted */
    private boolean isIntercept(MotionEvent ev) {
    		// Get the child view of the current touch based on the touch point
        View target = getTouchTarget((int) ev.getRawX(), (int) ev.getRawY());

        if(target ! =null) {
          // Determine whether the child view allows the parent layout to intercept events
            ViewGroup.LayoutParams lp = target.getLayoutParams();
            if (lp instanceof LayoutParams) {
                if(! ((LayoutParams) lp).isConsecutive) {return false; }}// Determine whether the child view can slide vertically
            if (ScrollUtils.canScrollVertically(target)) {
                return true; }}return false;
    }

public class ScrollUtils {

  static boolean canScrollVertically(View view) {
        return canScrollVertically(view, 1) || canScrollVertically(view, -1);
    }
  
  static boolean canScrollVertically(View view, int direction) {
        returnview.canScrollVertically(direction); }}Copy the code

Determine whether need to intercept the events, mainly by judging whether can touch the child view of vertical sliding, vertical sliding, if can intercept events, event processing by ConsecutiveScrollerLayout themselves. If not, don’t intercept, generally can’t sliding view will not consume events, so events will eventually by ConsecutiveScrollerLayout consumption. The reason for not intercepting directly is to give the child view as much opportunity as possible to handle the event and distribute it to the view below.

Here’s a isConsecutive LayoutParams attributes, it is ConsecutiveScrollerLayout LayoutParams custom attributes, Used to represent a child view whether to allow ConsecutiveScrollerLayout intercept it slide events, the default is true. If it is set to false, the parent layout will not intercept the child view’s events, but will defer them entirely to the child view. This gives the child view the opportunity to handle the sliding event and the initiative to distribute the event. This is useful for special needs that require sliding within a local area. I have a detailed explanation of isavg in the demo and usage guide provided on GitHub, so I won’t go into too much detail here.

Sliding handle

Once the event is intercepted, the sliding event is handled in the onTouchEvent method.

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            		// Record the touch points
                mTouchY = (int) ev.getY();
            		// Track the sliding speed
                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                if (mTouchY == 0) {
                    mTouchY = (int) ev.getY();
                    return true;
                }
                int y = (int) ev.getY();
                int dy = y - mTouchY;
                mTouchY = y;
            		// Slide layout
                scrollBy(0, -dy);
								// Track the sliding speed
                initVelocityTrackerIfNotExists();
                mVelocityTracker.addMovement(ev);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchY = 0;

                if(mVelocityTracker ! =null) {
                  	// Handle inertial sliding
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int yVelocity = (int) mVelocityTracker.getYVelocity();
                    recycleVelocityTracker();
                    fling(-yVelocity);
                }
                break;
        }
        return true;
    }

		// Inertial sliding
    private void fling(int velocityY) {
        if (Math.abs(velocityY) > mMinimumVelocity) {
            mScroller.fling(0, mOwnScrollY,
                    1, velocityY,
                    0.0, Integer.MIN_VALUE, Integer.MAX_VALUE); invalidate(); }}@Override
    public void computeScroll(a) {
        if (mScroller.computeScrollOffset()) {
            int curY = mScroller.getCurrY();
          	// Slide layoutdispatchScroll(curY); invalidate(); }}Copy the code

The logic of the onTouchEvent method is very simple, which is to slide the layout content through the View’s scrollBy method according to the sliding distance of the finger, and track the sliding speed of the finger through the VelocityTracker. Inertial sliding is realized by Scroller with computeScroll() method.

Distribution of slip distance

In the treatment of the inertia is sliding, we call the dispatchScroll () method, this method is the core of the whole ConsecutiveScrollerLayout, it determines who should to consume the sliding, sliding the layout should be. In fact ConsecutiveScrollerLayout scrollBy () and scrollTo () method is invoked to handle eventually slip distribution.

    @Override
    public void scrollBy(int x, int y) {
        scrollTo(0, mOwnScrollY + y);
    }

    @Override
    public void scrollTo(int x, int y) {
        // All scroll operations are dispatched by dispatchScroll()
        dispatchScroll(y);
    }

		private void dispatchScroll(int y) {
        int offset = y - mOwnScrollY;
        if (mOwnScrollY < y) {
            // Slide up
            scrollUp(offset);
        } else if (mOwnScrollY > y) {
            // Slide downscrollDown(offset); }}Copy the code

There’s a mOwnScrollY attribute, is used to record ConsecutiveScrollerLayout integral sliding distance, equivalent to the View of mScrollY properties.

The dispatchScroll() method divides sliding into two parts: up and down. Let’s look at the handling of the slide up part first.

    private void scrollUp(int offset) {
        int scrollOffset = 0;  // Slide record of consumption
        int remainder = offset; // Unconsumed sliding distance
        do {
            scrollOffset = 0;
            // Whether to slide to the bottom
            if(! isScrollBottom()) {// Find the first View currently displayed
                View firstVisibleView = findFirstVisibleView();
                if(firstVisibleView ! =null) {
                    awakenScrollBars();
                    // Get the offset where the View slides to the bottom of itself
                    int bottomOffset = ScrollUtils.getScrollBottomOffset(firstVisibleView);
                    if (bottomOffset > 0) {
                        // If bottomOffset is greater than 0, that means the view has not slid to the bottom of itself, then the view consumes the sliding distance.
                        int childOldScrollY = ScrollUtils.computeVerticalScrollOffset(firstVisibleView);
                      	// Calculate the distance you need to slide
                        scrollOffset = Math.min(remainder, bottomOffset);
                        // Slide the subview
                        scrollChild(firstVisibleView, scrollOffset);
                        // Calculate the true slip distance
                        scrollOffset = ScrollUtils.computeVerticalScrollOffset(firstVisibleView) - childOldScrollY;
                    } else {
                        // If the child view has slid to the bottom of itself, the parent layout consumes the sliding distance until the child view is slid off the screen
                        int selfOldScrollY = getScrollY();
                      	// Calculate the distance you need to slide
                        scrollOffset = Math.min(remainder,
                                firstVisibleView.getBottom() - getPaddingTop() - getScrollY());
                        // Slide parent layout
                        scrollSelf(getScrollY() + scrollOffset);
                        // Calculate the true slip distance
                        scrollOffset = getScrollY() - selfOldScrollY;
                    }
                    // Calculate the sliding distance of consumption, if not finished consumption, continue to cycle consumption.mOwnScrollY += scrollOffset; remainder = remainder - scrollOffset; }}}while (scrollOffset > 0 && remainder > 0);
    }

    public boolean isScrollBottom(a) {
        List<View> children = getNonGoneChildren();
        if (children.size() > 0) {
            View child = children.get(children.size() - 1);
            returngetScrollY() >= mScrollRange && ! child.canScrollVertically(1);
        }
        return true;
    }

    public View findFirstVisibleView(a) {
        int offset = getScrollY() + getPaddingTop();
        List<View> children = getNonGoneChildren();
        int count = children.size();
        for (int i = 0; i < count; i++) {
            View child = children.get(i);
            if (child.getTop() <= offset && child.getBottom() > offset) {
                returnchild; }}return null;
    }

    private void scrollSelf(int y) {
        int scrollY = y;

        // boundary detection
        if (scrollY < 0) {
            scrollY = 0;
        } else if (scrollY > mScrollRange) {
            scrollY = mScrollRange;
        }
        super.scrollTo(0, scrollY);
    }

    private void scrollChild(View child, int y) {
        child.scrollBy(0, y);
    }
Copy the code

The logic for swiping up is to find the first child view that is currently displayed, determine whether its contents have been swiped to the bottom, and if not, let it consume the swiping distance. If the slide to the bottom of it already, it is consumption by ConsecutiveScrollerLayout sliding distance, until the child view slide out of the screen. So the next time display for the first view is the next view, repeat the above operation, until the ConsecutiveScrollerLayout and all sub view slide to the bottom, so the whole slide to the bottom.

We use a while loop here, because a sliding distance can be consumed by multiple objects, such as a sliding distance of 50px, but the first child view that is currently displayed needs to slide 10px to its bottom, so the child view will consume 10px. The remaining 40px is the next distribution, finding the objects that need to consume it, and so on.

The process of sliding down is the same as that of sliding up, but the object judged and the direction of sliding are different.

    private void scrollDown(int offset) {
        int scrollOffset = 0;  // Slide record of consumption
        int remainder = offset;  // Unconsumed sliding distance
        do {
            scrollOffset = 0;
            // Whether to slide to the top
            if(! isScrollTop()) {// Find the last View currently displayed
                View lastVisibleView = findLastVisibleView();
                if(lastVisibleView ! =null) {
                    awakenScrollBars();
                    // Get the offset of the View sliding to the top of itself
                    int childScrollOffset = ScrollUtils.getScrollTopOffset(lastVisibleView);
                    if (childScrollOffset < 0) {
                        // If childScrollOffset is greater than 0, the view has not yet slid to the top of itself, so the view consumes the sliding distance.
                        int childOldScrollY = ScrollUtils.computeVerticalScrollOffset(lastVisibleView);
                        // Calculate the distance you need to slide
                        scrollOffset = Math.max(remainder, childScrollOffset);
                        // Slide the subview
                        scrollChild(lastVisibleView, scrollOffset);
                        // Calculate the true slip distance
                        scrollOffset = ScrollUtils.computeVerticalScrollOffset(lastVisibleView) - childOldScrollY;
                    } else {
                        // If the child view is already sliding on top of itself, the parent layout consumes the sliding distance until the child view is fully sliding into the screen
                        int scrollY = getScrollY();
                        // Calculate the distance you need to slide
                        scrollOffset = Math.max(remainder,
                                lastVisibleView.getTop() + getPaddingBottom() - scrollY - getHeight());
                        // Slide parent layout
                        scrollSelf(scrollY + scrollOffset);
                        // Calculate the true slip distance
                        scrollOffset = getScrollY() - scrollY;
                    }
                    // Calculate the sliding distance of consumption, if not finished consumption, continue to cycle consumption.mOwnScrollY += scrollOffset; remainder = remainder - scrollOffset; }}}while (scrollOffset < 0 && remainder < 0);
    }

public boolean isScrollTop(a) {
        List<View> children = getNonGoneChildren();
        if (children.size() > 0) {
            View child = children.get(0);
            return getScrollY() <= 0 && !child.canScrollVertically(-1);
        }
        return true;
    }

public View findLastVisibleView(a) {
        int offset = getHeight() - getPaddingBottom() + getScrollY();
        List<View> children = getNonGoneChildren();
        int count = children.size();
        for (int i = 0; i < count; i++) {
            View child = children.get(i);
            if (child.getTop() < offset && child.getBottom() >= offset) {
                returnchild; }}return null;
    }
Copy the code

Here, about the implementation approach and core ConsecutiveScrollerLayout code analysis is done. Due to space problem, I put on the analysis of the layout features suction a top wrote another article: Android slide layout ConsecutiveScrollerLayout layout top absorption function

I also wrote an article are specially introduced ConsecutiveScrollerLayout use, interested friends can see: the use of the Android continues to slide layout ConsecutiveScrollerLayout

ConsecutiveScrollerLayout to project address is given below, if you like my work, or the layout for your help, please give me a star bai!

Github.com/donkinglian…