preface
In the last two articles, we talked about the principle of event distribution. Through principle and work experience, we know that knowledge of event distribution alone is not enough to make a sophisticated user experience.
Here’s the most common scenario:
Obviously, if you want to achieve this effect, it is obviously not possible to achieve it through a regular event distribution mechanism. After all, once the Bar above starts to slide, it means that it has determined to consume this event, so in a slide, the RecyclerView below can not get this event in any case.
* * but! ** Since RecyclerView + CoordinatorLayout realized this effect, that means there is a way to do it. This method is called NestedScrolling
This article does not talk about usage, mainly source code analysis ~~~
# text
If we understand the event distribution mechanism, it becomes clear that the downside of event distribution is that once a View consumes the event, the sequence of consuming events is completely owned by the View. Therefore, it is impossible for a View to consume half of its MOVE events and then give the rest to others.
In order to solve this problem, Google still added NestedScrolling mechanism in the process of event distribution based on the idea of event distribution. To put it plainly, there are two interfaces: NestedScrollingParent and NestedScrollingChild
Of course, newer SDKS will find this interface changed to NestedScrollingParent2 and NestedScrollingChild2. NestedScrollingParent2 inherits from NestedScrollingParent. So we article is based on NestedScrollingParent/NestedScrollingChild is analyzed.
This mechanism is surprisingly simple, and can be summed up in one sentence: When the internal View implementing NestedScrollingChild interface slides, first give the sliding distance DX and dy to the external View implementing NestedScrollingChild interface (may not be the direct parent View), and the external View can consume part of it. And the rest of it goes back to the internal View.
NestedScrollingParent
The first time you tap into this interface… WTF? So many ways? . But it’s easy to calm down.
public interface NestedScrollingParent {
// is called when the slide starts. The return value indicates whether the internal View sliding parameters (x, y) are consumed.
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
// Accept inner View (can be indirect child View) slide
void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
// Stop consuming events for internal views
void onStopNestedScroll(View target);
// Called when the inner View slides
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
// Called before the inner View starts sliding. The parameters dx and dy indicate the lateral and longitudinal distances of the slide,
//consumed in //consumed in //consumed in //consumed in
// Indicates that the outer View and the inner View handle 1/2 of the sliding distance respectively
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
// Call when the inner View starts to Fling
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
// The inner View is called before the Fling begins. The parameters velocityX and velocityY represent the horizontal and vertical velocities.
// The Fling is not executed. The Fling is not executed. The Fling is not executed.
boolean onNestedPreFling(View target, float velocityX, float velocityY);
// Slide vertically or horizontally
int getNestedScrollAxes(a);
}
Copy the code
NestedScrollingChild
public interface NestedScrollingChild {
// Set whether NestedScrolling is supported
void setNestedScrollingEnabled(boolean enabled);
boolean isNestedScrollingEnabled(a);
// Ready to slide
boolean startNestedScroll(int axes);
// Stop sliding
void stopNestedScroll(a);
// Whether there are nested sliding external views
boolean hasNestedScrollingParent(a);
// When the inner View slides, notify the outer View.
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
// Let the external View handle the slide before the inner View slides.
// The dx and dy parameters represent the horizontal and vertical distances of the slide, and the consumed external View represents the horizontal and vertical distances of the slide.
// The return value indicates whether the external View consumes the slide.
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
// The external View is notified when the internal View is engaged in a Fling.
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
// Similar to the dispatchNestedPreScroll method...
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
Copy the code
Interpretation of the
Method many, but really really really very simple!!
I’m guessing that those of you who have looked carefully at each method name have already guessed how this mechanic works.
The following interpretation, directly according to the implementation code, to thoroughly clarify the meaning of NestedScrollingParent, NestedScrollingChild so many methods.
First of all, the NestedScrolling mechanism is a mechanism for “exploratory scrolling” from the son to the father. So let’s start from the realization of NestedScrollingChild’s RecyclerView.
A, RecyclerView
public class RecyclerView extends ViewGroup implements ScrollingView.NestedScrollingChild2
Copy the code
1.1 startNestedScroll() process
In onInterceptTouchEvent() we can see that startNestedScroll() is called when a DOWN event occurs:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
// Omit some code
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
/ / call startNestedScroll ()
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
break;
// Omit some code
// The retrun value depends on whether the current RecyclerView is sliding
return mScrollState == SCROLL_STATE_DRAGGING;
}
Copy the code
Point into the startNestedScroll (), we will find that the implementation agent into NestedScrollingChildHelper:
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
Copy the code
Inside the Helper, call getParent() to get the parent View and call onStartNestedScroll() in NestedScrollingParent2:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
// Omit some code
// Whether to start NestedScrolling
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while(p ! =null) {
// If Parent is not null, call the onStartNestedScroll() method of the Parent class, and return true if this method returns true
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
// Call onNestedScrollAccepted() if true
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceofView) { child = (View) p; } p = p.getParent(); }}return false;
}
Copy the code
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
// Omit some code
Copy the code
The CoordinatorLayout (which implements NestedScrollingParent2) in onStartNestedScroll() finds that the CoordinatorLayout converts this method into Behavior. The return value of this method depends on the return value of onStartNestedScroll() in the Behavior.
I’m using AppBarLayout, so the implementation of the Behavior is in AppBarLayout.
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
// Omit some code
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if(viewBehavior ! =null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false); }}return handled;
}
Copy the code
If it returns true, it means that setNestedScrollAccepted() in Behavior is called. This method has a default implementation in CoordinatorLayout, which basically means that a member variable is assigned a value of true.
void setNestedScrollAccepted(int type, boolean accept) {
switch (type) {
case ViewCompat.TYPE_TOUCH:
mDidAcceptNestedScrollTouch = accept;
break;
case ViewCompat.TYPE_NON_TOUCH:
mDidAcceptNestedScrollNonTouch = accept;
break; }}Copy the code
This variable records whether a child View responds to NestedScrolling.
1.2. DispatchNestedPreScroll () process
Next we’ll see the real thing.
@Override
public boolean onTouchEvent(MotionEvent e) {
// Omit some code
switch (action) {
// Omit some code
case MotionEvent.ACTION_MOVE: {
// Omit some code
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1]; }}// Omit some code
}
return true;
}
Copy the code
We can see that in the MOVE event in onTouchEvent(), RecyclerView calls dispatchNestedPreScroll().
This also means that RecyclerView starts consuming this event.
We can see dispatchNestedPreScroll () method is also through NestedScrollingChildHelper, Then go to the ViewParentCompat and go to the onNestedPreScroll() of the CoordinatorLayout.
CoordinatorLayout also calls its implementation of onNestedPreScroll() by finding a Behavior that responds.
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
// Iterate over the subview
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
// Omit some code
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
// Determine whether the View responds to NestedScrolling (startNestedScroll())
if(! lp.isNestedScrollAccepted(type)) {continue;
}
final Behavior viewBehavior = lp.getBehavior();
if(viewBehavior ! =null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair, type);
// Omit some code}}// Omit some code
}
Copy the code
The implementation in AppBarLayout looks like this:
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dx, int dy, int[] consumed, int type) {
if(dy ! =0) {
int min;
int max;
if(dy < 0) {
min = -child.getTotalScrollRange();
max = min + child.getDownNestedPreScrollRange();
} else {
min = -child.getUpNestedPreScrollRange();
max = 0;
}
if(min ! = max) {// Call scroll, scroll itself and send the consumed dy back through the array.
consumed[1] = this.scroll(coordinatorLayout, child, dy, min, max);
// Determine whether to stop
this.stopNestedScrollIfNeeded(dy, child, target, type); }}}Copy the code
When you go to CoordinatorLayout, if anyone has noticed, the return value of this set of methods is void. Because the return value of dispatchNestedPreScroll() is processed in the Helper:
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if(dx ! =0|| dy ! =0) {
// Omit some code
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
// Omit some code, so long as 0 and 1 bits of consumed are not 0, return true
return consumed[0] != 0 || consumed[1] != 0;
// Omit some code
}
return false; }}Copy the code
For RecyclerView, dispatchNestedPreScroll() returns ture, which means that the MOVE event is consumed by a superior View, and the next thing for oneself is to do some consumption according to the remaining events.
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
// dx and dy minus dx and dy consumed by other views.
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
// Omit some code
}
Copy the code
1.3 dispatchNestedScroll() process
This method is called when the RecyclerView itself has a sliding action:
boolean scrollByInternal(int x, int y, MotionEvent ev) {
// omit some code, dispatchNestedScroll() is called
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
TYPE_TOUCH)) {
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if(ev ! =null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
// omit its own sliding business code
returnconsumedX ! =0|| consumedY ! =0;
}
Copy the code
.
No doubt, this method will eventually be called into the Behavior in AppBarLayout:
public void onNestedScroll(CoordinatorLayout coordinatorLayout, T child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if(dyUnconsumed < 0) {
this.scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
this.stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
}
if(child.isLiftOnScroll()) {
child.setLiftedState(target.getScrollY() > 0); }}Copy the code
OK, so originally belong to RecyclerView events, abruptly passed to be. Can only “play” others “play” the rest of the event…
1.4 stopNestedScroll() process
Since stop is used, it must be initiated by Parent. Yes, stopNestedScrollIfNeeded(dyUnconsumed, child, target, type) is called in the above process, which means stop is tried:
private void stopNestedScrollIfNeeded(int dy, T child, View target, int type) {
if(type == 1) {
int curOffset = this.getTopBottomOffsetForScrollingSibling();
if(dy < 0 && curOffset == 0 || dy > 0 && curOffset == -child.getDownNestedScrollRange()) {
ViewCompat.stopNestedScroll(target, 1); }}}Copy the code
Once the conditions are met, call RecyclerView in reverse via ViewCompat:
public static void stopNestedScroll(@NonNull View view, @NestedScrollType int type) {
if (view instanceof NestedScrollingChild2) {
((NestedScrollingChild2) view).stopNestedScroll(type);
} else if(type == ViewCompat.TYPE_TOUCH) { stopNestedScroll(view); }}Copy the code
. In short, it is called layer by layer to complete the stop process.
Second, the CoordinatorLayout
For CoordinatorLayout, there is no more to talk about, because we have basically learned how to use it in RecyclerView…
Act as a middleman, passing events from NestedScrollingChild to the Behavior in order to pass sliding events from one child View to another.
In actual combat
To create an effect like this:
Here is the code (very simple, just a few lines) :
class NestedTopLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr), NestedScrollingParent {
private var mShowTop = false
private var mHideTop = false
private val mTopViewHeight = 800
private val defaultMarginTop = 800
override fun onFinishInflate(a) {
super.onFinishInflate()
scrollBy(0, -defaultMarginTop)
}
override fun onStartNestedScroll(@NonNull child: View.@NonNull target: View, nestedScrollAxes: Int): Boolean {
returnnestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL ! =0
}
override fun onNestedScrollAccepted(@NonNull child: View.@NonNull target: View, nestedScrollAxes: Int) {}
override fun onStopNestedScroll(@NonNull target: View) {}
override fun onNestedScroll(@NonNull target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {}
override fun onNestedPreScroll(@NonNull target: View, dx: Int, dy: Int.@NonNull consumed: IntArray) {
var dy = dy
mShowTop = dy < 0&& Math.abs(scrollY) < mTopViewHeight && ! target.canScrollVertically(- 1)
if (mShowTop) {
if (Math.abs(scrollY + dy) > mTopViewHeight) {
dy = -(mTopViewHeight - Math.abs(scrollY))
}
}
mHideTop = dy > 0 && scrollY < 0
if (mHideTop) {
if (dy + scrollY > 0) {
dy = -scrollY
}
}
if (mShowTop || mHideTop) {
consumed[1] = dy
scrollBy(0, dy)
}
}
override fun onNestedFling(@NonNull target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
returnscrollY ! =0
}
override fun onNestedPreFling(@NonNull target: View, velocityX: Float, velocityY: Float): Boolean {
returnscrollY ! =0
}
override fun getNestedScrollAxes(a): Int {
return ViewCompat.SCROLL_AXIS_VERTICAL
}
}
Copy the code
The end of the
Plus this article, this piece of event distribution feels like enough. It can fully cope with a variety of sliding experience needs.
OK, sauce ~