ScrollView源码分析

本文分析版本: Android API 23

1.简介

ScrollView是我们在开发中经常使用的控件。当我们需要展示的内容比较多但并不是重复的item时,我们就会使用ScrollView使内容可以在垂直方向滚动显示防止显示不全。ScrollView使用起来非常简单,大多数情况下你甚至都不用写一行Java代码就能使用ScrollView了。但是要注意的是ScrollView中只能添加一个子View。今天我们就来看看ScrollView到底是如何实现的。以及最后会教大家一行代码实现类似IOS上的弹性ScrollView_

2.源码分析

2.1 继承关系

extend_relation.png

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);即父类FrameLayoutonMeasure()方法。如果我们将mFillViewport设置为false的话将会直接return。当为true时才会继续执行,会根据子View的高度和ScrollView本身的高度决定是否重新measureView使其充满ScrollViewScrollViewonMeasure()其实就是处理了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的滑动。最后记录了mLastMotionYmActivePointerId

  • 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赋值。接着如果mIsBeingDraggedtrue。就取得处理滑动需要的各种参数,并调用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本身是否可以滑动的设定,等等来最终决定了新的newScrollXnewScrollY。接着调用了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();
        }
    }

可以看到是调用了mScrollerfling方法,在上一篇Scroller源码分析中,我们已经详细解释了Scroller的原理,ScrollView中虽然使用的是OverScroller但是使用方法也是类似的。所以在调用了mScrollerfling方法后。我们需要在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语句里,可以看到是给mScrollXmScrollY赋值后调用了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

3.一行代码实现弹性ScrollView

我们都知道ios上的弹性滑动做的相当顺滑。我们Android系统一直都没有。看完ScrollView的代码发现,其实通过变量mOverflingDistance就能决定弹性滑动的最大值。但是ScrollView并没有暴露出方法给我们设置。但是我们只需要通过反射来设定mOverflingDistance的值即可。。JOOR对反射做了封装,可以使我们非常简洁的来写反射,所以这里我们只需要一行代码即可:

Reflect.on(scrollView).set("mOverflingDistance", 100);

这样就实现了弹性ScrollView。以上demo的代码在SkyScrollViewDemo

我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: http://weibo.com/u/2030683111
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容