美文网首页
Android ScrollView 源码浅析

Android ScrollView 源码浅析

作者: ChrisChanSysu | 来源:发表于2020-07-12 11:00 被阅读0次

    OverView

    • A view group that allows the view hierarchy placed within it to be scrolled.
    • Scroll view may have only one direct child placed within it.
    • To add multiple views within the scroll view, make the direct child you add a view group, for example {@link LinearLayout}, and place additional views within that LinearLayout.
      从上面官网的描述中可以看到,ScrollView是一个只能有1个直接子View的可滑动的ViewGroup
      ScrollView只能纵向滑动,如果需要横向滑动则使用HorizontalScrollView,两者实现原理一致
      其继承层次为:
    public class ScrollView extends FrameLayout
    

    可以看到ScrollView继承自FrameLayout

    构造函数

        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();
    
            if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
                setRevealOnFocusHint(false);
            }
        }
    

    构造函数中只对1个属性:fillViewport进行了提取,这个属性的作用是,如果被设置成true,ScrollView的子View将充满视图

    绘制相关

    • 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) {
                final View child = getChildAt(0);
                final int widthPadding;
                final int heightPadding;
                final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
                final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
                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 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);
                }
            }
        }
    

    ScrollView的onMeasure()函数,先调用了父类的super.onMeasure(),随后如果mFillViewport属性为false,实际上函数就结束,也就是说在mFillViewport为false的情况下,ScrollView在测量截断是没有额外工作的;如果mFillViewport为true,就会调用一次child的measure()方法来重新测量大小以达到充满视图的效果

    • 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;
    
            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);
        }
    

    ScrollView的onLayout()过程也是先调用了父类的onLayout(),随后会判断是否需要滑动到子View的位置;最后一行scrollTo(mScrollX, mScrollY)将滑动到对应的位置。中间的isLaidOut()是View里面的方法,用于判断该View是否已经被添加到window上。

    • draw()
        @Override
        public void draw(Canvas canvas) {
            super.draw(canvas);
            if (mEdgeGlowTop != null) {
                final int scrollY = mScrollY;
                final boolean clipToPadding = getClipToPadding();
                if (!mEdgeGlowTop.isFinished()) {
                    final int restoreCount = canvas.save();
                    final int width;
                    final int height;
                    final float translateX;
                    final float translateY;
                    if (clipToPadding) {
                        width = getWidth() - mPaddingLeft - mPaddingRight;
                        height = getHeight() - mPaddingTop - mPaddingBottom;
                        translateX = mPaddingLeft;
                        translateY = mPaddingTop;
                    } else {
                        width = getWidth();
                        height = getHeight();
                        translateX = 0;
                        translateY = 0;
                    }
                    canvas.translate(translateX, Math.min(0, scrollY) + translateY);
                    mEdgeGlowTop.setSize(width, height);
                    if (mEdgeGlowTop.draw(canvas)) {
                        postInvalidateOnAnimation();
                    }
                    canvas.restoreToCount(restoreCount);
                }
                if (!mEdgeGlowBottom.isFinished()) {
                    final int restoreCount = canvas.save();
                    final int width;
                    final int height;
                    final float translateX;
                    final float translateY;
                    if (clipToPadding) {
                        width = getWidth() - mPaddingLeft - mPaddingRight;
                        height = getHeight() - mPaddingTop - mPaddingBottom;
                        translateX = mPaddingLeft;
                        translateY = mPaddingTop;
                    } else {
                        width = getWidth();
                        height = getHeight();
                        translateX = 0;
                        translateY = 0;
                    }
                    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);
                }
            }
        }
    

    在draw()阶段涉及到的变量主要是mEdgeGlowTop和mEdgeGlowBottom,这两个变量的类型是EdgeEffect,用于控制边缘的阴影效果,ScrollView的draw()阶段主要工作是绘制了上下阴影

    事件相关

    作为一个ViewGroup的子类,ScrollView与事件相关的有dispatchTouchEvent()/onInterceptTouchEvent()/onTouchEvent()这3个处理函数,其中ScrollView没有重写dispatchTouchEvent(),因此主要来关注下onInterceptTouchEvent()和onTouchEvent()这两个函数做了什么

    • onInterceptTouchEvent()
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            /*
             * This method JUST determines whether we want to intercept the motion.
             * If we return true, onMotionEvent will be called and we do the actual
             * scrolling there.
             */
    
            // 移动事件并且处于拖拽过程,这种状况肯定拦截
            final int action = ev.getAction();
            if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
                return true;
            }
    
            if (super.onInterceptTouchEvent(ev)) {
                return true;
            }
    
            // 如果没有滑动空间,则肯定不拦截
            if (getScrollY() == 0 && !canScrollVertically(1)) {
                return false;
            }
    
            switch (action & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_MOVE: {
                   // 走到这里,证明是移动事件,但还不是拖拽过程,需要检测用户是否移动了足够远
                    final int activePointerId = mActivePointerId;
                    if (activePointerId == INVALID_POINTER) {
                        break;
                    }
    
                    final int pointerIndex = ev.findPointerIndex(activePointerId);
                    if (pointerIndex == -1) {
                        Log.e(TAG, "Invalid pointerId=" + activePointerId
                                + " in onInterceptTouchEvent");
                        break;
                    }
                    
                    // 下面几行是用这一次事件的y和上次的y来进行差值,看是否大于最小滑动距离,是的话就需要标记拖拽状态为true并且进行一系列的
                    final int y = (int) ev.getY(pointerIndex);
                    final int yDiff = Math.abs(y - mLastMotionY);
                    if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                        mIsBeingDragged = true;
                        mLastMotionY = y;
                        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内,将拖拽状态置为false
                    if (!inChild((int) ev.getX(), (int) y)) {
                        mIsBeingDragged = false;
                        recycleVelocityTracker();
                        break;
                    }
    
                    // 记录下当前按下的位置
                    mLastMotionY = y;
                    mActivePointerId = ev.getPointerId(0);
    
                    initOrResetVelocityTracker();
                    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.
                    */
                    mScroller.computeScrollOffset();
                    mIsBeingDragged = !mScroller.isFinished();
                    if (mIsBeingDragged && mScrollStrictSpan == null) {
                        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                    }
                    startNestedScroll(SCROLL_AXIS_VERTICAL);
                    break;
                }
    
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    // 抬起动作,重置拖拽状态为false
                    mIsBeingDragged = false;
                    mActivePointerId = INVALID_POINTER;
                    recycleVelocityTracker();
                    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                        postInvalidateOnAnimation();
                    }
                    stopNestedScroll();
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    onSecondaryPointerUp(ev);
                    break;
            }
    
           
            return mIsBeingDragged;
        }
    

    由官方注释可以看出,onInterceptTouchEvent()只是决定是否拦截事件,而并不处理触摸逻辑。

    • onTouchEvent()
      ScrollView的onTouchEvent()源码结构是这样的:
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            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: {
                    // ……
                }
                case MotionEvent.ACTION_MOVE:
                    // ……
                case MotionEvent.ACTION_UP:
                   // ……
                case MotionEvent.ACTION_CANCEL:
                    // ……
                case MotionEvent.ACTION_POINTER_DOWN: {
                   // ……
                case MotionEvent.ACTION_POINTER_UP:
                    // ……
            }
    
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(vtev);
            }
            vtev.recycle();
            return true;
        }
    

    首先会初始化VelocityTracker,这个类的作用是追踪触摸事件的加速度;最后会调用其addMovement()函数,中间部分是对各个动作的具体处理,下面看下各个动作都会怎样处理:

    • ACTION_DOWN
                case MotionEvent.ACTION_DOWN: {
                    if (getChildCount() == 0) {
                        return false;
                    }
                    if ((mIsBeingDragged = !mScroller.isFinished())) {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
    
                    /*
                     * mScroller.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;
                }
    
    • ACTION_MOVE
                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);
                    int deltaY = mLastMotionY - y;
                    if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                        deltaY -= mScrollConsumed[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    }
                    // 如果还没有处在拖拽状态并且滑动距离大于最小滑动距离
                    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 = 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) {
                            // 这里面的逻辑是和边缘阴影的处理有关
                            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事件中需要关注的是,会调用View的overScrollBy()方法来真正处理触摸事件,overScrollBy()方法实现如下:

        protected boolean overScrollBy(int deltaX, int deltaY,
                int scrollX, int scrollY,
                int scrollRangeX, int scrollRangeY,
                int maxOverScrollX, int maxOverScrollY,
                boolean isTouchEvent) {
            final int overScrollMode = mOverScrollMode;
            final boolean canScrollHorizontal =
                    computeHorizontalScrollRange() > computeHorizontalScrollExtent();
            final boolean canScrollVertical =
                    computeVerticalScrollRange() > computeVerticalScrollExtent();
            final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
                    (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
            final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
                    (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
    
            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;
        }
    

    主要的流程就是根据传入的各个滑动参数,以及View本身与滑动相关的属性,计算出newScrollX/newScrollY等参数,然后调用onOverScrolled()方法来进行实际的滑动处理
    onOverScrolled()方法在View中是空实现,因此具体的实现在ScrollView中:

        @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();
        }
    

    有两条分支,mScroller.isFinished()为false证明上次的滑动还没有结束,这时需要计算出新的滑动位置进行滑动;如果此时不在滑动,则调用View的scrollTo()方法滑动到对应位置;最后一行awakenScrollBars()作用是让ScrollBar显示出来

    • ACTION_UP
               case MotionEvent.ACTION_UP:
                    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;
                        endDrag();
                    }
                    break;
    

    当手指抬起时,ScrollView的操作很简单,先判断是否处于滑动状态,如果不是就什么都不用干;如果是的话,需要让滑动继续惯性向前滑一段,这里借助了VelocityTracker和Scroller来实现。

    相关文章

      网友评论

          本文标题:Android ScrollView 源码浅析

          本文链接:https://www.haomeiwen.com/subject/nrsgahtx.html