效果图
概况可见:github.com/stewForAni/…
顺便给另一个项目求个Star:github.com/stewForAni/…
布景
一直想抽时间稳固一下嵌套滑动的知识点,尤其是NestedScrolling系列的东西,也查阅了很多文章,多多少少都不尽如人意,要么只讲解了NestedScrollingParent的用法,没有讲到NestedScrollingChild,这样导致顶部的TopView不能滑动;要么TopView简单实现了NestedScrollingChild,TopView和RecyclerView联动性时,始终是没有RecyclerView丝滑,打不过就加入,所以就去扒了RecyclerView的源码,看看人家到底是怎么做到如此丝滑的。。。
不足
很多临界点的处理暂时没有做,某些速度会导致Fling传导到RecyclerView时,造成RecyclerView推迟滑动等等问题,本篇重点处理了NestedScrollingChild滑动不顺的问题,首要是参考了RecyclerView的做法,不足之处待后续优化。。。
查阅
RecyclerView要害代码如下:
@Override
public boolean onTouchEvent(MotionEvent e) {
//获取速度跟踪器
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
//给事件设置嵌套偏移量,这个设置还挺要害的,决定了正常的滑动速度
//还请有研究的大佬告知小弟其详细原理
final MotionEvent vtev = MotionEvent.obtain(e);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {
//和NestedScrollingParent交互信息
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_MOVE: {
//滑动前,先让父布局滑动
dispatchNestedPreScroll
//如果有剩下未消费,则交给RecyclerView内部来消化,即RecyclerView自己滑动
scrollByInternal
} break;
case MotionEvent.ACTION_UP: {
//UP里首要处理fling事件,也是本篇的要害
//将事件交给速度跟踪器,并核算当时的速度
mVelocityTracker.addMovement(vtev);
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
//执行fling,交给了ViewFlinger,是一个Runnable
//内部run中的逻辑和MOVE中类似,先询问父布局,有剩下再自己处理
//经过不断提交Runnable,使fling继续下去,当然速度必定越来越小
fling(...)
} break;
}
...
}
详细实现
NSParentLayout2
public class NSParentLayout2 extends LinearLayout implements NestedScrollingParent2 {
private static final String TAG = NSParentLayout2.class.getSimpleName();
private View topView;
private View bottomView;
private int topViewHeight;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final LinearLayout.LayoutParams params;
public NSParentLayout2(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
setOrientation(VERTICAL);
params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = findViewById(R.id.top_view);
bottomView = findViewById(R.id.bottom_view);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
params.height = getMeasuredHeight();
bottomView.setLayoutParams(params);
topViewHeight = topView.getMeasuredHeight();
}
@Override
public void scrollTo(int x, int y) {
if (y < 0) {
y = 0;
}
if (y >= topViewHeight) {
y = topViewHeight;
}
super.scrollTo(x, y);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
return true;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
//dy代表方向,getScrollY代表总的间隔原始方位的滑动间隔
boolean TOPSHOW = dy > 0 && getScrollY() < topViewHeight;
//canScrollVertically(-1)负值查看向上翻滚,正向查看向下翻滚
boolean TOPHIDE = dy < 0 && getScrollY() >= 0 && !target.canScrollVertically(-1);
if (TOPSHOW || TOPHIDE) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
if (target == topView) {
bottomView.scrollBy(0, dyUnconsumed);
}
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mNestedScrollingParentHelper.onStopNestedScroll(target, type);
}
@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;
}
}
NSChildLayout3
public class NSChildLayout3 extends LinearLayout implements NestedScrollingChild2, NestedScrollingChild3 {
private int lastX = -1;
private int lastY = -1;
private final int[] consumed = new int[2];
private final int[] offset = new int[2];
private final int[] mNestedOffsets = new int[2];
private int mScrollPointerId = INVALID_POINTER;
private VelocityTracker vt;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private FlingTool flingTool;
public NSChildLayout3(Context context) {
super(context);
}
public NSChildLayout3(Context context, AttributeSet attrs) {
super(context, attrs);
//设置当时子View是否支撑嵌套滑动,如果是false,父View无法呼应嵌套滑动
setNestedScrollingEnabled(true);
setOrientation(VERTICAL);
ViewConfiguration vc = ViewConfiguration.get(context);
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();//默许50
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();//默许8000
flingTool = new FlingTool(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//处理fling
if (vt == null) {
vt = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
final MotionEvent vtev = MotionEvent.obtain(event);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN:
mScrollPointerId = event.getPointerId(0);
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH);
break;
case MotionEvent.ACTION_MOVE:
int currentX = (int) event.getRawX();
int currentY = (int) event.getRawY();
int dx = lastX - currentX;
int dy = lastY - currentY;
if (dispatchNestedPreScroll(dx, dy, consumed, offset, TYPE_TOUCH)) {
dx -= consumed[0];
dy -= consumed[1];
// Updated the nested offsets
mNestedOffsets[0] += offset[0];
mNestedOffsets[1] += offset[1];
}
dispatchNestedScroll(0, 0, dx, dy, null, TYPE_TOUCH);
lastX = currentX;
lastY = currentY;
break;
case MotionEvent.ACTION_UP:
vt.addMovement(vtev);
eventAddedToVelocityTracker = true;
vt.computeCurrentVelocity(1000, mMaxFlingVelocity);
//int xvt = (int) -vt.getXVelocity(mScrollPointerId);
int yvt = (int) -vt.getYVelocity(mScrollPointerId);
fling(0, yvt);
reSetScroll();
break;
case MotionEvent.ACTION_CANCEL:
reSetScroll();
break;
}
if (!eventAddedToVelocityTracker) {
vt.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void reSetScroll() {
lastX = -1;
lastY = -1;
if (vt != null) {
vt.clear();
}
stopNestedScroll(TYPE_TOUCH);
}
private void fling(int xvt, int yvt) {
if (Math.abs(xvt) < mMinFlingVelocity) {
xvt = 0;
}
if (Math.abs(yvt) < mMinFlingVelocity) {
yvt = 0;
}
if (xvt == 0 && yvt == 0) {
return;
}
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
xvt = Math.max(-mMaxFlingVelocity, Math.min(xvt, mMaxFlingVelocity));
yvt = Math.max(-mMaxFlingVelocity, Math.min(yvt, mMaxFlingVelocity));
flingTool.fling(xvt, yvt);
}
static final Interpolator sQuinticInterpolator = t -> {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
};
private final int[] mReusableIntPair = new int[2];
private class FlingTool implements Runnable {
private int mLastFlingX;
private int mLastFlingY;
OverScroller mOverScroller;
Interpolator mInterpolator = sQuinticInterpolator;
// When set to true, postOnAnimation callbacks are delayed until the run method completes
private boolean mEatRunOnAnimationRequest = false;
// Tracks if postAnimationCallback should be re-attached when it is done
private boolean mReSchedulePostAnimationCallback = false;
FlingTool(Context context) {
mOverScroller = new OverScroller(context, sQuinticInterpolator);
}
void fling(int xvt, int yvt) {
mLastFlingX = mLastFlingY = 0;
if (mInterpolator != sQuinticInterpolator) {
mInterpolator = sQuinticInterpolator;
mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
}
mOverScroller.fling(0, 0, xvt, yvt, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
internalPostOnAnimation();
}
}
private void internalPostOnAnimation() {
removeCallbacks(this);
ViewCompat.postOnAnimation(NSChildLayout3.this, this);
}
@Override
public void run() {
mReSchedulePostAnimationCallback = false;
mEatRunOnAnimationRequest = true;
final OverScroller scroller = mOverScroller;
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
int unconsumedX = x - mLastFlingX;
int unconsumedY = y - mLastFlingY;
mLastFlingX = x;
mLastFlingY = y;
int consumedX = 0;
int consumedY = 0;
// Nested Pre Scroll
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
TYPE_NON_TOUCH)) {
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
TYPE_NON_TOUCH, mReusableIntPair);
postOnAnimation();
}
mEatRunOnAnimationRequest = false;
if (mReSchedulePostAnimationCallback) {
internalPostOnAnimation();
} else {
stopNestedScroll(TYPE_NON_TOUCH);
}
}
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes, int type) {
return getScrollingChildHelper().startNestedScroll(axes, type);
}
@Override
public void stopNestedScroll(int type) {
getScrollingChildHelper().stopNestedScroll(type);
}
@Override
public boolean hasNestedScrollingParent(int type) {
return getScrollingChildHelper().hasNestedScrollingParent(type);
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
int type) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,
type);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
private NestedScrollingChildHelper mScrollingChildHelper;
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
}
结束
如果有其他成熟的方案,还请各位大佬引荐给我学习下,感谢!
参考文章
/post/684490…
/post/684490…
/post/684490…