ScrollView源码分析

前言

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>

前者是没有设置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方法中执行滑动到计算后的位置。

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

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值