1. What is nested sliding
Nested sliding is a common UI effect in android development. When a layout contains multiple slidable Views, and these Views are nested with each other, you need to do nested sliding processing to make the UI interaction have a smoother effect, such as a ceiling effect. The common effects are as follows:
As shown above, the outermost parent layout can slide, and the inner RecyclerView can also slide. When sliding up the RecyclerView, the outermost parent layout first slides up until it reaches the tab. At this time, the RecyclerView starts to slide, the parent layout stops sliding, and the finger does not need to leave the screen, and the entire operation can be completed at one time. In this way, the effect of continuous sliding of the internal and external layout is achieved, and the effect of tab ceiling is achieved.
Two, sliding nesting solution
So how can we achieve such a coherent nested sliding?
1. Manually override event distribution and interception
Everyone is familiar with Android's event distribution mechanism. To achieve the effect of nested sliding, rewriting event distribution is the most primitive method. Early android developers did this. Take the effect shown before as an example. When the ACTION_MOVE event is distributed, first determine whether the tab position is to the top. If it is not, let the outer parent layout intercept the MOVE event, and the parent layout slides. If it has already arrived, do not intercept and pass the event to the child RecyclerView. The process is as follows:
Whether intercept events that override the onIntercetTouchEvent
method, is at the heart of the process.
Disadvantages of manually rewriting event distribution
-
Only suitable for relatively simple nested sliding situations
This is easy to understand. Because you need to manually write the interception logic yourself, once the layout of nested sliding is complicated, a lot of code and logic are needed to implement nested sliding, which increases the cost of maintenance. Therefore, it is not suitable for complex nested sliding layout, and it is actually difficult to realize complex nested sliding.
-
Hard to support fling
Fling refers to the process in which the view continues to slide by inertia after the sliding is released. Generally speaking, considering the user experience, nested sliding needs to support fling. So for the preparation of a manual event distribution, in addition needs to be rewritten
onInterceptTouchEvent
, but also the need forACTION_UP
for specific processing event, because due to flingACTION_UP
generated eventVelocity
. However, the event distribution mechanism does not provide likeonInterceptTouchEvent
that kind of exposure to external interface allows developers to handle theACITON_UP
event. Only through replicationonTouchEvent
to deal with other methods, but doing so too restrictive, because you need to callsuper.onTouchEvent
, but you can not modify the code. -
There is no way to achieve coherent ceiling nesting sliding
Taking the previous example again, when the tab is on top, we hope that the finger will not release and continue to slide up to make the RecyclerView slide up. However, it is impossible to manually intercept the event. You must lift your finger first. Then slide again. Why is this happening? Take a look at
dispatchTouchEvent
the code:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { ...... } else { // 当 MotionEvent 为 ACTION_MOVE 且 mFirstTouchTarget == null 时,仍然拦截事件 intercepted = true; }
When the distribution ViewGroup event, if
mFirstTouchTarget == null
it indicates there are no sub ViewGroup View to the consumer event, which is handled by ViewGroup own. And when ViewGroup intercept events, will justmFirstTouchTarget
be empty. Back to the previous example, when the father of the outer slide layout interceptedACTION_MOVE
after the event, it will bemFirstTouchTarget
blank. Then do not intercept events even after the ceiling, asmFirstTouchTarget
alreadynull
, so the event will not be passed to the child RecyclerView, but continue to consume by the parent layout. In this way, the continuous sliding effect of the ceiling nesting is not achieved.
2、CoordinatorLayout + AppBar + Behavior + scrollFlag
CoordinatorLayout
Is a complex interaction effects can be achieved provided by google layout, and AppBar
, Behavior
, scrollFlag
with the use of decoupling can customize a variety of effects that made Behavior
and scrollFlag
specified. And Behavior
can be customized.
Use CoordinatorLayout
implement nested slide is very simple, as long as the preparation of the layout file as follows:
<?xml version="1.0" encoding="utf-8"?>
<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">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="300dp"
android:layout_width="match_parent">
// 可滑动部分
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_scrollFlags="scroll"/>
<TextView
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_gravity="bottom"
android:text="Top"
android:textSize="32sp"
android:textColor="@color/white"
android:gravity="center"
android:textStyle="bold"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
The AppBarLayout
need to hide the slide portion scrollFlag
is designated as scroll
specified in the RecyclerView behavior
is appbar_scrolling_view_behavior
easiest ceiling nested slide can be achieved, as follows:
It looks like the RecyclerView with header is sliding, but it is actually a nested sliding.
layout_scrollFlags
And layout_behavior
there are many possible values, together with the various effects can be achieved, is not limited to the slide nest. For details, please refer to the API documentation.
Use CoordinatorLayout
the nesting achieve much better than the manual sliding, sliding nested ceiling can be achieved only consistent, but also support fling. And it is the official layout, you can use it with confidence, the chance of bugs is very small, and the performance will not be problematic. But it is precisely because the official encapsulates very well, use CoordinatorLayout
is difficult to achieve more complex nested slide layouts, such as multi-level nested slide.
3. Nested sliding components NestedScrollingParent and NestedScrollingChild
NestedScrollingParent
And NestedScrollingChild
is the official google to solve a set designed to slidably nested components. They are two interfaces, the code is as follows:
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type);
}
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
void stopNestedScroll(@NestedScrollType int type);
boolean hasNestedScrollingParent(@NestedScrollType int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type);
}
Views that require nested sliding can implement these two interfaces and rewrite the methods in them. The core principle of this set of components to achieve nested sliding is very simple, mainly the following three steps:
NestedScrollingChild
In theonTouchEvent
method of the firstACITON_MOVE
displacement dx and dy events produced bydispatchNestedPreScroll
passing to theNestedScrollingParent
NestedScrollingParent
InonNestedPreScroll
the received dx and dy and consumption. Displacement into and consumedint[] consumed
, theconsumed
array is an array of type int length of 2,consumed[0]
representative of the x-axis consumption,consumed[1]
representative of the y-axis consumptionNestedScrollingChild
After theint[] consumed
get the arrayNestedScrollingParent
has been consumed displacement, obtained after subtracting the remaining displacement, and then by their own consumption
The transfer direction of the sliding displacement is from child -> parent -> child, as shown in the figure below. If the child is a Recyclerview, it will first transfer the displacement to the parent layout for consumption, and then the parent layout slides. When the parent layout slides to the point where it cannot slide, the Recyclerview consumes all the displacement at this time. At this time, it starts to slide by itself, thus forming a nested slide. The effect is as seen in the previous example.
dispatchNestedScroll
And onNestedScroll
the like of the action principle preScroll above, but this method of constructing two nested slide and preScroll reverse order, the consumer is the first child View, View child can not consume time, and then again by the parent View consumption.
This mechanism also supports fling, when the finger leaves the view, that is generated ACITON_UP
when an event, child at this time Velocity
into the displacement dx
or dy
flow before and repeat. By @NestedScrollType int type
judged value TYPE_TOUCH
or TYPE_NON_TOUCH
, TYPE_TOUCH
is slidable, TYPE_NON_TOUCH
is fling.
Which Views in Android use this sliding mechanism?
- Implement
NestedScrollingParent
the interface ViewNestedScrollView
are:CoordinatorLayout
, ,MotionLayout
etc. - Implement
NestedScrollingChild
the interface ViewNestedScrollView
are: ,RecyclerView
etc. NestedScrollView
It is the only View that implements two interfaces at the same time, which means that it can be used as an intermediary to implement multi-level nested sliding, which will be discussed later.
Can be seen from the above, in fact, mentioned before CoordinatorLayout
nested slide implemented, it is essentially achieved by this NestedScrolling interface. But because it is packaged so well, we cannot do too much customization. And directly using this set of interfaces, you can customize it according to your own needs.
Most of the scenes, we do not need to implement NestedScrollingChild
the interface, because RecyclerView has been done to achieve this, and it comes to the nested sub-slide scene View are also essential RecyclerView. Let's take a look at the relevant source code of RecyclerView:
public boolean onTouchEvent(MotionEvent e) {
...
case MotionEvent.ACTION_MOVE: {
...
// 计算 dx,dy
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
...
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
...
// 分发 preScroll
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// 减去父 view 消费掉的位移
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
getParent().requestDisallowInterceptTouchEvent(true);
}
...
} break;
...
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 先消耗掉自己的 scroll
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
// 计算剩余的量
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 分发 nestedScroll 给父 View,顺序和 preScroll 刚好相反
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
...
}
RecyclerView how is transferred to the parent View, onNestedPreSroll
and onNestedScroll
it? Analyze dispatchNestedPreScroll
the code below, dispatchNestedScroll
the principles and the code is similar, no longer posted:
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type);
}
// NestedScrollingChildHelper.java
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
...
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
...
}
...
}
return false;
}
// ViewCompat.java
public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
int[] consumed, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {
try {
parent.onNestedPreScroll(target, dx, dy, consumed);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onNestedPreScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
}
}
}
You can see, RecyclerView through a proxy class NestedScrollingChildHelper
sliding distribution is completed, the last to the ViewCompat
static method of treatment to make the parent View onNestedPreScroll
. ViewCompat
The main function is to be compatible with different versions of the sliding interface.
Implement the onNestedPreScroll method
From the above code can be clearly seen RecyclerView for NestedScrollingChild
implementation nested slide and the trigger timing. If we are to achieve nested slide and slide inside child View is RecyclerView, then just let the outer layer of the parent View implementations NestedScrollingParent
method on the line, such as in the onNestedPreScroll
method,
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// 滑动 dy 距离
scrollBy(0, dy);
// 将消耗掉的 dy 放入 consumed 数组通知子 view
consumed[1] = dy;
}
This achieves the simplest nested sliding. Of course, in the actual situation, the sliding distance must be judged, and the parent view cannot always consume the displacement of the child view.
About NestedScrollView
Like NestedScrollView
this class because it implements the inside onNestedScroll
, so in the fall, it fell in RecyclerView inside the top of the list until the outer continue to decline without lifting your finger. There is also a realization onNestedPreScroll
method, but it continues to slide upward in the transfer process, not their consumption, the following code:
// NestedScrollView.java
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
// 只分发了 preScroll 自己并没有消费。之所以能分发是因为 NestedScrollView 同时实现了 NestedScrollingChild 接口
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}
// NestedScrollingChildHelper.java
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
...
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
...
}
...
}
return false;
}
So if directly RecyclerView outer jacket NestedScrollView
is no way to achieve complete nested slides, you will find when on slippery, no nested slide effect, but fell when nested slide effect.
Unconsidered issues
In fact, in the content mentioned before, it is assumed that the finger starts to slide from the sub Viw. If the finger starts to slide from the outer parent View, when the parent View flings to the top, the child View cannot continue to fling, and will stop immediately, failing to achieve continuous nested sliding.
This is because the nested slide assembly, the shift from consumption only NestedScrollingChild
to NestedScrollingParent
, not from NestedScrollingParent
to NestedScrollingChild
, because only NestedScrollingChild
in order to dispatch, NestedScrollingParent
can not dispatch.
If you want to achieve from NestedScrollingParent
the NestedScrollingChild
slide coherent, there is no particularly good way, can only override the parent View event distribution, after the remainder of the top parent View slide displacement manually distributed to its sub-View. (First dig a hole to see if there is a better way, you can achieve the goal by expanding the nested sliding component)
Tips
NestedScrollingParent
And NestedScrollingChild
a total of three versions.
The earliest is NestedScrollingParent
and NestedScrollingChild
which set of interfaces to scroll and fling processed separately, resulting in unnecessary complexity.
After that I came NestedScrollingParent2
and NestedScrollingChild2
inherited from the previous generation, but it will fling into the distance scroll the reunification process. The above-mentioned nested sliding components all refer to the second generation.
Subsequently, a further NestedScrollingParent3
and NestedScrollingChild
inherited from the second generation, which increases compared to the second generation dispatchNestedScroll
, and onNestedScroll
can consume part sliding displacement, i.e. displacement after the parent View consumption, the consumption value into the consumed
array of notification sub-View. The second generation will not let the child View know the consumption value of the parent View. Generally speaking, if you want to implement nested sliding yourself, you only need to implement the 2nd generation and above interface. The first generation is basically no longer used.
Note : Use NestedScrollView
a need to pay attention, when it is RecyclerView child View this when content can be infinitely long layout, should be taken to limit the height of the sub-View, do not use the wrap_content
set RecyclerView height. Because NestedScrollView
when measuring a child View restriction is UNSPECIFIED
that no limit, RecyclerView how high you want to have tall. Like RecyclerView if too many internal item, RecyclerView in wrap_content
the case of all item will show up, equivalent to no recovery. This will cause a lot of memory consumption. If you call setVisibility
to change visibility, then, when from invisible to visible, but will instantly call to all item test layout process, resulting in Caton. This is the problem I actually encountered in the project.
Three, multi-level nested sliding
We know that NestedScrollingParent
and NestedScrollingChild
can be used to customize realize their nest slide. It is easy to think that if a View implements two interfaces at the same time, it can not only accept the sliding of the child, but also distribute the sliding to the parent, thus forming a chain. The core principle of multi-level nested sliding comes from this, as shown in the figure:
The principle is actually not complicated. Let's show it in pseudo code below:
-
for
NestedScrollingParent
@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { scrollByMe(dx, dy); ... consumed[0] = dxConsumed; consumed[1] = dyConsumed; }
-
For intermediaries, that is, while achieving
NestedScrollingParent
andNestedScrollingChild
middle View is@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { // 先分发,再消费。当然也可以先以先消费,再分发,这取决于业务 dispatchNestedPreScroll(dispatchNestedPreScroll(dx, dy, consumed, null, type); int dx -= consumed[0]; int dy -= consumed[1]; scrollByMe(dx, dy); consumed[0] = dxConsumed; consumed[1] = dyConsumed; }
-
For the innermost layer
NestedScrollingChild
, RecyclerView can generally be used.
In the multi-level nested sliding, you can set the priority of each layer in the process of sliding up and down according to the business.
The working project will not be put up because it has not been released. Here is the image of the multi-level nested sliding of the instant app found on the Internet:
Fourth, the design pattern used in nested sliding components
As a summary, discuss it.
-
Strategy mode
NestedScrollingParent
AndNestedScrollingChild
a pair of interfaces, the different interfaces of View to achieve different pair of nested achieved by sliding effect. At the same time, the use of interfaces also ensures scalability. -
Agency model
As described above, when the method of nesting a sliding interface View implementations, the transfer carriage are specific to the agent
NestedScrollingParentHelper
andNestedScrollingChildHelper
to achieve, which is provided by two classes sdk inNestedScrollingParent
andNestedScrollingChild
found the following interfaces:This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view. Classes implementing this interface should create a final instance of a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper methods of the same signature.
-
Adapter Mode / Appearance Mode
RecyclerView implements
NestedScrollingChild2
the interface, but if its parent view to achieve a generation ofNestedScrollingParent
interfaces how to do? This means that different versions of nested sliding components need to be compatible. How to achieve compatibility, useViewCompat
it as follows:// ViewCompat.java public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed, int type) { if (parent instanceof NestedScrollingParent2) { // First try the NestedScrollingParent2 API ((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type); } else if (type == ViewCompat.TYPE_TOUCH) { // Else if the type is the default (touch), try the NestedScrollingParent API if (Build.VERSION.SDK_INT >= 21) { try { parent.onNestedPreScroll(target, dx, dy, consumed); } catch (AbstractMethodError e) { Log.e(TAG, "ViewParent " + parent + " does not implement interface " + "method onNestedPreScroll", e); } } else if (parent instanceof NestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); } } }
Sliding distribute all of the child, will by
ViewCompat
static method final before being passed to parent, through this class and static methods to achieve the compatibility with different versions of nested slide assembly. At the same time,ViewCompat
the easy-to-use interface is exposed to the outside, and the compatible process is hidden inside itself, which can also be seen as a kind of appearance mode.