ScrollView源码分析

先抛个问题 ScrollView.setonClickView ?

从Touch分发看滚动原理

SDK 基于 5.0   

1 onInterceptTouchEvent  是否拦截Touch  如果返回true,则onTouchEvent会被调用,并处理滑动事件。

注意:ACTION_DOWN 时并未拦截

          ACTION_CANCLE |UP 清除 mIsBeingDragged

           ACTION_MOVE时mIsBeingDragged =true 的条件

         1有效移动距离大于 mTouchSlop 2 方向SCROLL_AXIS_VERTICAL 

sdk低版本 没有实现类似NestScrollParent嵌套滚动相关方法 


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

2  onTouchEvent(MotionEvent ev)  

在处理各种事件之前,首先初始化了VelocityTracker。并且复制一个新的MotionEvent对象用于计算加速度。

特别说明:

final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop(); //系统默认滚动最小有效距离

 if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }

    public boolean onTouchEvent(MotionEvent ev) {
        //1. 如果没有初始化速率轨迹 初始化它,这个还是用于手指离开后计算fling的
        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: {
                if (getChildCount() == 0) {
                    return false;
                }
                //2.请求父视图不要拦截
                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.
                 */
                //3. 如果当前在fling 就是mScroller还没有完成就触摸了
                //立刻放弃当前的滚动
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    if (mFlingStrictSpan != null) {
                        mFlingStrictSpan.finish();
                        mFlingStrictSpan = null;
                    }
                }

                // Remember where the motion event started
                //4. 记住触摸的位置 mLastMotionY 这个值在move的时候用来计算手指移动的变化量,然后用来计算需要滚动的距离
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                //5. 这个是处理内部滚动 可以先不用管这个
                //涉及到Nested的都可以先不用管它  这个好像是为了支持v4包内的某个功能做的处理
                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);
                //6. deltaY 计算手指移动的距离 在4中记录的 同时下面还会更新这个值 8中会用到这个值来计算需要滚动的距离
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                //7. 如果先点击没有滑动,拦截事件中为false,ScrollView中的button也能接受到事件,这是再根据滑动的距离来决定是不是需要拦截事件
                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 这个很关键 否则根本滑不懂
                    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.
                    //8. 调用overScrollBy方法计算滚动 这个方法就是计算一下滚动的距离然后回调给onOverScrolled()在这里调用scrollTo方法
                    // 到这里的时候 ScrollView还不会滚动,滚动的代码在onOverScrolled()中,紧接着下面会出现
                    // 这里返回true表示滑动超出了内容区域 像滑倒顶部会有阻尼的那种效果就可以用这个实现
                    // 这个是最关键的地方 关键的源码都有注释 厉害了word
                    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                            && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    //9.下面就没必要仔细去研究了 这里处理一下滑到边界出的效果
                    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:
                if (mIsBeingDragged) {
                    //10. 速率轨迹终于要大显神威了
                    // up后 8中的计算滚动就会停止,但是实际上ScrollView还会滚动一段距离
                    // 这里根据 VelocityTracker 得到手指离开这一瞬间的Velocity
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

                    //11. 速录很大 则会认为是一个fling 动作
                    // flingWithNestedDispatch()方法内部就是执行了mScroller.fling()方法
                    //else if 含义:速录很小,例如我们滑动最后停下来,然后手指离开屏幕,这时的速率可能为0,就不需要fling
                    //但是若滑动到顶部就需要回弹动画 ,直接动用 mScroller.springBack()即可
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        flingWithNestedDispatch(-initialVelocity);
                    } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                            getScrollRange())) {
                        postInvalidateOnAnimation();
                    }

                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;
            //12. 取消事件的处理 类似于up事件 理解上面的下面的多个触摸点的处理就很简单了
            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;
        }

ACTION_DOWN:首先是给mIsBeingDragged赋值,接着检查是否在fling动画执行过程中,如果正在执行则停止,这也是为什么我们在ScrollView滑动过程中手指触摸时会终止ScrollView的滑动。最后记录了mLastMotionY与mActivePointerId。

ACTION_MOVE: 首先计算当前的垂直偏移量deltaY。然后判断是否大于最小滑动距离,并且给mIsBeingDragged赋值。接着如果mIsBeingDragged为true。就取得处理滑动需要的各种参数,并调用overScrollBy()方法来处理触摸事件,overScrollBy()是在View里实现的方法,大致实现如下

ACTION_UP: 最终调用 

mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
        Math.max(0, bottom - height), 0, height/2);  执行滚动动画

fling执行流程 和 smoothScrollBy大概一致 最终执行 invalidate 开始绘制流程 通过computeScrollOffset 不断计算 是否滚动结束 未结束则 invalidate 不断重复至结束

View postInvalidateOnAnimation() invalidate()  见

https://blog.csdn.net/fyfcauc/article/details/43308467

   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);
    }
public boolean computeScrollOffset() {
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
    
        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
            	//已经耗时和总时间的百分比
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                
                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);
                
                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }
                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

 draw()和scroller关系 scrollY

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

velocity决定了滚动触发到结束执行时间和最终滑动距离

再看绘制流程

 

补充 ScrollView测量部分

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); //framwlayout 测量child
 
        //测量chid(0) EXACTLY

        if (!mFillViewport) {
            return;
        }
 
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }
 
        if (getChildCount() > 0) {
           ...
            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);
            }
        }
    }

重写 measureChild()measureChildWithMargins (),将测量模式改成了UNSPECIFIED,这样在测量时能够得到子View想要的高度。

    @Override
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();

        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
                + mPaddingRight, lp.width);
        final int verticalPadding = mPaddingTop + mPaddingBottom;
        childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值