Before, I wrote a nested sliding –NestedScroll project example (defects of Taobao home page), and the linkage principle of CoordinatorLayout and AppbarLayout, compared the sliding effects of Taobao and JINGdong home page, analyzed the reasons for the difference in effects, and gave a general solution. At that time, there was no demo, only code snippet, which may make it not very clear to read, so this article is dedicated to a detailed analysis of relevant knowledge, to give a general nesting slide solution, and attached to the GitHub demo.
Code Github address, welcome star and issue
I. Problems and solutions
Let’s start with a picture:
The outermost layer is RecyclerView for multi-layout, and the last item is tabLayout+ViewPager. Each fragment of ViewPager is also RecyclerView. This is the common layout of the home page of e-commerce apps.
Let’s look at the slide in action:
Suck the top
Suck the top
So, if we directly according to the above layout structure to achieve, will be jingdong this effect? The answer is no, the effect is as follows, right?
Suck the top
Click to see the code
So what to do? According to the relevant knowledge of sliding conflict, we know that the outer RecyclerView must intercept the touch event, and the inner RecyclerView cannot obtain the event, so it cannot slide. So can the tabLayout top, the outer layer do not intercept events, so the inner RecyclerView to get events and slide?
This works, but after tabLayout slides to the top, you have to lift your finger, re-slide, and the inner RecyclerView can continue to slide. Why is that? From the blog I mentioned at the beginning:
We know from the view event distribution mechanism that when the parent View intercepts an event, all the events in the same sequence are sent directly to the parent, and the child view will not accept the event. Therefore, the process is handled according to the normal idea of handling sliding conflicts — when the TAB is not at the top, the parent intercepts the event, but when the TAB is at the top, the parent does not intercept the event. But because the finger is not lifted, the event sequence is still given to the parent, not to the internal RecyclerView. So the flow of goods doesn’t slide.
The solution has to be nested sliding layouts. The code is as follows:
<? The XML version = "1.0" encoding = "utf-8"? > <com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl3 xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/nested_scrolling_parent2_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView_parent" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.hfy.demo01.module.home.toucheveCopy the code
See we changed the roots of the outer RecyclerView layout NestedScrollingParent2LayoutImpl3, after the operation found really solve the above problems, the sliding effect with jingdong. The NestedScrollingParent2LayoutImpl3 what is this? NestedScrollingParent2LayoutImpl3 is inherited NestedScrollingParent2 LinearLayout, used for handling the nested sliding.
The effect is as follows:
If you don’t care about the principle and implementation, to this end, because NestedScrollingParent2LayoutImpl3 can solve the above problem.
Second, the implementation principle of NestedScrollingParent2LayoutImpl3
2.1 Let’s review the nested sliding mechanism.
If you don’t already know NestedScrolling and NestedScrollingParent2, I recommend reading the advanced custom View events (NestedScrolling) before continuing.
NestedScrolling mechanism simply means: generate NestedScrolling child views. Before scrolling, ask the parent view whether it has priority to process events and how many events are consumed, and then send the remaining part to the child view. You can think of it as a sequence of events distributed twice. NestedScrollingChild2 and NestedScrollingParent2 are used to create nested sliding child views.
Commonly used RecyclerView is to achieve NestedScrollingChild2, and NestedScrollView is to achieve both NestedScrollingChild2 and NestedScrollingParent2.
Usually we have to manually handle RecyclerView as a nested sliding child view. NestedScrollView is generally used directly as the root layout to solve nested sliding.
2.2 Look again at NestedScrollView nested RecyclerView
NestedScrollView nested RecyclerView, where headers and lists can slide together. The diagram below:
The real name opposes the usage of NestedScrollView nested RecyclerView in Alibaba Android development Manual
NestedScrollView nested RecyclerView can achieve the effect, but RecyclerView will instantly load all items
Here is the solution:
<? The XML version = "1.0" encoding = "utf-8"? > <com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl2 xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv_head" android:layout_width="match_parent" android:layout_height="200dp" Android :background="@color/colorAccent" Android :gravity="center" Android :padding="15dp" Android :text=" I am the header. The outermost layer is NestedScrollingParent2LayoutImpl2 "android: textColor =" # FFF 20 dp "android: textSize =" "/ > <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/design_default_color_primary" /> </com.hfy.demo01.module.home.touchevent.view.NestedScrollingParent2LayoutImpl2>Copy the code
NestedScrollingParent2LayoutImpl2 was also realized NestedScrollingParent2.
The effect is as follows: it can be seen that the slide is smooth, and there is no need to lift the finger to slide again at the critical point, and the log is not loaded all at one time.
First look at the implementation of the NestedScrollingParent2LayoutImpl2 to simpler, then see NestedScrollingParent2LayoutImpl3 implementation principle, the overall train of thought is the same.
/ * * * processing header + recyclerView * Description: nested sliding under NestedScrolling2 mechanism, implement NestedScrollingParent2 interface, the treatment effect of fling difference * * /
public class NestedScrollingParent2LayoutImpl2 extends NestedScrollingParent2Layout implements NestedScrollingParent2 {
private View mTopView;
private View mRecylerVIew;
private int mTopViewHeight;
public NestedScrollingParent2LayoutImpl2(Context context) {
this(context, null);
}
public NestedScrollingParent2LayoutImpl2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedScrollingParent2LayoutImpl2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
return(axes & ViewCompat.SCROLL_AXIS_VERTICAL) ! =0;
}
/** * Before the nested sliding child View does not slide, determine whether the parent View and child View first (i.e. the parent View can consume, then consume child View) **@paramTarget Specifies the subclass of the nested slide *@paramDx horizontal nested sliding child View wants to change the distance *@paramThe distance that a child View that slides in dy vertically wants to change dy<0 slide down dy>0 slide up *@paramThe consumed parameter tells us when we implement this function to go back to the child View and tell it how far the current parent View consumes * consumed[0] horizontally and consumed[1] vertically so that the child View can adjust accordingly *@paramType Slide type,ViewCompat. TYPE_NON_TOUCH fling effect,ViewCompat.TYPE_TOUCH slide */
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// Both scroll and fling are handled
boolean hideTop = dy > 0 && getScrollY() < mTopViewHeight;
boolean showTop = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
if (hideTop || showTop) {
scrollBy(0, dy);
consumed[1] = dy; }}@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// When the child control finishes processing, it is handed to the parent control for processing.
if (dyUnconsumed < 0) {
// Indicates that you have slid down to the top
scrollBy(0, dyUnconsumed); }}@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Change the height of the mRecylerVIew to the height of the screen, otherwise the bottom will be blank. (Since the scrollTo method is a sliding subView, it slides the mRecylerVIew onto it.)
ViewGroup.LayoutParams layoutParams = mRecylerVIew.getLayoutParams();
layoutParams.height = getMeasuredHeight();
mRecylerVIew.setLayoutParams(layoutParams);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onFinishInflate(a) {
super.onFinishInflate();
mTopView = findViewById(R.id.tv_head);
mRecylerVIew = findViewById(R.id.recyclerView);
if(! (mRecylerVIewinstanceof RecyclerView)) {
throw new RuntimeException("id RecyclerView should be RecyclerView!"); }}@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mTopViewHeight = mTopView.getMeasuredHeight();
}
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y > mTopViewHeight) {
y = mTopViewHeight;
}
super.scrollTo(x, y); }}Copy the code
Mainly onNestedPreScroll to do the critical processing: sliding RecyclerView sliding root layout first, so that the head hide or display, and then to RecyclerView sliding.
2.3 NestedScrollingParent2LayoutImpl3 principle
The following code
/** * Dispose of RecyclerView set of viewPager, the fragment inside the viewPager also has RecyclerView, dispose of the outer and inner layer of the nested sliding problem of RecyclerView
public class NestedScrollingParent2LayoutImpl3 extends NestedScrollingParent2Layout {
private final String TAG = this.getClass().getSimpleName();
private RecyclerView mParentRecyclerView;
private RecyclerView mChildRecyclerView;
private View mLastItemView;
public NestedScrollingParent2LayoutImpl3(Context context) {
super(context);
}
public NestedScrollingParent2LayoutImpl3(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NestedScrollingParent2LayoutImpl3(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
}
/** * There is a nested slide coming, check whether the parent view accepts nested slides **@paramChild child of the parent class of the nested slide@paramTarget Specifies the subclass of the nested slide *@paramNestedScrollAxes supports nestedScrollAxes. Horizontal, vertical, or do not specify *@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
// handle the logic yourself
// Accept vertical nested sliding
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
/** * Before the nested sliding child View does not slide, determine whether the parent View and child View first (i.e. the parent View can consume, then consume child View) **@paramTarget is the subclass of the nested slide, which is the view * that produces the nested slide@paramDx horizontal nested sliding child View wants to change the distance *@paramThe distance that a child View that slides in dy vertically wants to change dy<0 slide down dy>0 slide up *@paramThe consumed parameter tells us when we implement this function to go back to the child View and tell it how far the current parent View consumes * consumed[0] horizontally and consumed[1] vertically so that the child View can adjust accordingly *@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// handle the logic yourself
if (mLastItemView == null) {
return;
}
int lastItemTop = mLastItemView.getTop();
if (target == mParentRecyclerView) {
handleParentRecyclerViewScroll(lastItemTop, dy, consumed);
} else if(target == mChildRecyclerView) { handleChildRecyclerViewScroll(lastItemTop, dy, consumed); }}/** * The process of sliding the outer RecyclerView **@paramThe distance between the lastItemTop TAB and the top of the screen is 0 *@paramDy target sliding distance, dy>0 means sliding up *@param consumed
*/
private void handleParentRecyclerViewScroll(int lastItemTop, int dy, int[] consumed) {
// TAB does not reach the top
if(lastItemTop ! =0) {
if (dy > 0) {
/ / slip up
if (lastItemTop > dy) {
// If you want to slide dy, let the external RecyclerView process itself
} else {
// top of TAB <= to slide dy, slide external RecyclerView, slide distance lastItemTop, just to the top; The rest of it slides on the inner layer.
consumed[1] = dy;
mParentRecyclerView.scrollBy(0, lastItemTop);
mChildRecyclerView.scrollBy(0, dy - lastItemTop); }}else {
// Let the external RecyclerView process itself}}else {
// The TAB top reaches the top
if (dy > 0) {// Up, the inner layer is consumed directly
mChildRecyclerView.scrollBy(0, dy);
consumed[1] = dy;
}else {
int childScrolledY = mChildRecyclerView.computeVerticalScrollOffset();
if (childScrolledY > Math.abs(dy)) {
// The distance that the inner layer has rolled is greater than the desired distance, and the inner layer consumes it directly
mChildRecyclerView.scrollBy(0, dy);
consumed[1] = dy;
}else {
// The distance that the inner layer has rolled is less than the distance that the inner layer wants to roll
mChildRecyclerView.scrollBy(0, -(Math.abs(dy)-childScrolledY));
consumed[1] = -(Math.abs(dy)-childScrolledY); }}}}/** * The process of sliding inner RecyclerView **@paramThe distance between the lastItemTop TAB and the top of the screen is 0 *@param dy
* @param consumed
*/
private void handleChildRecyclerViewScroll(int lastItemTop, int dy, int[] consumed) {
// TAB does not reach the top
if(lastItemTop ! =0) {
if (dy > 0) {
/ / slip up
if (lastItemTop > dy) {
// TAB top> want to slide dy, outer layer directly consumed
mParentRecyclerView.scrollBy(0, dy);
consumed[1] = dy;
} else {
// TAB top<= dy, lastItemTop, lastItemTop; The rest of it slides on the inner layer.
mParentRecyclerView.scrollBy(0, lastItemTop);
consumed[1] = dy - lastItemTop; }}else {
// The outer layer is consumed directly
mParentRecyclerView.scrollBy(0, dy);
consumed[1] = dy; }}else {
// The TAB top reaches the top
if (dy > 0) {// Up, the inner layer handles itself
}else {
int childScrolledY = mChildRecyclerView.computeVerticalScrollOffset();
if (childScrolledY > Math.abs(dy)) {
// The inner layer has rolled the distance greater than the desired distance, the inner layer handles itself
}else {
// The inner layer has been rolled less than the desired distance, so the inner layer consumes part of it, after reaching the top, the remaining outer layer slides
mChildRecyclerView.scrollBy(0, -childScrolledY);
mParentRecyclerView.scrollBy(0, -(Math.abs(dy)-childScrolledY));
consumed[1] = dy; }}}}@Override
protected void onFinishInflate(a) {
super.onFinishInflate();
// Get the external RecyclerView
mParentRecyclerView = getRecyclerView(this);
Log.i(TAG, "onFinishInflate: mParentRecyclerView=" + mParentRecyclerView);
// About the internal RecyclerView: at this time can not get the RecyclerView of the internal fragment of ViewPager, need to be passed when the fragment is visible after loading the ViewPager
}
private RecyclerView getRecyclerView(ViewGroup viewGroup) {
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
if (childAt instanceof RecyclerView) {
if (mParentRecyclerView == null) {
return(RecyclerView) childAt; }}}return null;
}
/** * pass internal RecyclerView **@param childRecyclerView
*/
public void setChildRecyclerView(RecyclerView childRecyclerView) {
mChildRecyclerView = childRecyclerView;
}
/** * The last item of the external RecyclerView, that is: TAB + viewPager * used to judge the sliding critical position **@param lastItemView
*/
public void setLastItem(View lastItemView) { mLastItemView = lastItemView; }}Copy the code
NestedScrollingParent2LayoutImpl3 inherited from NestedScrollingParent2Layout. NestedScrollingParent2Layout is inherited from LinearLayout implements and realizes NestedScrollingParent2, mainly dealing with the general method of implementation.
/** * Description: generic sliding nested processing layout used for processing {@linkAndroidx. Recyclerview. Widget. Recyclerview} a condom sliding * /
public class NestedScrollingParent2Layout extends LinearLayout implements NestedScrollingParent2 {
private NestedScrollingParentHelper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
public NestedScrollingParent2Layout(Context context) {
super(context);
}
public NestedScrollingParent2Layout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NestedScrollingParent2Layout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/** * There is a nested slide coming, check whether the parent view accepts nested slides **@paramChild child of the parent class of the nested slide@paramTarget Specifies the subclass of the nested slide *@paramNestedScrollAxes supports nestedScrollAxes. Horizontal, vertical, or do not specify *@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {
// handle the logic yourself
return true;
}
/** * When the parent view accepts nested sliding, the onStartNestedScroll method will call ** when it returns true@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
}
/** * Before the nested sliding child View does not slide, determine whether the parent View and child View first (i.e. the parent View can consume, then consume child View) **@paramTarget Specifies the subclass of the nested slide *@paramDx horizontal nested sliding child View wants to change the distance *@paramThe distance that a child View that slides in dy vertically wants to change dy<0 slide down dy>0 slide up *@paramThe consumed parameter tells us when we implement this function to go back to the child View and tell it how far the current parent View consumes * consumed[0] horizontally and consumed[1] vertically so that the child View can adjust accordingly *@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// handle the logic yourself
}
/** * Inline sliding child View after sliding, determine whether the parent View continue processing (i.e., the parent consume a certain distance, the child consume, finally determine whether the parent consume) **@paramTarget Specifies the subclass of the nested slide *@paramDxConsumed How far a child View that slides horizontally has nested slides *@paramDyConsumed the distance to which a child View that is nested sliding vertically slides *@paramDxUnconsumed distance (unconsumed distance) * of a horizontally nested sliding child View@paramDyUnconsumed distance (unconsumed distance) * of the child View that is nested sliding vertically@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// handle the logic yourself
}
/** * nested slide ends **@paramTYPE_NON_TOUCH Fling effect ViewCompat.TYPE_TOUCH Slide */
@Override
public void onStopNestedScroll(@NonNull View child, int type) {
mNestedScrollingParentHelper.onStopNestedScroll(child, type);
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
// Decide for yourself whether to process
return false;
}
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
// handle the logic yourself
return false;
}
@Override
public int getNestedScrollAxes(a) {
returnmNestedScrollingParentHelper.getNestedScrollAxes(); }}Copy the code
The implementation principle is mainly in the onNestedPreScroll method, that is, before sliding the nested sliding child view, ask the corresponding parent view whether priority processing, and how much processing.
So whatever sliding outer urban RecyclerView or inner RecyclerView, can ask NestedScrollingParent2LayoutImpl3, namely will all come to onNestedPreScroll method. Then according to the tabLayout position and sliding direction, decide to slide the outer RecyclerView or sliding inner layer, and sliding how much. It is equivalent to distributing a leave sequence twice, avoiding the problem that the child view cannot handle after the parent view intercepts the regular event distribution.
OnNestedPreScroll in the specific processing, see the code, there are detailed comments. To be combined with the sliding actual situation to understand, easy to meet other cases can also be processed.
Here are three implemented schemes for handling nested slides:
- NestedScrollingParent2LayoutImpl1: handle the header + TAB + + recyclerView viewPager
- The header + recyclerView NestedScrollingParent2LayoutImpl2: processing
- NestedScrollingParent2LayoutImpl3: Process RecyclerView set viewPager, there is also RecyclerView in the fragment of viewPager, process the nested sliding problem of outer and inner RecyclerView, similar to taobao, JINGdong home page.
Code Github address, welcome star and issue, if there is help star wave.
.
Welcome to the public account