欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ScrollView源码分析

程序员文章站 2022-05-31 09:06:31
...

前言

Scrollview是我们经常使用的控件,假如一个界面的高度大于屏幕高度的时候,使用它可以很方便的实现一个界面的滑动显示,如果没有它,那么你的布局句会被压缩或者显示不全。一直想看看Scrollview内部怎么实现的,因为学习自定义view和自定义布局最好的老师就是源码,现在就来看看scrollview的源码。

首先先看注释

Scrollview是一个这样的view group,让放在它里面的view可以滑动的。
Scrollview里面只能有一个子view,为了让多个子 view可以滑动,你可以先放一个布局包括他们
Scrollview只支持竖直方向的滑动,如果想要水平滑动请使用HorizontalScrollView
永远不要把RecyclerView或者ListView包括在Scrollview的里面,这样使用会让你的用户体验很差
对于竖直方向的滑动还可以考虑NestedScrollView,有着更加强大灵活的用户接口,和对Material design有着更好的支持

尊重作者劳动
转载请注明出处 https://blog.csdn.net/dreamsever/article/details/80861641

进入源码

首先ScrollView 继承自 FrameLayout,我们看源码就是一个学习的过程,学习他们的写法,为什么这里继承了FrameLayout而不是直接ViewGroup,FrameLayout基本是是我们常用的布局里面最简单的ViewGroup,为什么是FrameLayout而不是直接ViewGroup我还知道,但是Google工程师这么做一定有他的道理,比如FrameLayout不消耗太多性能恰巧又做了一些Scrollview需要的前提工作,是不是这样只有看代码了。ScrollView 虽然继承自 FrameLayout,但是它还是ViewGroup,我们看一个ViewGroup需要关心哪些呢?自定义View我们肯定关心onMeasure,onDraw,因为我们关心的是这个View长什么样子,也许还有一些手势操作,自定义ViewGroup我们关心的就是onMeasure,onLayout方法了,这里是ScrollView肯定也少不了手势相关的逻辑

带着疑问进入源码

对于ScrollView的疑问,我想最大的就是,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);
//设置是否延伸内容,用于解决一些留白问题
//当子控件的高度小于ScrollView的高度时,会导致屏幕下面留白
    setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));

    a.recycle();

    if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
        setRevealOnFocusHint(false);
    }
}

先看initScrollView()
mScroller,当看到这个名字的时候我就感觉它不是个青铜,这个控件叫ScrollView它叫Scroller,难道滑动就是靠它实现的

private void initScrollView() {
//新建一个OverScroller
    mScroller = new OverScroller(getContext());
//设置可获取焦点
    setFocusable(true);
//设置后代的获取焦点的情况
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
//如果不绘制,这样设置让性能更好
    setWillNotDraw(false);
//ViewConfiguration包含着关于View的各种常量,配置信息
    final ViewConfiguration configuration = ViewConfiguration.get(mContext);
//仿误触,低于这个值忽略用户手势操作
    mTouchSlop = configuration.getScaledTouchSlop();
//最小速率
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//最大速率
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
//到View的边的时候还能滑动的最大距离
    mOverscrollDistance = configuration.getScaledOverscrollDistance();
//
    mOverflingDistance = configuration.getScaledOverflingDistance();
//
    mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
}

onMeasure

现在构造方法走完了,我们直接去看onMeasure 方法,onMeasure方法直接

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

//当mFillViewport 为false的时候直接返回,表示走的是父类FrameLayout的测量方法
//mFillViewport为true重新走一遍测量
    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);
        }
    }
}

首先关于mFillViewport

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:background="@color/color_yellow">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="text"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="button"/>
    </LinearLayout>
</ScrollView>

ScrollView源码分析

ScrollView源码分析

前者是没有设置mFillViewport,后者是设置了mFillViewport为true,可见mFillViewport为true使LinearLayout 的高这个属性生效了。既然是ScrollView继承自FrameLayout,那么当把ScrollView换成
FrameLayout

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_yellow">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="text"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="button"/>
    </LinearLayout>
</FrameLayout>

发现显示效果和后者是一样的,不应该啊,不设置fillViewport属性也就是默认为false,那么走的onMeasure方法是FrameLayout的onMeasure,为什么显示不一样呢,原来是ScrollView重写了measureChildWithMargins方法,使子控件,也就是LinearLayout显示的高度为 模式为MeasureSpec.UNSPECIFIED

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

onLayout

下面看onLayout方法,onLayout方法没什么说的,基本上是用了父类FrameLayout的布局方法,加入了一些scrollTo操作滑动到指定位置

@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拿掉,仅仅留下里面的布局,运行你会发现,下面的Button也是看不到了,为什么因为TextView太高,把Button顶到屏幕外面了,现在你去触摸滑动屏幕是没有效果的,滑不动。假如这时候外面包一个ScrollView你就可以向上滑动然后看到下面的button了。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="2000dp"
        android:text="text"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"/>
</LinearLayout>

既然是这种效果,那么就要看View的几个手势事件了
事件分发流程ViewRootImpl–>DecorView(DecorView包涵于PhoneWindow)–>Activity–>PhoneWindow–>DecorView,然后一级一级传递给我们的布局以及ziview,事件的传递主要牵涉到一下几个方法:

dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

触摸事件先来到dispatchTouchEvent,搜索ScrollView源码发现并没有复写dispatchTouchEvent方法,FrameLayout也没有实现这个方法,也就是处理逻辑和ViewGroup是一样的。
直接去看onInterceptTouchEvent方法


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    /*这个方法仅仅决定是否拦截事件,假如返回true,onTouchEvent事件会调用去处理滑动
     * 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.
     */

    /*用户触摸着屏幕并且滑动手指,我们要拦截这个事件
    * Shortcut the most recurring case: the user is in the dragging
    * state and he is moving his finger.  We want to intercept this
    * motion.
    */
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }

    if (super.onInterceptTouchEvent(ev)) {
        return true;
    }

    /*滑到最上面了
     * Don't try to intercept touch if we can't scroll anyway.
     */
    if (getScrollY() == 0 && !canScrollVertically(1)) {
        return false;
    }

    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            /*
             * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
             * whether the user has moved far enough from his original down touch.
             */

            /*
            * Locally do absolute value. mLastMotionY is set to the y value
            * of the down event.
            */
            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;
            }

            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);//向上或者向下的滑动距离
            //假如yDiff大于防误触的距离,并且当前嵌套模式不是SCROLL_AXIS_VERTICAL
            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();
            //是不是在子布局区域里面,不在的话拖动模式为false
            if (!inChild((int) ev.getX(), (int) y)) {
                mIsBeingDragged = false;
                recycleVelocityTracker();//回收速率追踪器
                break;
            }

            /*记录按下位置
             * Remember location of down touch.
             * ACTION_DOWN always refers to pointer index 0.
             */
            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");
            }
            //开始对相应轴也就是对VERTICAL方向的嵌套滑动,这是特殊情况,先不考虑嵌套滑动的情况
            startNestedScroll(SCROLL_AXIS_VERTICAL);
            break;
        }

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            //对于up和cancel事件
            /* Release the drag */
            //释放触摸或者拖动事件
            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;
    }

    /*
    * The only time we want to intercept motion events is if we are in the
    * drag mode.
    */
    return mIsBeingDragged;
}

onInterceptTouchEvent事件就是做一件事,决定事件是不是要继续交给自己的onTouchEvent处理,经过断点发现,滑动时onInterceptTouchEvent都返回false,表示所有的点击触摸事件都先给被包含的子View,子View不处理在给ScrollView的onTouchEvent,ScrollView的ACTION_MOVE事件不会经过onInterceptTouchEvent,因为当onTouchEvent消费了事件时,ACTION_DOWN以后的事件将不在经过onInterceptTouchEvent,直接由onTouchEvent处理后续所有的事件。

  • 注意:断点时需要把源码版本和手机系统版本对应

onTouchEvent事件
可以看到最后onTouchEvent都返回了true,来者不拒 return true; 这里也就可以解释为什么onInterceptTouchEvent方法看不到ACTION_MOVE事件了,加入子view不处理事件,只有一种可能后续事件都被onTouchEvent返回true消费了。

对于Scroll这个动作来说,滑动分为两种
drag :拖曳操作
fling : 快速滚动

onTouchEvent的ACTION_MOVE事件记录了按下时的y即开始的位置和mActivePointerId--第一个触摸点的位置id


@Override
public boolean onTouchEvent(MotionEvent ev) {
    initVelocityTrackerIfNotExists();

    //从ev里面copy出一个新的MotionEvent
    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: {
            //子控件数量为0什么也不做
            if (getChildCount() == 0) {
                return false;
            }
            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.
             */
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                if (mFlingStrictSpan != null) {
                    mFlingStrictSpan.finish();
                    mFlingStrictSpan = null;
                }
            }

            //记录滑动开始位置
            // Remember where the motion event started
            mLastMotionY = (int) ev.getY();
            mActivePointerId = ev.getPointerId(0);
            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);
            int deltaY = mLastMotionY - y;//滑动距离
            //先不考虑嵌套滑动的情况,忽略这个判断
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            //mIsBeingDragged为false并且滑动距离大于最小触发距离,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) {
                // 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);

                // Calling overScrollBy will call onOverScrolled, which
                // calls onScrollChanged if applicable.
                //overScrollBy会执行滑动操作,具体如何执行会分两种情况1,拖动状态,2,fling状态
                //具体去看onOverScrolled方法
                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;
                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:
            //拖动状态并且手指离开了,这时候就要看看滑动速率是不是触发fling状态
            if (mIsBeingDragged) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                //如果当前的速率绝对值大于最小触发速率,执行fling滑动
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                        getScrollRange())) {
                    postInvalidateOnAnimation();
                }

                mActivePointerId = INVALID_POINTER;
                //结束拖动状态
                endDrag();
            }
            break;
        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;
    }

    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}

onOverScrolled

@Override
protected void onOverScrolled(int scrollX, int scrollY,
        boolean clampedX, boolean clampedY) {
    // Treat animating scrolls differently; see #computeScroll() for why.
    if (!mScroller.isFinished()) {//fling状态回调滑动改变
        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();
}
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);
        }
    }
}

/**
 * Fling the scroll view
 * 快速滑动ScrollView
 * @param velocityY The initial velocity in the Y direction. Positive
 *                  numbers mean that the finger/cursor is moving down the screen,
 *                  which means we want to scroll towards the top.
 */
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();
    }
}

OverScroller.java

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


//  mScrollerY.fling
void fling(int start, int velocity, int min, int max, int over) {
    mOver = over;
    mFinished = false;
    mCurrVelocity = mVelocity = velocity;
    mDuration = mSplineDuration = 0;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mCurrentPosition = mStart = start;

    if (start > max || start < min) {
        startAfterEdge(start, min, max, velocity);
        return;
    }

    mState = SPLINE;
    double totalDistance = 0.0;

    if (velocity != 0) {
        mDuration = mSplineDuration = getSplineFlingDuration(velocity);
        totalDistance = getSplineFlingDistance(velocity);
    }

    mSplineDistance = (int) (totalDistance * Math.signum(velocity));
    mFinal = start + mSplineDistance;

    // Clamp to a valid final position
    if (mFinal < min) {
        adjustDuration(mStart, mFinal, min);
        mFinal = min;
    }

    if (mFinal > max) {
        adjustDuration(mStart, mFinal, max);
        mFinal = max;
    }
}

看到这里就有点疑惑了,在onTouchEvent方法里面的MotionEvent.ACTION_MOVE事件里面,先调用了overScrollBy,然后里面调用了onOverScrolled,在这里分两种情况一种是拖动状态,直接走super.scrollTo(scrollX, scrollY);,然后是fling状态,最后调用OverScroller的fling方法,SplineOverScroller mScrollerY的fling方法,但是都没有看到ScrollView执行scrollTo或者scrollBy方法

摘录:https://blog.csdn.net/huangbiao86/article/details/23220251
1、它是怎样滑动View的(如何与View关联的)?
2、又是谁触发了它?

其实要分析这两个问题,主要还得从View的绘制流程开始分析:
关于View的绘制流程,网上资料众多,基本上相差无几,这里就不再阐述,下面提取下解析Scroller功能的必要的几个View的绘制方法:

scrllTo()/scrollBy() —> invalidate()/postInvalidate() —> computeScroll();(这个流程我们可以分析源码得到)。scrllTo()/scrollBy()是view移动的两个方法;它会更新View的新的坐标点,然后调用invalidate/postInvalidate方法刷新view; 滑动完成后再调用computeScroll()方法;computeScroll()是View.java的一个空的方法,需要由我们去实现处理。

也就是说Scroller并不是一个滑动操作者,它只是一个滑动辅助计算类,所以我们现在再来看刚才ScrollView的fling方法

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");
        }
        //刷新调用computeScroll
        postInvalidateOnAnimation();
    }
}

computeScroll方法才是让你看到滑动变化的地方,你会看到在这里执行了overScrollBy方法,参数是从mScroller里获得的。

@Override
public void computeScroll() {
    //计算滑动偏移
    if (mScroller.computeScrollOffset()) {
        // This is called at drawing time by ViewGroup.  We don't want to
        // re-show the scrollbars at this point, which scrollTo will do,
        // so we replicate most of scrollTo here.
        //
        //         It's a little odd to call onScrollChanged from inside the drawing.
        //
        //         It is, except when you remember that computeScroll() is used to
        //         animate scrolling. So unless we want to defer the onScrollChanged()
        //         until the end of the animated scrolling, we don't really have a
        //         choice here.
        //
        //         I agree.  The alternative, which I think would be worse, is to post
        //         something and tell the subclasses later.  This is bad because there
        //         will be a window where mScrollX/Y is different from what the app
        //         thinks it is.
        //
        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;
        }
    }
}

总结

现在我们大致了解了ScrollView的滑动实现,继承FrameLayout实现基本的测量和布局功能,然后在onInterceptTouchEvent里面不拦截子view的事件,但是在onTouchEvent里面每次都返回true,也就是子view不处理,那么后续的事件ScrollView都要处理了,onTouchEvent的down事件记录位置信息,在move事件中做具体的滑动,滑动分为两种:平滑拖动和快速滑动(fling),平滑拖动可以执行scrollTo,fling需要借助mScroller计算位置,然后在computeScroll方法中执行滑动到计算后的位置。

分析有误的地方还请指正。

相关标签: ScrollView