嵌套滑动的预备知识
Scroller的用法
1.scroll方法
mScroller.startScroll(0, 0, -300, -300, 2000)
postInvalidate()//触发重绘
override fun computeScroll() {//重写computeScroll方法
if (mScroller.computeScrollOffset()) {
Log.d("vivi", "currX:${mScroller.currX}-----currY:${mScroller.currY}")
scrollTo(-mScroller.currX, -mScroller.currY)
postInvalidate()
}
}
复制代码
2.fling方法
mScroller.fling(0, 0, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), 0, width, 0, height)
postInvalidate()
复制代码
同样也需要重写computeScroll方法,fling方法需要传xy方向的速度单位是像素/秒,获取速度就需要用到VelocityTracker
VelocityTracker的用法
private val mVelocityTracker: VelocityTracker = VelocityTracker.obtain()//获取速度跟踪器
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.also {
mVelocityTracker.addMovement(event)//每次处理触摸事件时,我们将触摸事件通过addMovement方法传递给它
when (it.action) {
MotionEvent.ACTION_DOWN -> {
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP ->
mVelocityTracker.computeCurrentVelocity(1000)//up的时候计算此时的速度,参数为单位(此处返回的是以1000毫秒为单位的像素),并通过get方法获取xy方向的速度
mScroller.fling(0, 0, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), 0, width, 0, height)
postInvalidate()
}
}
}
return true
}
复制代码
同样也需要重写computeScroll方法
嵌套滑动的相关总结
-
嵌套滑动机制的由来
大家都知道touch事情的分发机制是一锤子买卖,要么不消费,要么把整个事件消费掉,如果需要处理嵌套滑动,那么就需要引入嵌套滑动
-
嵌套滑动机制相关接口与工具 NestedScrollingParent、NestedScrollingParentHelper、NestedScrollingChild、NestedScrollingChildHelper NestedScrollingParent、NestedScrollingChild是两个接口,需要支持嵌套滑动的父控件需要实现NestedScrollingParent,需要支持嵌套滑动的子控件需要实现NestedScrollingChild,NestedScrollingParentHelper、NestedScrollingChildHelper是两个帮助类,封装嵌套滑动机制的一些实现,我们只需要实现嵌套滑动接口接口,将接口定义的api用帮助进行代理,基本就可以实现嵌套滑动事件的传递了(具体参考下面的示例代码)
-
相关接口的调用执行流程
NestedChildView----setNestedScrollingEnabled
NestedChildView----startNestedScroll
NestedParentLayout---onStartNestedScroll
NestedParentLayout---onNestedScrollAccepted
NestedChildView----dispatchNestedPreScroll (分发嵌套滑动前的事件,由child调用,会触发onNestedPreScroll)
NestedParentLayout---onNestedPreScroll (当嵌套滑动前)
NestedChildView----dispatchNestedScroll (分发嵌套滑动后的事件,由child调用,会触发onNestedScroll)
NestedParentLayout---onNestedScroll (当嵌套滑动后)
NestedChildView----dispatchNestedPreFling (分发Fling前的事件,由child调用,会触发onNestedPreFling)
NestedParentLayout---onNestedPreFling (当Fling前)
NestedChildView----dispatchNestedFling (分发Fling后的事件,由child调用,会触发onNestedFling)
NestedParentLayout---onNestedFling (当Fling滑动后)
NestedChildView----stopNestedScroll
NestedParentLayout---onStopNestedScroll
图片源自网络,如有侵权,请联系删之
child端示例代码
package com.hotniao.project.nestedscrolldemo.view
import android.content.Context
import android.support.v4.view.NestedScrollingChild
import android.support.v4.view.NestedScrollingChildHelper
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View
import android.widget.Scroller
/**
* 项目名称:NestedScrollDemo
* 类描述:
* 创建人:kevinxie
*
*/
class NestedChild(context: Context?, attrs: AttributeSet?) : View(context, attrs), NestedScrollingChild {
private val mHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)
private val mVelocityTracker: VelocityTracker = VelocityTracker.obtain()
private val mScroller = Scroller(context)
private var mLastX: Float = 0.toFloat()//手指在屏幕上最后的x位置
private var mLastY: Float = 0.toFloat()//手指在屏幕上最后的y位置
private var mDownX: Float = 0.toFloat()//手指第一次落下时的x位置(忽略)
private var mDownY: Float = 0.toFloat()//手指第一次落下时的y位置
private val consumed = IntArray(2)//消耗的距离
private val offsetInWindow = IntArray(2)//窗口偏移
init {
isNestedScrollingEnabled = true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.also {
val x = it.x
val y = it.y
when (it.action) {
MotionEvent.ACTION_DOWN -> {
mDownX = x
mDownY = y
mLastX = x
mLastY = y
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL or ViewCompat.SCROLL_AXIS_VERTICAL)
}
MotionEvent.ACTION_MOVE -> {
val dx = x - mDownX
val dy = y - mDownY
var consumedDx = 0f
var consumedDy = 0f
if (dispatchNestedPreScroll(dx.toInt(), dy.toInt(), consumed, offsetInWindow)) {
consumedDx = dx - consumed[0]
consumedDy = dy - consumed[1]
offsetLeftAndRight(consumedDx.toInt())
offsetTopAndBottom(consumedDy.toInt())
} else {
consumedDx = dx
consumedDy = dy
offsetLeftAndRight(dx.toInt())
offsetTopAndBottom(dy.toInt())
}
if (dispatchNestedScroll(consumedDx.toInt(), consumedDy.toInt(), (dx - consumedDx).toInt(), (dy - consumedDy).toInt(), offsetInWindow)) {
}
}
MotionEvent.ACTION_UP -> {
mVelocityTracker.computeCurrentVelocity(1000)
if (dispatchNestedPreFling(mVelocityTracker.xVelocity, mVelocityTracker.yVelocity)) {
}
mScroller.fling(scrollX, scrollY, mVelocityTracker.xVelocity.toInt(), mVelocityTracker.yVelocity.toInt(), -1000, 1000, -1000, 1000)
if (dispatchNestedFling(mVelocityTracker.xVelocity, mVelocityTracker.yVelocity, true)) {
}
postInvalidate()
}
}
mLastX = x
mLastY = y
mVelocityTracker.addMovement(event)
}
return true
}
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean {
Log.d("vivi", "NestedChild---dispatchNestedScroll-----dxConsumed:$dxConsumed----dyConsumed:$dyConsumed----dxUnconsumed:$dxUnconsumed----dyUnconsumed:$dyUnconsumed")
return mHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
}
override fun isNestedScrollingEnabled(): Boolean {
Log.d("vivi", "NestedChild---isNestedScrollingEnabled-----")
return mHelper.isNestedScrollingEnabled
}
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
Log.d("vivi", "NestedChild---dispatchNestedPreScroll-----dx:$dx---dy:$dy----consumed[0]:${consumed?.get(0)}----consumed[1]:${consumed?.get(1)}")
return mHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
}
override fun stopNestedScroll() {
Log.d("vivi", "NestedChild---stopNestedScroll-----")
mHelper.stopNestedScroll()
}
override fun hasNestedScrollingParent(): Boolean {
Log.d("vivi", "NestedChild---hasNestedScrollingParent-----")
return mHelper.hasNestedScrollingParent()
}
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
Log.d("vivi", "NestedChild---dispatchNestedPreFling-----")
return mHelper.dispatchNestedPreFling(velocityX, velocityY)
}
override fun setNestedScrollingEnabled(enabled: Boolean) {
Log.d("vivi", "NestedChild---setNestedScrollingEnabled-----")
mHelper.isNestedScrollingEnabled = enabled
}
override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
Log.d("vivi", "NestedChild---dispatchNestedFling-----")
return mHelper.dispatchNestedFling(velocityX, velocityY, consumed)
}
override fun startNestedScroll(axes: Int): Boolean {
Log.d("vivi", "NestedChild---startNestedScroll-----")
return mHelper.startNestedScroll(axes)
}
private var mFlingLastX = 0
private var mFlingLastY = 0
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.d("vivi", "currX:${mScroller.currX}-----currY:${mScroller.currY}")
offsetLeftAndRight(mScroller.currX - mFlingLastX)
offsetTopAndBottom(mScroller.currY - mFlingLastY)
//scrollTo(mScroller.currX, mScroller.currY)
mFlingLastX = mScroller.currX
mFlingLastY = mScroller.currY
postInvalidate()
} else {
mFlingLastX = 0
mFlingLastY = 0
stopNestedScroll()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mHelper.onDetachedFromWindow()
}
}
复制代码
parent端示例代码
package com.hotniao.project.nestedscrolldemo.view
import android.content.Context
import android.support.v4.view.NestedScrollingParent
import android.support.v4.view.NestedScrollingParentHelper
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.FrameLayout
/**
* 创建人:kevinxie
*/
class NestedParent(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs), NestedScrollingParent {
private val mHelper: NestedScrollingParentHelper = NestedScrollingParentHelper(this)
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
Log.d("vivi", "NestedParent---onNestedScrollAccepted-----")
}
override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
Log.d("vivi", "NestedParent---onStartNestedScroll-----")
return true
}
override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
Log.d("vivi", "NestedParent---onNestedFling-----velocityX:$velocityX------velocityY:$velocityY----consumed:$consumed")
return true
}
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
Log.d("vivi", "NestedParent---onNestedPreFling-----velocityX:$velocityX------velocityY:$velocityY")
return true
}
override fun getNestedScrollAxes(): Int {
Log.d("vivi", "NestedParent---getNestedScrollAxes-----")
return mHelper.nestedScrollAxes
}
override fun onNestedPreScroll(target: View, dxs: Int, dys: Int, consumed: IntArray) {
Log.d("vivi", "NestedParent---onNestedPreScroll-----dx:$dxs---dy:$dys----consumed[0]:${consumed[0]}----consumed[1]:${consumed[1]}")
var dx = dxs
var dy = dys
if (dx > 0) {
if (target.right + dx > width) {
dx = target.right + dx - width//多出来的
offsetLeftAndRight(dx)
consumed[0] += dx//父亲消耗
}
} else {
if (target.left + dx < 0) {
dx += target.left
offsetLeftAndRight(dx)
consumed[0] += dx//父亲消耗
}
}
if (dy > 0) {
if (target.bottom + dy > height) {
dy = target.bottom + dy - height
offsetTopAndBottom(dy)
consumed[1] += dy
}
} else {
if (target.top + dy < 0) {
dy += target.top
offsetTopAndBottom(dy)
consumed[1] += dy//父亲消耗
}
}
}
override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {
Log.d("vivi", "NestedParent---onNestedScroll-----dxConsumed:$dxConsumed----dyConsumed:$dyConsumed----dxUnconsumed:$dxUnconsumed----dyUnconsumed:$dyUnconsumed")
}
override fun onStopNestedScroll(target: View) {
Log.d("vivi", "NestedParent---onStopNestedScroll-----")
}
}
复制代码
嵌套滑动的原理分析
按照上面的相关api的执行流程,以及示例代码,我们来一起阅读源码,首先我们需要在child调用setNestedScrollingEnabled方法,来设置是否支持嵌套滑动,我们一起看看helper里面的实现
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
复制代码
前面有个判断,如果嵌套滑动已经可用,会先停止嵌套滑动,最后跟进去
public void stopNestedScroll(View view) {
if (view instanceof NestedScrollingChild) {
((NestedScrollingChild) view).stopNestedScroll();
}
}
复制代码
其实就是调用了child的stopNestedScroll方法,那我们接着再看startNestedScroll方法
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
复制代码
先看hasNestedScrollingParent方法,直接调用了getNestedScrollingParentForType方法,继续跟
public boolean hasNestedScrollingParent(@NestedScrollType int type) {
return getNestedScrollingParentForType(type) != null;
}
private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
switch (type) {
case TYPE_TOUCH:
return mNestedScrollingParentTouch;
case TYPE_NON_TOUCH:
return mNestedScrollingParentNonTouch;
}
return null;
}
复制代码
很显然就是看有没有嵌套滑动的父控件,如有就直接返回true,表示有嵌套滑动的父控件来处理此次嵌套滑动,反之我们继续startNestedScroll,调用isNestedScrollingEnabled,如果不支持嵌套滑动就返回false,但是我们一开始就设置过支持嵌套滑动,可以看到进行遍历view树来寻找是否有嵌套滑动的父控件,ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)里面进行了判断是否是NestedScrollingParent,然后直接调用其onStartNestedScroll方法,回到startNestedScroll,如果找到嵌套滑动的父控件,就调用了setNestedScrollingParentForType方法,跟getNestedScrollingParentForType对应,避免了每次都去遍历整个树,然后又继续调用了父控件的onNestedScrollAccepted方法,看来前几步跟我们打印的流程完全一致,开始嵌套滑动,父控件也收到了嵌套滑动的开始了,那我们就需要通过子控件去分发自己的滑动事件了,先看dispatchNestedPreScroll
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) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码
代码稍微有点多,其实也就是做了一些健壮性的判断,最后还是调用了ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type),里面还是一样,调用了父控件的onNestedPreScroll,再继续看onNestedScroll
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码
跟dispatchNestedPreScroll代码相似,就是调用了ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,dyConsumed, dxUnconsumed, dyUnconsumed, type),从而调用了父控件的onNestedScroll,那dispatchNestedPreFling和dispatchNestedFling应该也类似吧,那我们继续看看dispatchNestedPreFling
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled()) {
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
velocityY);
}
}
return false;
}
复制代码
猜的没错,也还是调用ViewParentCompat.onNestedPreFling(parent, mView, velocityX,velocityY),其实dispatchNestedFling也是如此,也就是将一些事件进行传递给父控件,最后我们来看看
public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}
复制代码
很简单,就是调用了onStopNestedScroll,后面还将之前设置的parent进行置空,整个嵌套滑动机制也就是这样一个流程,总结下来感觉就是child通过接口把自己的滑动事件发送给父控件,根据父控件的消费的事件来做相应的处理。恩,如果我们把事件发送出去,通过其他的流程应该也可以仿写出其他的滑动机制...