先抛个问题 ScrollView.setonClickView ?
从Touch分发看滚动原理
SDK 基于 5.0
1 onInterceptTouchEvent 是否拦截Touch 如果返回true,则onTouchEvent会被调用,并处理滑动事件。
注意:ACTION_DOWN 时并未拦截
ACTION_CANCLE |UP 清除 mIsBeingDragged
ACTION_MOVE时mIsBeingDragged =true 的条件
1有效移动距离大于 mTouchSlop 2 方向SCROLL_AXIS_VERTICAL
sdk低版本 没有实现类似NestScrollParent嵌套滚动相关方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果是移动手势并在处于拖拽阶段,直接返回true
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
//如果并不能滑动则返回false
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
//检测用户是否移动了足够远的距离。
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
//得到当前触摸的y左边
final int y = (int) ev.getY(pointerIndex);
//计算移动的插值
final int yDiff = Math.abs(y - mLastMotionY);
//如果yDiff大于最小滑动距离,并且是垂直滑动则认为触发了滑动手势。
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
//标记拖动状态为true
mIsBeingDragged = true;
//赋值mLastMotionY
mLastMotionY = y;
//初始化mVelocityTracker并添加
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
//通知父布局不再拦截触摸事件
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
//触摸点不在子View内
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
//记录当前位置
mLastMotionY = y;
//记录pointer的ID,ACTION_DOWN总会在index 0
mActivePointerId = ev.getPointerId(0);
//初始化mVelocityTracker
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
//如果在滑动过程中则mIsBeingDragged = true
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
//回调NestedScroll相关接口
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//清除Drag状态
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
//回调NestedScroll相关接口
stopNestedScroll();
break;
case MotionEvent.ACTION_POINTER_UP:
//当多个手指触摸中有一个手指抬起时,判断是不是当前active的点,如果是则寻找新的
//mActivePointerId
onSecondaryPointerUp(ev);
break;
}
//最终根据是否开始拖拽的状态返回
return mIsBeingDragged;
}
2 onTouchEvent(MotionEvent ev)
在处理各种事件之前,首先初始化了VelocityTracker。并且复制一个新的MotionEvent对象用于计算加速度。
特别说明:
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop(); //系统默认滚动最小有效距离
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
public boolean onTouchEvent(MotionEvent ev) {
//1. 如果没有初始化速率轨迹 初始化它,这个还是用于手指离开后计算fling的
initVelocityTrackerIfNotExists();
MotionEvent vtev = MotionEvent.obtain(ev);
final int actionMasked = ev.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
//2.请求父视图不要拦截
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
//3. 如果当前在fling 就是mScroller还没有完成就触摸了
//立刻放弃当前的滚动
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
//4. 记住触摸的位置 mLastMotionY 这个值在move的时候用来计算手指移动的变化量,然后用来计算需要滚动的距离
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
//5. 这个是处理内部滚动 可以先不用管这个
//涉及到Nested的都可以先不用管它 这个好像是为了支持v4包内的某个功能做的处理
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
//6. deltaY 计算手指移动的距离 在4中记录的 同时下面还会更新这个值 8中会用到这个值来计算需要滚动的距离
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
//7. 如果先点击没有滑动,拦截事件中为false,ScrollView中的button也能接受到事件,这是再根据滑动的距离来决定是不是需要拦截事件
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
//更新mLastMotionY 这个很关键 否则根本滑不懂
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
//8. 调用overScrollBy方法计算滚动 这个方法就是计算一下滚动的距离然后回调给onOverScrolled()在这里调用scrollTo方法
// 到这里的时候 ScrollView还不会滚动,滚动的代码在onOverScrolled()中,紧接着下面会出现
// 这里返回true表示滑动超出了内容区域 像滑倒顶部会有阻尼的那种效果就可以用这个实现
// 这个是最关键的地方 关键的源码都有注释 厉害了word
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
//9.下面就没必要仔细去研究了 这里处理一下滑到边界出的效果
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
//10. 速率轨迹终于要大显神威了
// up后 8中的计算滚动就会停止,但是实际上ScrollView还会滚动一段距离
// 这里根据 VelocityTracker 得到手指离开这一瞬间的Velocity
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
//11. 速录很大 则会认为是一个fling 动作
// flingWithNestedDispatch()方法内部就是执行了mScroller.fling()方法
//else if 含义:速录很小,例如我们滑动最后停下来,然后手指离开屏幕,这时的速率可能为0,就不需要fling
//但是若滑动到顶部就需要回弹动画 ,直接动用 mScroller.springBack()即可
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity);
} else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
//12. 取消事件的处理 类似于up事件 理解上面的下面的多个触摸点的处理就很简单了
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
ACTION_DOWN:首先是给mIsBeingDragged赋值,接着检查是否在fling动画执行过程中,如果正在执行则停止,这也是为什么我们在ScrollView滑动过程中手指触摸时会终止ScrollView的滑动。最后记录了mLastMotionY与mActivePointerId。
ACTION_MOVE: 首先计算当前的垂直偏移量deltaY。然后判断是否大于最小滑动距离,并且给mIsBeingDragged赋值。接着如果mIsBeingDragged为true。就取得处理滑动需要的各种参数,并调用overScrollBy()方法来处理触摸事件,overScrollBy()是在View里实现的方法,大致实现如下
ACTION_UP: 最终调用
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, height/2); 执行滚动动画
fling执行流程 和 smoothScrollBy大概一致 最终执行 invalidate 开始绘制流程 通过computeScrollOffset 不断计算 是否滚动结束 未结束则 invalidate 不断重复至结束
View postInvalidateOnAnimation() invalidate() 见
https://blog.youkuaiyun.com/fyfcauc/article/details/43308467
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX);
mScrollerY.fling(startY, velocityY, minY, maxY, overY);
}
public boolean computeScrollOffset() {
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
//已经耗时和总时间的百分比
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
draw()和scroller关系 scrollY
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (shouldDisplayEdgeEffects()) {
final int scrollY = mScrollY;
final boolean clipToPadding = getClipToPadding();
if (!mEdgeGlowTop.isFinished()) {
...
canvas.translate(translateX, Math.min(0, scrollY) + translateY);
mEdgeGlowTop.setSize(width, height);
if (mEdgeGlowTop.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeGlowBottom.isFinished()) {
...
canvas.translate(-width + translateX,
Math.max(getScrollRange(), scrollY) + height + translateY);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
}
velocity决定了滚动触发到结束执行时间和最终滑动距离
再看绘制流程
补充 ScrollView测量部分
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); //framwlayout 测量child
//测量chid(0) EXACTLY
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
...
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
重写 measureChild()
和measureChildWithMargins ()
,将测量模式改成了UNSPECIFIED
,这样在测量时能够得到子View想要的高度。
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ mPaddingRight, lp.width);
final int verticalPadding = mPaddingTop + mPaddingBottom;
childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}