做android开发也有很长一段时间类,一直没有仔细想过ScrollView是怎么实现的,如何实现滚动的,所以就去研究类一下其源码,顺便做一下笔记,望日后好查阅。俗话说好记性不如烂笔头嘛。小弟不才,哪里理解错了还望大神指教,再此先谢过。
理论上弄清楚源码是怎么做的,我们按照这个逻辑也可以写出一个的ScrollView的,所有我也写了一个ScrollView,留作参考。这个ScrollView对于滑动到边界的处理,只做了回弹的处理。所以支持边界阻尼回弹的ScrollView。
原理请参考:实现一个ScrollView
项目地址:https://github.com/cyuanyang/ScrollView.git
FillViewport
众所周知ScrollView有一个FillViewport属性,而他的实现也很简单,下面是源码,注释是依照我的理解自己加上去的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.如果是false 按照父视图的测量方式测量ScrollView的子View的宽高
// 即使你的子View设置math_parant 也只当者wrap_content处理
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
//2.如果设置mFillViewport=true 则会走这里开始测量子View的宽高
if (getChildCount() > 0) {
//3.因为ScrollView有且只有一个子View所以直接取第一个
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
//4.拿到布局参数
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
//5.计算padding
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
//6. desiredHeight 是scrollView的高度减去上下margin剩下的高度 如果child的高度小于这个才去测量
// 如果大于的话已经充满里没必要再折腾一次 源码的水平还是很有质量的
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
//7. 计算宽高 调用child的measure 完成
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
这里细心的人可能会有疑问,scrollView是FrameLayout的子类,而mFillViewport=false,会调用 super.onMeasure()测量子View的宽高,这样我们也会得到一个正确的值的。事实并不是这么简单的,再FrameLayout的测量View的方法中,测量child是有一个额外条件
if (mMeasureAllChildren || child.getVisibility() != GONE)
mMeasureAllChildren再mFillViewport为false的时候就是false
onInterceptTouchEvent
这个方法对于ScrollView是很关键的。如果想要滑动,肯定得返回true的,但是又不能全部返回true要不子View就接受不到事件了。这个方法就是处理何时该拦截事件。还是拿关键的源码说话。如果不懂mScroller或者VelocityTracker请参考实现一个ScrollView
case MotionEvent.ACTION_DOWN: {
// 1. 如果按下的位置在不在 子View上
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
/*
* 2. 记住down事件 取第一个手指
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
//3. 这个是计算速率的 主要用来计算手指离开后的fling的速率
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged. We need to call computeScrollOffset() first so that
* isFinished() is correct.
*
*/
//4. 下面是如何区分是点击子View还是拖动ScrollView 原因上面源码注释也很清楚
//如果mScroller再滚动 即认为是拖动 直接赋值true
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}
这个是move事件的处理
//6.如果先点击没有滑动,拦截事件中为false,ScrollView中的button也能接受到事件,这是再根据滑动的距离来决定是不是需要拦截事件
//mTouchSlop(这个值是一个系统值,判断滑动的一个阈值)
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
//7. 赋值down后的y的位置
mLastMotionY = y;
//8. 初始化速率轨迹计算 主要用来计算手指离开后的fling的速率
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
onTouchEvent
这个是ScrollView最关键,最关键,最关键的地方,重要的话说三遍。理解这个地方后自己就可以写出一个ScrollView了。还是拿代码说话吧
//代码不必要每一步都懂 只需要理解关键的地方即可,毕竟android是一个系统,考虑的很多很多,我们没有必要理解每一句代码的含义
//所以这里列举一下关键的地方
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;
}
刚刚在8中调用overScrollBy用来计算滚动的距离然后回调给onOverScrolled来处理是否需要滚动,这里就是处理逻辑
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
//这个if是用来区别mScroller滚动调用的还是手指拖动滚动的
//mScroller.isFinished()为true 就是手指拖动引起的滚动 直接调用super.scrollTo,这样就完成了滚动 完美
//if代码块其实就是一个和scrollTo的代码差不多,这里并没有直接调用我也不知道为什么,看注解也没太明白,哪位大神知道麻烦告诉我一下,谢谢。
if (!mScroller.isFinished()) {
final int oldX = mScrollX;
final int oldY = mScrollY;
mScrollX = scrollX;
mScrollY = scrollY;
invalidateParentIfNeeded();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
}
//显示滚动条 滚动条是View的方法,其实每个View都有滚动的功能的。
awakenScrollBars();
}
到此,ScrollView就能滚动了。
总结
浏览源码不是为了去写一个ScrollView,而是在看完之后我们学到了啥。就像小时候学校组织看电影一样,学校单纯的只想让你看完电影就算了,一般都会让我们写一篇读后感。haha。。。
OverScroller
如果你要是想做一个滚动的View,这个一定能帮助你实现梦想。 自带强大的滚动技能。一般配合VelocityTracker来计算fling滚动。
如何优雅的区分是点击还是滑动操作
当我们做一个滑动的容器组件的时候,当我们快速的滑动的时候,并不想让down事件传递下去,但同时又不影响点击容器内的View。我们可以这么做。这是在onInterceptTouchEvent中哦!
case MotionEvent.ACTION_DOWN:
.....
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
......
break;
return mIsBeingDragged;
可能会坑猿的地方
ScrollView会自动滚动到获取焦点的View上面。例如我们在ScrollView中放一个WebView,就会发现总是会滚动到WebView那里。笔者有一次用WebView来加载MathJax来渲染数学符号的时候就遇到这个坑。解决办法有很多。主要思路就是移除不必要的焦点。
scrollBy参数是Int 会丢失小数部分