- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
2. 源码分析
2.1 继承关系
2.2 主要辅助类
//用来计算滑动位置 private OverScroller mScroller; //用来绘制边缘阴影 private EdgeEffect mEdgeGlowTop; private EdgeEffect mEdgeGlowBottom; //用于计算滑动时的加速度 private VelocityTracker mVelocityTracker;
2.3 构造方法
ScrollView
的构造方法如下:
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initScrollView(); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes); setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false)); a.recycle(); }
在构造方法中分别调用了 initScrollView()
与 setFillViewport()
方法,代码如下:
private void initScrollView() { //初始化 OverScroller mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); final ViewConfiguration configuration = ViewConfiguration.get(mContext); //被认为是滑动操作的最小距离 mTouchSlop = configuration.getScaledTouchSlop(); //最小加速度 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); //最大加速度 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); //用手指拖动超过边缘的最大距离 mOverscrollDistance = configuration.getScaledOverscrollDistance(); //滑动超过边缘的最大距离 mOverflingDistance = configuration.getScaledOverflingDistance(); }
可以看到是初始化了一些类与参数,继续看看 setFillViewport()
:
public void setFillViewport(boolean fillViewport) { if (fillViewport != mFillViewport) { mFillViewport = fillViewport; requestLayout(); } }
只是根据布局文件中的 fillViewport
属性来给 mFillViewport
赋值并调用 requestLayout()
方法。 mFillViewport
如果为 true
则表示:将子 View
的高度延伸到和视图高度一致,即充满整个视图。初始化结束之后,会进入到绘制流程。下面我们按照 Measure
-> Layout
-> Draw
的绘制流程来分析 ScrollView
中的实现。
2.4 Measure、Layout 与 Draw
2.4.1 onMeasure 方法的实现
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { // 获取子 View final View child = getChildAt(0); // 获取 ScrollView 的高度 final int height = getMeasuredHeight(); if (child.getMeasuredHeight() < height) { final int widthPadding; final int heightPadding; final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; // 获取 ScrollView 的 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; } final int childWidthMeasureSpec = getChildMeasureSpec( widthMeasureSpec, widthPadding, lp.width); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height - heightPadding, MeasureSpec.EXACTLY); //根据新的高度重新 measure 子 View child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } }
从代码中可以看到首先调用了 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
即父类 FrameLayout
的 onMeasure()
方法。如果我们将 mFillViewport
设置为 false
的话将会直接 return
。当为 true
时才会继续执行,会根据子 View
的高度和 ScrollView
本身的高度决定是否重新 measure
子 View
使其充满 ScrollView
。 ScrollView
的 onMeasure()
其实就是处理了 mFillViewport
。
2.4.1 onLayout 方法的实现
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mIsLayoutDirty = false; // Give a child focus if it needs it if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { scrollToChild(mChildToScrollTo); } mChildToScrollTo = null; //是否还未添加过 window 中去 if (!isLaidOut()) { if (mSavedState != null) { mScrollY = mSavedState.scrollPosition; mSavedState = null; } // mScrollY default value is "0" final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; final int scrollRange = Math.max(0, childHeight - (b - t - mPaddingBottom - mPaddingTop)); // Don't forget to clamp if (mScrollY > scrollRange) { mScrollY = scrollRange; } else if (mScrollY < 0) { mScrollY = 0; } } // Calling this with the present values causes it to re-claim them scrollTo(mScrollX, mScrollY); }
首先也是调用了父类的 onLayout
方法。接下来处理了是否有需要滚动到的 View
,以及根据保存的滚动状态来决定是否需要滚动。如果需要则调用 scrollTo()
方法。
2.4.1 draw 方法的实现
@Override public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeGlowTop != null) { final int scrollY = mScrollY; final boolean clipToPadding = getClipToPadding(); if (!mEdgeGlowTop.isFinished()) { ...... if (mEdgeGlowTop.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } if (!mEdgeGlowBottom.isFinished()) { ...... if (mEdgeGlowBottom.draw(canvas)) { postInvalidateOnAnimation(); } canvas.restoreToCount(restoreCount); } } }
依然是调用了父类的 draw
方法。之后则是根据是否需要绘制边缘阴影来绘制阴影。 ScrollView
的边缘阴影就是在这里绘制的。值得一提的是包括 ListView
以及 RecycleView
的边缘阴影都是用这种方法来绘制的。以上就是 ScrollView
的整个绘制流程。可以看出都是调用了父类的对应方法。自身只处理了一些与 ScrollView
相关的属性。分析完绘制流程我们就来看看 ScrollView
中的触摸事件处理机制,来看看 ScrollView
中的滑动滚动到底是如何做到的:
2.5 触摸事件处理
说到触摸事件的分发与消费机制这算是一个比较基础的知识。但是要是完全掌握也并不是那么容易的,这里推荐一篇文章 Android:View 的事件分发与消费机制 。对事件处理机制还不了解的同学可以先看看这边文章。 ScrollView
因为是继承自 ViewGroup
的,所以触摸事件会依次调用 dispatchTouchEvent()
-> onInterceptTouchEvent()
若返回 true
-> onTouchEvent()
处理触摸事件。 ScrollView
并没有重写 dispatchTouchEvent()
方法,所以我们从 onInterceptTouchEvent()
方法来看。
2.5.1 onInterceptTouchEvent 方法的实现
//这个方法只决定我们是否拦截这个手势,如果返回 true,则 onMotionEvent 会被调用,并处理滑动事件。 //此方法并不处理事件 @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; }
以上就是 onInterceptTouchEvent()
的整体实现。 onInterceptTouchEvent()
只决定是否拦截触摸事件并交给 onTouchEvent()
处理。内部并不处理触摸逻辑。 ScrollView
中根据 mIsBeingDragged
来决定是否拦截事件。当手指按下发生 MotionEvent.ACTION_DOWN
时,会记录当前位置并检测是否在快速滚动过程中如果是则返回 true
。
当手指移动发生 MotionEvent.ACTION_MOVE
时,会判断是否是垂直方向上的滑动事件,如果是则返回 true
。当手指抬起发生 MotionEvent.ACTION_UP
时,则清除状态并返回 false
。在返回 true
的情况中, onTouchEvent()
方法就会被调用来处理触摸事件。我们继续来看 onTouchEvent()
方法的实现。
2.5.2 onTouchEvent 方法的实现
在看 onTouchEvent()
的实现之前,我们知道在 ScrollView
中手指无论怎么移动,只会有垂直方向上的滑动发生。而触摸事件的大致流程是:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
我们根据事件的类型分别来分析:
- ACTION_DOWN:
ACTION_DOWN
代表手指按下时第一个发生的事件,在onTouchEvent()
中实现如下:
@Override public boolean onTouchEvent(MotionEvent ev) { //初始化 VelocityTracker initVelocityTrackerIfNotExists(); //复制当前的 MotionEvent 赋值给 vtev MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } //调整 vtev 的偏移量 vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } // 将!mScroller.isFinished() 赋值给 mIsBeingDragged if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } //如果正在 fling 状态并且用户触摸。则停止 fling。 //当处于 fling 过程中 isFinished 为 false。 //fling :即快速滑动。 if (!mScroller.isFinished()) { //停止 mScroller.abortAnimation(); if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); mFlingStrictSpan = null; } } //记录触摸事件的初始值 mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); startNestedScroll(SCROLL_AXIS_VERTICAL); break; } ...... } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return true; }
在处理各种事件之前,首先初始化了 VelocityTracker
。并且复制一个新的 MotionEvent
对象用于计算加速度。接着开始处理 ACTION_DOWN
:首先是给 mIsBeingDragged
赋值,接着检查是否在 fling
动画执行过程中,如果正在执行则停止,这也是为什么我们在 ScrollView
滑动过程中手指触摸时会终止 ScrollView
的滑动。最后记录了 mLastMotionY
与 mActivePointerId
。
- ACTION_MOVE: 当手指移动时,会产生
ACTION_MOVE
事件:
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (actionMasked) { ...... //如果为 ACTION_MOVE 事件时 case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } //得到当前 y 值 final int y = (int) ev.getY(activePointerIndex); //计算偏移量 deltaY int deltaY = mLastMotionY - y; //如果 dispatchNestedPreScroll 返回 true,即有 NestedScroll 存在 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } //如果还未处于 drag 状态,并且 deltaY 大于最小滑动距离, //则赋值 mIsBeingDragged 为 true 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) { //记录当前的 y 值 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); // 调用 overScrollBy() 方法处理滑动事件。 if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true) && !hasNestedScrollingParent()) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } final int scrolledDeltaY = mScrollY - oldY; final int unconsumedY = deltaY - scrolledDeltaY; //处理 NestedScroll if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } else if (canOverscroll) { //如果 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; } ...... }
ACTION_MOVE
事件中,首先计算当前的垂直偏移量 deltaY
。然后判断是否大于最小滑动距离,并且给 mIsBeingDragged
赋值。接着如果 mIsBeingDragged
为 true
。就取得处理滑动需要的各种参数,并调用 overScrollBy()
方法来处理触摸事件, overScrollBy()
是在 View
里实现的方法,大致实现如下:
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { ..... int newScrollX = scrollX + deltaX; if (!overScrollHorizontal) { maxOverScrollX = 0; } int newScrollY = scrollY + deltaY; if (!overScrollVertical) { maxOverScrollY = 0; } // Clamp values if at the limits and record final int left = -maxOverScrollX; final int right = maxOverScrollX + scrollRangeX; final int top = -maxOverScrollY; final int bottom = maxOverScrollY + scrollRangeY; boolean clampedX = false; if (newScrollX > right) { newScrollX = right; clampedX = true; } else if (newScrollX < left) { newScrollX = left; clampedX = true; } boolean clampedY = false; if (newScrollY > bottom) { newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { newScrollY = top; clampedY = true; } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); return clampedX || clampedY; }
方法的参数很清楚了应该不难理解,可以看到在 overScrollBy()
方法中根据我们传入的参数以及 View
本身是否可以滑动的设定,等等来最终决定了新的 newScrollX
与 newScrollY
。接着调用了 onOverScrolled()
方法来处理滑动, onOverScrolled()
方法在 View
中是空实现,所以再回到 ScrollView
中可以看到重写了 onOverScrolled()
方法:
@Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Treat animating scrolls differently; see #computeScroll() for why. 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); } awakenScrollBars(); }
首先是有一句注释是说:不同的对待滑动动画,查看 computeScroll()
方法看原因。说到滑动动画一定是和 Scroller
相关了,目前我们还没涉及到,下面我们再谈。回到这里看到根据 !mScroller.isFinished()
来判断,根据前面的判断得知,要么是滑动动画并不存在,要么就已经被终止,所以在这里 !mScroller.isFinished()
为 false
。所以会调用 super.scrollTo(scrollX, scrollY);
最终产生滑动。到这里手指触摸产生的滑动就分析完了。
- ACTION_UP:
ACTION_UP
是当我们手指离开时产生的事件,在ScrollView
中当我们手指离开时,会根据当前的加速度再滑动一段距离。具体的实现我们来看看是如何实现的:
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (actionMasked) { ...... case MotionEvent.ACTION_UP: //如果实在 drag 状态中 if (mIsBeingDragged) { //计算加速度 final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); //如果有有效的加速度 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { //处理带有加速度的滑动事件 flingWithNestedDispatch(-initialVelocity); } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } mActivePointerId = INVALID_POINTER; //清除 drag 状态 endDrag(); } break; } ...... }
可以看到代码并不复杂,在计算了加速度后,调用了 flingWithNestedDispatch(-initialVelocity);
:
private void flingWithNestedDispatch(int velocityY) { final boolean canFling = (mScrollY > 0 || velocityY > 0) && (mScrollY < getScrollRange() || velocityY < 0); if (!dispatchNestedPreFling(0, velocityY)) { dispatchNestedFling(0, velocityY, canFling); if (canFling) { fling(velocityY); } } }
代码如上,我们这里不考虑 NestedFling
的方式,所以 dispatchNestedPreFling(0, velocityY)
默认会返回 false
,所以最终会执行 fling(velocityY);
:
public void fling(int velocityY) { if (getChildCount() > 0) { int height = getHeight() - mPaddingBottom - mPaddingTop; int bottom = getChildAt(0).getHeight(); mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, height/2); if (mFlingStrictSpan == null) { mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling"); } postInvalidateOnAnimation(); } }
可以看到是调用了 mScroller
的 fling
方法,在上一篇 Scroller 源码分析 中,我们已经详细解释了 Scroller
的原理, ScrollView
中虽然使用的是 OverScroller
但是使用方法也是类似的。所以在调用了 mScroller
的 fling
方法后。我们需要在 computeScroll()
处理 mScroller
计算出的值。 ScrollView
中的 computeScroll()
方法实现如下:
@Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int oldX = mScrollX; int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 0, mOverflingDistance, false); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (canOverscroll) { if (y < 0 && oldY >= 0) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } else if (y > range && oldY <= range) { mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); } } } if (!awakenScrollBars()) { // Keep on drawing until the animation has finished. postInvalidateOnAnimation(); } } else { if (mFlingStrictSpan != null) { mFlingStrictSpan.finish(); mFlingStrictSpan = null; } } }
我省略了一些注释,意思是说: computeScroll()
会在绘制的过程中调用,为了不重复的显示滚动条。这里重复做了 scrollTo()
方法中的代码。但并没有调用 scrollTo()
,因为 scrollTo()
中也有滚动条相关的处理。所以 computeScroll()
中也调用了 overScrollBy()
方法处理滑动。所以最终仍然会调用 onOverScrolled()
方法:
@Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Treat animating scrolls differently; see #computeScroll() for why. 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); } awakenScrollBars(); }
这次就会进入到第一个 if
语句里,可以看到是给 mScrollX
与 mScrollY
赋值后调用了 invalidateParentIfNeeded();
方法来完成最终的滑动处理。
- ACTION_POINTER_DOWN
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (actionMasked) { ...... case MotionEvent.ACTION_POINTER_DOWN: { //更新状态,即新的触摸手势决定是否滑动。 final int index = ev.getActionIndex(); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; } } ...... }
ACTION_POINTER_DOWN
是指有另外一个手指发生了触摸。这里的处理是将 mActivePointerId
赋值给新的点了。所以在 ScrollView
中当有一个手指按下,我们再按下另一个手指时,第二个按下的手指能决定 ScrollView
的滑动。
- ACTION_POINTER_UP
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (actionMasked) { ...... case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } ...... }
ACTION_POINTER_UP
是指多个手指中的一个手指离开屏幕。所以这里会检测是否是当前 active
的手指离开了,并做相应的处理,具体逻辑在 onSecondaryPointerUp(ev);
方法中,我们就不多解释了。至此整个 ScrollView
我们应该有了一个清晰完整的理解了。最后再分享一个小 trick
。一行代码实现仿 ios
的弹性 ScrollView
。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论