What is nested sliding
Nested sliders are a common UI effect in Android development. When a layout contains multiple views that can be swiped, and these views are nested within each other, nested swiping is required to make the UI interaction smoother, such as the top effect. Common effects are:
As shown above, the outermost parent layout can slide, as can the RecyclerView on the inner layer. When sliding RecyclerView, the outermost parent layout first slide, until the slide to TAB, this time began to slide, the parent layout to stop sliding, and fingers do not need to leave the screen, you can complete the entire operation at once. In this way, the internal and external layout of a coherent sliding effect, and to achieve the TAB top effect.
Second, sliding nesting solution
So how do you do this coherent nested slide?
1. Manual override event distribution and interception
You’re all familiar with Android’s event distribution mechanism, and rewriting event distribution is one of the most primitive ways to achieve nested sliders. Early Android developers did it this way. For example, when the ACTION_MOVE event is distributed, first determine whether the TAB position is at the top. If not, then let the outer parent layout intercept the MOVE event and the parent layout slide. If it has arrived, then do not intercept, the event is passed to the child RecyclerView, the process is as follows:
Intercepting an event, or overriding the onIntercetTouchEvent method, is central to the process.
Disadvantages of manually rewriting event distribution
-
It is only suitable for the simple case of nested sliding
It makes sense. Because you need to manually write the intercepting logic yourself, once the layout of nested sliders becomes complex, it takes a lot of code and logic to implement nested sliders, increasing maintenance costs. Therefore, it is not suitable for complex nested sliding layout, and in fact difficult to implement complex nested sliding.
-
Difficult to support fling
Fling is the process by which the view continues to slide with inertia after the hand has been released. In general, nested sliders need to support Fling for user experience. For manual event distribution, not only onInterceptTouchEvent needs to be overwritten, but ACTION_UP event needs to be specifically handled, because Velocity is generated from ACTION_UP event. However, the event distribution mechanism does not provide an exposed interface like onInterceptTouchEvent for developers to handle the ACITON_UP event. You can only do this by copying onTouchEvent and so on, which is very restrictive because you need to call super.onTouchEvent, but you can’t change the code in it.
-
There is no way to achieve a coherent top nesting slide
Or in the previous example, when TAB suction top, we hope that the fingers do not loosen to continue to slide up can make RecyclerView slide up, but the manual interception event is not to do, must first lift the finger and then slide again. Why is that? Look at the code in dispatchTouchEvent:
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {... }else { // When MotionEvent is ACTION_MOVE and mFirstTouchTarget == NULL, the event is still intercepted intercepted = true; } Copy the code
When a ViewGroup distributes an event, if mFirstTouchTarget == NULL then there are no subviews in the ViewGroup to consume the event. The event is handled by the ViewGroup itself. When the ViewGroup intercepts the event, it simply sets the mFirstTouchTarget blank. Going back to the previous example, when the outer sliding parent layout intercepts the ACTION_MOVE event, it sets the mFirstTouchTarget blank. Then even if the event is not intercepted after the top, because the mFirstTouchTarget has been null, so the event is not passed to the child RecyclerView, but continues to be consumed by the parent layout. This does not achieve a coherent top nesting sliding effect.
CoordinatorLayout + AppBar + Behavior + scrollFlag
CoordinatorLayout is a Set of layouts provided by Google that can be used to achieve complex interactions with appbars, behaviors, and ScrollFlags to decoupage and customize multiple effects. These effects are specified by the Behavior and scrollFlag. And behaviors can be customized.
Implementing nested sliding with CoordinatorLayout is as simple as writing the layout file as follows:
<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">// The sliding part<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>
Copy the code
Set scrollFlag to scroll for the part of AppBarLayout that you want to hide. RecyclerView appbar_scrolling_view_behavior appbar_scrolling_view_behavior
It looks like a RecyclerView with a header is sliding, but it’s actually a nested slide.
There are many alternative values for layout_scrollFlags and layout_behavior that can work together to achieve a variety of effects, not just nested slides. Refer to the API documentation for details.
Using CoordinatorLayout for nested sliding is much better than doing it manually, as it allows for consistent top-notch nested sliding with fling support. And is the official provided layout, you can rest assured to use, the probability of a bug is very small, performance will not have a problem. However, because CoordinatorLayout is so well encapsulated officially, it is difficult to implement complex nested slide layouts, such as multi-level nested slides, using CoordinatorLayout.
3. Nested sliders NestedScrollingParent and NestedScrollingChild
NestedScrollingParent and NestedScrollingChild are a set of components that are provided by Google for solving nested sliders. They are two interfaces with the following code:
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);
}
Copy the code
A View that needs to be nested to slide can implement both interfaces, copying the methods in them. The core principle of this set of components to achieve nested sliding is simple, mainly in the following three steps:
NestedScrollingChild
在onTouchEvent
Method firstACITON_MOVE
The displacement produced by the event dx and dy passes throughdispatchNestedPreScroll
Passed to theNestedScrollingParent
NestedScrollingParent
在onNestedPreScroll
It takes dx and dy and consumes them. And I put in the displacement that I consumedint[] consumed
,consumed
An array is an int of length 2,consumed[0]
Represents the consumption of the X-axis,consumed[1]
This is the consumption on the Y-axisNestedScrollingChild
After fromint[] consumed
ArrayNestedScrollingParent
You subtract the displacement that you’ve consumed and you get the rest of the displacement that you can consume by yourself
Slide displacement transfer direction from child -> parent -> child, as shown in the figure below. If the child is Recyclerview, it will first shift to the parent layout consumption, then the parent layout sliding. When the parent layout slides up to the point where it can’t slide, the Recyclerview consumes all the displacement, and then it begins to slide on its own, creating a nested slide, as you saw in the previous example.
DispatchNestedScroll and onNestedScroll work in the same way as preScroll, except that the nested slide order of dispatchNestedScroll is the opposite of preScroll’s. When the child View cannot consume, the parent View consumes again.
The mechanism also supports fling. When the finger leaves the view (ACITON_UP), the Child converts the Velocity to a displacement dx or dy and repeats the process. The value of @nestedscrollType int type is not a TYPE_TOUCH, but a TYPE_NON_TOUCH.
Which Android views use this sliding mechanism?
- implementation
NestedScrollingParent
The View of the interface has:NestedScrollView
,CoordinatorLayout
,MotionLayout
等 - implementation
NestedScrollingChild
The View of the interface has:NestedScrollView
,RecyclerView
等 NestedScrollView
Is the only View that implements both interfaces at the same time, which means it can be used as a mediation to implement multi-level nested sliding, as we’ll see later.
As you can see above, the CoordinatorLayout implementation is actually implemented through this NestedScrolling interface in essence. But because it’s packaged so well, we can’t do much customization. Using this interface directly, you can customize it according to your own needs.
Most of the scenarios, we do not need to implement the NestedScrollingChild interface, because RecyclerView has done this implementation, and involves a nested sliding scene subview is also basically RecyclerView. RecyclerView RecyclerView
public boolean onTouchEvent(MotionEvent e) {...case MotionEvent.ACTION_MOVE: {
...
// Compute dx, dy
int dx = mLastTouchX - x;
intdy = mLastTouchY - y; . mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0; ./ / distribute preScroll
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
// Subtracted the displacement consumed by the parent 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;
// I'm going to consume my scroll first
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
// Calculate the remaining amount
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// Distribute nestedScroll to parent View in reverse order of preScroll
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1]; . }Copy the code
RecyclerView is how to adjust to the parent View onNestedPreSroll and onNestedScroll? The code for dispatchNestedPreScroll is similar to that for dispatchNestedScroll.
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 instanceofNestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); }}}Copy the code
As you can see, RecyclerView through a proxy class NestedScrollingChildHelper distribute sliding, finally to ViewCompat handle onNestedPreScroll static method to let the parent View. The main purpose of ViewCompat is to be compatible with different versions of the sliding interface.
Implement the onNestedPreScroll method
From the above code you can clearly see the RecyclerView implementation for NestedScrollingChild, and the timing of triggering nested slides. If we want to implement nested slides and the inner slide child View is RecyclerView, then all we need to do is have the outer parent View implement the NestedScrollingParent method, such as in the onNestedPreScroll method.
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
// Slide dy distance
scrollBy(0, dy);
// Notifies the subview of consumed dy in the consumed array
consumed[1] = dy;
}
Copy the code
This makes for the simplest possible nested slide. Of course, in the real world, you have to judge the sliding distance, so you can’t have the parent View consuming the displacement of the child View all the time.
About NestedScrollView
A class like NestedScrollView, because it implements onNestedScroll internally, can slide down in its internal RecyclerView until it reaches the top of the list, and the outer layer continues to slide down without lifting a finger. The onNestedPreScroll method is also implemented, but it continues to pass the slide up in the method without consuming itself, as follows:
// NestedScrollView.java
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
// Only preScroll is distributed and not consumed. It can be distributed because NestedScrollView also implements the NestedScrollingChild interface
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;
}
Copy the code
So if directly in the RecyclerView of the outer layer of NestedScrollView is no way to achieve a complete nested sliding, you will find that on the slide, there is no nested sliding effect, and the slide has nested sliding effect.
Problems not considered
In fact, in the previous example, the default is to slide from the child Viw. When the parent View reaches the top, the child View cannot continue to fling, so it stops immediately.
This is because in nested sliders, the displacement consumption can only go from NestedScrollingChild to NestedScrollingParent, not from NestedScrollingParent to NestedScrollingChild, Because only NestedScrollingChild can dispatch, NestedScrollingParent cannot dispatch.
If you want to slide from NestedScrollingParent to NestedScrollingChild, there’s no good way to do it, but to override the parent View’s event distribution and manually distribute the remaining shift from the parent View to its children. (Dig a hole and see if there’s a better way to do this by extending the nested sliding components)
Tips
There are three versions of NestedScrollingParent and NestedScrollingChild.
The first is NestedScrollingParent and NestedScrollingChild. This set of interfaces handles Scroll and Fling separately, creating unnecessary complexity.
Later, NestedScrollingParent2 and NestedScrollingChild2 are inherited from the first generation, but the distance from fling to Scroll is treated in the same way. The above nested sliding components refer to second generation.
And then NestedScrollingParent3 and NestedScrollingChild inherited from the second generation, They add the function of dispatchNestedScroll and onNestedScroll consuming part of the sliding displacement compared to generation 2. That is, after the parent View consumes displacement, the consumed value is consumed in the consumed array to inform the child View. The second generation does not let the child View know the cost of the parent View. Generally speaking, to achieve their own nested sliding, you only need to implement 2 generations and above the interface. Generation ONE is basically no longer used.
Note: One of the important things about using NestedScrollView is that when its subviews are recyclerViews that can be infinitely long, limit the height of those subviews. Don’t use wrAP_content to set the RecyclerView height. As the NestedScrollView gives the subview UNSPECIFIED, that is, UNSPECIFIED, the RecyclerView can be as high as it wishes. Like RecyclerView if the number of internal items is too much, RecyclerView in the case of wrAP_content will show all items, equivalent to no recycling. This is a big memory drain. If you call setVisibility to change the visibility, when it is visible from the invisible, the measurement layout process of all items will be called instantly, resulting in a lag. This is a real problem I have encountered on projects.
Three, multi-level nested sliding
We know that NestedScrollingParent and NestedScrollingChild can be used to customize their own nested sliders. It’s easy to imagine that if a View implements both interfaces, it can accept sliders from the child and distribute sliders to the parent, thus forming a chain. This is where the core principle of multilevel nested sliding comes in, as shown here:
The principle is actually not complicated, as shown in pseudocode:
-
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; }Copy the code
-
For intermediaries, intermediate views that implement both NestedScrollingParent and NestedScrollingChild
@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { // Distribute first, consume later. It can also be consumed first and then distributed, depending on the business dispatchNestedPreScroll(dispatchNestedPreScroll(dx, dy, consumed, null, type); int dx -= consumed[0]; int dy -= consumed[1]; scrollByMe(dx, dy); consumed[0] = dxConsumed; consumed[1] = dyConsumed; } Copy the code
-
For the innermost NestedScrollingChild, generally use RecyclerView can be.
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.
I don’t want to post the project because it hasn’t been published yet. Here is a picture of jike App’s multi-level nested slide found on the Internet:
You can refer to this article: zhuanlan.zhihu.com/p/56582475
Design patterns used in nested sliding components
To conclude, let’s discuss.
-
The strategy pattern
NestedScrollingParent and NestedScrollingChild are a pair of interfaces that are implemented by different views to achieve different nested sliding effects. The use of interfaces also ensures scalability.
-
The proxy pattern
As aforesaid, when a View nested sliding interface to realize the method, the specific transfer slip to the NestedScrollingParentHelper and NestedScrollingChildHelper agent, these two classes is provided by the SDK, The NestedScrollingParent and NestedScrollingChild interfaces are described as follows:
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. Copy the code
-
Adapter mode/appearance mode
RecyclerView implements the NestedScrollingChild2 interface, but what if its parent view implements the NestedScrollingParent interface? This requires compatibility between different versions of nested sliders. To achieve compatibility, use ViewCompat, 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 instanceofNestedScrollingParent) { ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); }}}Copy the code
All child sliding distributions are passed to parent via ViewCompat’s static methods, which are compatible with different versions of nested sliding components. At the same time, ViewCompat exposes easy-to-use interfaces and hides compatible processes internally, which can also be seen as a look-and-feel mode.