withNestedScrollingParent2Create a smooth pull-down refresh pull-up load control

Preface:

NestedScrollingParent2 is a nested sliding interface introduced by Google. It is paired with NestedScrollingChild2.

Implementing a pull-down refresh control the traditional way involves handling event distribution. To determine whether the current touch event should be handed over to the parent or child control. The next step is to record the distance the finger moves and slide accordingly; And if it’s inertia, you have to do the fling, which requires speed and other parameters. In a word, it’s just a bit of a hassle.

The appearance of NestedScrollingParent2 solves the above problems.

Why use NestedScrollingParent2 to create this control instead of + NestedScrollingChild2?

Because system control RecyclerView has realized the interface of NestedScrollingChild2. So, we just have to deal with NestedScrollingParent2.

  • Of course, this also means that the control currently supports only all implementationsNestedScrollingChild2Control, pull down refresh, and pull up load.
  • But, fortunately, for nowRecyclerViewThe usage scenarios are far greater thanListView.

If you must use ListView/ScrollView as a child control. Just… You can modify the control as appropriate and then use it.

Implementation strategy

Cut in line. Let’s see what happens

Pull down refresh effect

Pull up to load more effects

  1. Obviously, the “display click operation feedback” has been turned on, but there is still no trace of finger ~🤣
  2. Because the recording time is too long, resulting in the conversion togifAnd then it wasn’t very friendly. Just so-😹
  3. It just shows the effect of the control without actually updating the data ~😹

Train of thought

  • Anticipate the layout of the control. At the top is a refresh header layout, hidden by default; And then the one in the middleRecyclerView, is displayed by default; There is a tail layout at the bottom that loads more, which is also invisible by default.

For the convenience of expression, the subsequent RecyclerView is directly abbreviated as RV or RV;

  • For the above layout, we can simply inherit the linear layout and do it with less modification.
  • Header layout processing:
    • The header layout is hidden by default, and the height can be set to 0. You can set themarginTopThe same height as oneself;
    • So my implementation here is going to take approach two, which is going to bemarginTopIt’s the same height as you.
    • Why not use method one? This will dynamically change the height during the pull-down, causing it to be called repeatedlymeasureAs well aslayout, not performance friendly.
  • RecyclerViewAnd the handling of the tail end:
    • The tail game is also hidden by default. But we have to measure its height, so we can’t just take itRecyclerViewSet the height tomatch_parent, which results in the tail not being properly measured.
    • We can givervSet a fixed height, for example10dpOr,layout_height=0dp; layout_weigit=1;So the tail end can be measured normally.
  • This does not complete the layout. Because it leads torvHeight is a fixed value or its own height – the height of the tail.
  • Solution: rewriteonMeasureAnd letrvThe height is the same as the default height. And its own height becomesrv.height + footer.heightCan.
  • Now that the static layout is complete, we need to anticipate sliding processing. If the current is sliding it should be handedrvOf, we don’t deal with, letrvYou can slide it yourself; Hide the header layout if you want to show it; Show tail, hide tail; We’ll take care of it ourselves.

You have to take a look at the methods provided by NestedScrollingParent2.

OnStartNestedScroll: corresponding to startNestedScroll, the internal controller calls this method of the external control to determine whether the external control receives sliding information.

OnNestedScrollAccepted: This method is called back when the outer control determines that it has received a slide message, allowing the outer control to do some upfront work on nested slides.

OnNestedPreScroll: The key method, receives the sliding distance information of the internal controller before sliding, in which the external control can preferentially respond to the sliding operation, consuming part or all of the sliding distance.

OnNestedScroll: the key method to receive the sliding distance information of the internal controller after it has processed the sliding distance, and the external control can choose whether to process the remaining sliding distance.

Use Android nesting sliding mechanism to easily achieve top layout

The above introduction comes from the online blog, the introduction of the more clear.

Although it is introduced to NestedScrollingParent, NestedScrollingParent2 has little change to NestedScrollingParent. Note the addition of an int type parameter to each method, which specifies whether the current trigger is from touch sliding or inertial sliding. Then remove NestedScrollingParent# onNestedScroll from the scroll, because the scroll is inertia.

  1. Well, we definitely have to deal with itonStartNestedScrollBecause we’re dealing with sliding. And, we’re going to limit it to dealing only with longitudinal slides;
  2. We have to deal with thatonStopNestedScrollPull down to show the head, or pull up to show the tail and then let go. We want it to hide the corresponding head and tail layout.
  3. We still have to deal withonNestedPreScrollIn thervWhen we swipe to the top, if we continue to pull down, we want to slide ourselves to show the refresh head; Similarly, inrvWhen sliding to the tail, if the finger continues to pull up, we want to slide ourselves to show the tail (The tail is to load more layouts).
  4. We still have to deal with itonNestedPreScrollBecause if the head and tail are currently in the display, and the finger is not released, and the finger is sliding in the opposite direction, the user expects to actively hide the head or tail. So we have to behave as expected, namely, slide ourselves to hide our heads and tails; And then letrvKeep sliding.

Ok, the above is our overall implementation idea.

Detail analysis

  • Inertial sliprvCan the head or tail be shown?
  • No, because that’s not what users expect. Let users take the initiative to slide with their fingers to display the head and tail layout.
  • In the header and tail display, the user let go, how to make it smooth back to original?
  • usingScrollerTo complete the action.
  • To what extent do you need to pull down or up to trigger a refresh?
  • Pull down or up to fully display the head or tail to trigger the refresh. (This policy can be modified)
  • Should I keep the user swiping during the refresh process?
  • Do not let the user slide at this point, and so on to restore the original, can continue to slide. (This policy can also be modified, but the implementation is a bit more or more cumbersome.)
  • How do I notify the caller that a pull-down refresh or pull-up load has been triggered?
  • Expose the interface for the caller to implement. similarSwipeRefreshLayout#setOnRefreshListenerThis kind of.
  • How can the caller tell me that the data refresh is complete and that the refresh header should be hidden?
  • Expose the public method and let the caller actively notify me. similarSwipeRefreshLayout#setRefreshing(boolean)
  • In fact, there are many states in drop-down refresh, such as: drop-down state, refresh state, refresh state; These states often need to be switched in the refresh header. How to do this? Let the caller do different displays according to different states; Still? (Same with loading more.)
  • If we let the caller display according to different states, it means we have to expose more interfaces for the caller to implement, which is very troublesome; However, if each state display is blocked, if different pages want different state display text, you have to change it several times. It’s really a dilemma. At this time, you can take custom attributes to operate; Let the caller configure the different copy in the custom property, and then display the copy according to the state obtained in the property.

Above are some of the details that need to be dealt with so far, and the corresponding solutions.

Code practice

After the general idea and detailed analysis, it’s time to actually write the code.

Based on the previous analysis, you need three controls in XML, no more, no less. If the onMeasure is larger or smaller, the corresponding onMeasure needs to be modified, and the corresponding nestedScroll process needs to be modified as well.

  • So, let’s do the layout first.

<com.python.cat.mvvm.widgets.NestedRefreshLayout
    android:id="@+id/nested_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/light_gray3"
    android:orientation="vertical"
    app:loadDoneText="@string/do_load_done"
    app:loadInitText="@string/refresh_footer"
    app:loadStartText="@string/drop_to_load"
    app:loadingText="@string/do_load_now"
    app:refreshDoneText="@string/do_refresh_done"
    app:refreshDrawable="@drawable/ic_replay_black_24dp"
    app:refreshInitText="@string/refresh_header"
    app:refreshStartText="@string/drop_to_refresh"
    app:refreshingText="@string/do_refresh_now">

    <LinearLayout
        android:id="@+id/refresh_header"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginTop="-100dp"
        android:contentDescription="@string/refresh_header"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/header_refresh_img"
            android:layout_width="45dp"
            android:layout_height="45dp"
            android:layout_gravity="center_horizontal"
            android:contentDescription="@string/refresh_header"
            android:gravity="center"
            android:src="@drawable/ic_replay_black_24dp"
            android:textColor="@color/white" />

        <TextView
            android:id="@+id/header_refresh_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:gravity="center"
            android:text="@string/refresh_header"
            android:textColor="@color/normal_color" />
    </LinearLayout>
    <! Footer can't be measured as 0dp. The real measurement is done in parent#onMeasure.
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/articles_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    <LinearLayout
        android:id="@+id/refresh_footer"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:contentDescription="@string/refresh_header"
        android:orientation="vertical">

        <TextView
            android:id="@+id/footer_refresh_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:gravity="center"
            android:text="@string/refresh_footer"
            android:textColor="@color/normal_color" />

        <ImageView
            android:id="@+id/footer_refresh_img"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="10dp"
            android:contentDescription="@string/refresh_header"
            android:gravity="center"
            android:src="@drawable/refresh_progress" />

    </LinearLayout>

</com.python.cat.mvvm.widgets.NestedRefreshLayout>

Copy the code

Look at our layout file. It’s a little long. The header and tail layout here could have been added with the
tag to make it shorter. Then there are a lot of custom attributes in it, which makes it a bit longer.

One of the attributes is Android :orientation=”vertical”, which is important because we inherited the linear layout, so we made it straight up.

And then notice that I’ve put a property inside the header that says Android :layout_marginTop=”-100dp”, because we’ve defined the height to be Android :layout_height=”100dp”. If you set it to wrap_content, then you don’t know how to write the marginTop. This can be handled in other ways than here.

  • Then there are controls.

I’m not going to write custom property handling here, because it’s independent of the topic.

Handle the onMeasure first. Once the measurement is handled, the layout will display normally.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int hs = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
        int ws = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
        
        View recyclerV = getChildAt(1);
        View footer = getChildAt(2);
        int selfHeight = getMeasuredHeight() + footer.getMeasuredHeight();
        setMeasuredDimension(getMeasuredWidth(),
                selfHeight);
        
        ViewGroup.LayoutParams lp2 = recyclerV.getLayoutParams();
        
        lp2.height = MeasureSpec.getSize(hs);
        measureChildWithMargins(recyclerV, ws, 0, MeasureSpec.makeMeasureSpec(selfHeight, MeasureSpec.EXACTLY), 0);
    }

Copy the code

There’s not a lot of code, but that’s what we’re looking for. The height of NestedRefreshLayout is match_parent, that is, the expected height of NestedRefreshLayout is the same as the height of its parent. Then, we can set the height of the RV according to this expectation, so that the RV becomes its own expected height. At the same time, we want to make ourselves the height we want to be plus the height of the footer.

This means that the parent’s height is actually one height higher than the footer.

Once measured, the layout is complete. Since it is an inherited linear layout, there is no need to rewrite onLayout.

The following are related to nesting slides, such as displaying refresh headers, loading tails, and hiding headers and tails; Also, loading, refreshing animations and so on.

  • To clarify:

In contrast to NestedScrollingParent, note that each method in NestedScrollingParent2 has almost twice as many callbacks as the NestedScrollingParent method of the same name.

For example, onStartNestedScroll is called once after each scroll by default. However, in most cases your finger slides in a fling before letting go, causing the onStartNestedScroll to snap back again. The first time, type == ViewCompat.TYPE_TOUCH; The second type == ViewCompat#TYPE_NON_TOUCH.

Same thing with the other methods.

@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
                                   int axes, int type) {
    // indicates that only vertical slides are handled, not horizontal slides, which is the first method to be called back to
    // Only once or twice will be called back during a slide.
    // Type = not_touch and type= not_touch
    return (axes == ViewCompat.SCROLL_AXIS_VERTICAL);
}
Copy the code
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
                                   int axes, int type// This method will be called back immediately after onStartNestedScroll // this method will be called back only twice during a scroll; // I have done the refresh header, reload tail reset work here. ResetHeaderState (); resetHeaderState(); resetFooterState(); clearAllAnimator(); }Copy the code
// This method is called back after onNestedScrollAccepted is called and is called several times until the onNestedScrollAccepted is let go. // That is to say, if I receive dy completely directly here, @override public void onNestedPreScroll(@nonnull View target, int dx, int dy, @nonnull int[] Consumed, inttype) {

    if(1 == 2) { consumed[1] = dy; // This will cause the interface to not move! ~ bingo! // Because you actively consume the entire sliding distance without actually doing any sliding logic.return; } // dy < 0; FetchTargetTop =! target.canScrollVertically(-1); boolean fetchTargetBottom = ! target.canScrollVertically(1); int firstHeight = getChildAt(0).getHeight(); int lastH = getChildAt(2).getHeight(); // footer height boolean hideTop = dy > 0 && getScrollY() < 0; boolean hideBottom = dy < 0 && getScrollY() > 0; TextView tvRefresh = getChildAt(0).findViewById(R.id.header_refresh_tv);if(getScrollY() <= -firstheight) {// header fully shows tvRefresh. SetText (mRefreshStartText); }else{// Headers are not fully displayed, or simply not visible tvRefresh.settext (mRefreshInitText); } View lastV = getChildAt(getChildCount() - 1); TextView loadMreTv = lastV.findViewById(R.id.footer_refresh_tv);if (getScrollY() >= lastH) {
        loadMreTv.setText(mLoadStartText);
    } else {
        loadMreTv.setText(mLoadInitText);
    }

    ifHideTop {// dy < 0, indicating finger slide; > 0 on the slideif (getScrollY() + dy > 0) {
            dy = 0 - getScrollY();
        }
        LogUtils.e("scrollBy zz : %s", dy);
        scrollBy(0, dy);
        consumed[1] = dy;
    } else if (hideBottom) {
        LogUtils.w("scrollBy zz : %s", dy); // dy < 0; > 0 on the slideif(getScrollY() + dy < 0) { dy = 0 - getScrollY(); } logutils.e ("scrollBy zz : %s", dy); scrollBy(0, dy); consumed[1] = dy; }}Copy the code
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                           int dxUnconsumed, int dyUnconsumed, int type) {// dy < 0, indicating finger slip; FetchTargetTop =! target.canScrollVertically(-1); // boolean fetchTargetBottom = ! target.canScrollVertically(1); int firstHeight = getChildAt(0).getHeight(); int lastH = getChildAt(2).getHeight(); // footer height boolean showTop = fetchTargetTop && dyUnconsumed < 0 && getScrollY() <= 0 &&type == ViewCompat.TYPE_TOUCH;
    boolean showBottom = fetchTargetBottom && dyUnconsumed > 0 && getScrollY() >= 0
            && type== ViewCompat.TYPE_TOUCH; / / add a = 0, including edge cases Boolean moreHead = showTop && getScrollY () < = - firstHeight; boolean moreBottom = showBottom && getScrollY() >= lastH; TextView tvRefresh = getChildAt(0).findViewById(R.id.header_refresh_tv);if(getScrollY() <= -firstheight) {// header fully shows tvRefresh. SetText (mRefreshStartText); }else{// Headers are not fully displayed, or simply not visible tvRefresh.settext (mRefreshInitText); } View lastV = getChildAt(getChildCount() - 1); TextView loadMreTv = lastV.findViewById(R.id.footer_refresh_tv);if (getScrollY() >= lastH) {
        loadMreTv.setText(mLoadStartText);
    } else {
        loadMreTv.setText(mLoadInitText);
    }

    if (moreHead) {
        if(! hasRefreshFeedback) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); hasRefreshFeedback =true;
        }

        scrollBy(0, dyUnconsumed);
    } else if(showTop) {// dy < 0; > 0 on slippery LogUtils. E ("scrollBy zz : %s", dyUnconsumed);
        scrollBy(0, dyUnconsumed);
    } else if (moreBottom) {
        if(! hasLoadMoreFeedback) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); hasLoadMoreFeedback =true;
        }
        scrollBy(0, dyUnconsumed);
    } else if (showBottom) {
        LogUtils.e("scrollBy zz : %s", dyUnconsumed); // dy < 0; > 0 on slippery LogUtils. E ("scrollBy zz : %s", dyUnconsumed); scrollBy(0, dyUnconsumed); }}Copy the code
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
    LogUtils.i("onStopNestedScroll: %s,%s, sy=%s", target, type, getScrollY());

    if (type! = viewCompat.type_touch) {// Each fling goes herereturn;
    }
    View head = getChildAt(0);
    View foot = getChildAt(getChildCount() - 1);
    if(getScrollY() <= -head.getheight ()) {// The header layout is fully displayed to refresh autoScroll =true;
        hasRefreshFeedback = false;
        mRefreshDone = false;
        showRefreshAnimator();
    } else if(getScrollY() > foot.getheight ()) {load more related autoScroll =true;
        hasLoadMoreFeedback = false;
        mLoadMoreDone = false;
        showLoadMoreAnimator();
        // some codes ...
    } else if(getScrollY() ! = 0) { autoScroll =true; smooth2Normal(); }}Copy the code

The code is almost done. However, a callback is also provided so that the caller can call the corresponding refresh to load more. To handle the actual load, refresh logic.

public interface OnRefreshListener {
    /**
     * Called when a swipe gesture triggers a refresh.
     */
    void onRefresh();
}

public interface OnLoadMoreListener {
    /**
     * Called when a swipe gesture triggers a load-more.
     */
    void onLoadMore();
}

Copy the code

Then provide the corresponding set method.

This part of the code has nothing to do with the nesting slide itself, and I won’t add it here.

Above, to create a smooth pull-down refresh pull-up loading of the control. Of course, this is currently only implemented for RV.

Finally, special thanks

  • Inertial sliding of nested sliding

  • Android nested sliding

  • Android nested sliding hongyang

  • And the documents that can be viewed in the AS.

  • And comrade Yellow Cat. No help was offered to me this time.