看一看ScrollView的源码

欣赏
你对明天有没有美好的期待呢,它是否会如期而来呢.....
线索
  1. 主要的成员标记
  2. 核心功能与方法
1. 成员
mScroller: 用来计算内部内容滚动的辅助类OverScroller,但是主要的滚动并没有使用到他哦

mEdgeGlowTop: 当滚动到最顶部的时候的上边界阴影效果

mEdgeGlowBottom: 当滚动到最底部时候的下边界阴影效果

mChildToScrollTo: 记录拥有焦点的view, 在layout的时候会将位置滚动到他的地方

mIsBeingDragged: true表示scrollview在滚动;注意在嵌套滚动的时候,不属于scrollView的滚动,他为false;

mFillViewport: true, ScrollView模式不是MeasureSpec.UNSPECIFIED, 会用scrollView的高度来当自己的高度,重新在给子view测量一遍;一般他都不会是这个模式, 什么情况下ScrollView是UNSPECIFIED模式呢,从scollView的测量可以看出,他给子view测量的规格就是UNSPECIFIED,因此scrollView嵌套scrollView的时候, 第二个scrollView如果设置了mFillViewport,依然是不会重新测量他的子view的哦。

mActivePointerId: 当前激活的手指; 这个是scrollView处理多手指滑动的原则。他的基本规则是这样的,当第二个手指落下滑动的时候,那么前面第一个手指滑动就失效了,因为将激活的手指给到了第二个手指,当第二个手指抬起的时候,又会把激活的点给到第一个手指。

其他的就不一一列出来了......
2. 功能与方法

普通的LinearLayout, RelativeLayout, FrameLayout他们的页面内容都是有限的,限于一屏之内,而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);
  //读取是否设置了viewport属性,让子view填充scrollView高度
    setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));

    a.recycle();
}


private void initScrollView() {
    mScroller = new OverScroller(getContext());
    setFocusable(true);
    //优先让子view获得焦点
    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
    setWillNotDraw(false);
    final ViewConfiguration configuration = ViewConfiguration.get(mContext);
    //滚动识别距离,
    mTouchSlop = configuration.getScaledTouchSlop();
    //fling滑行的最小速度
    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
     //fling滑行的最大速度
    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    mOverscrollDistance = configuration.getScaledOverscrollDistance();
    mOverflingDistance = configuration.getScaledOverflingDistance();
}
  • 总结一下,其实构造和初始化比较简单,就是读取scrollView的一个_fillViewport属性,如果他为true, 他的子child一般会经过两侧测量,最终的高度和scollView是一样的; 然后就是读取系统的默认配置,比如移动多少算是滚动,多大速度可以在松手时候认为是要去滑翔。

  • ScrollView只能有一个child,否则会抛异常, 看这里:

     public void addView(View child) {
         if (getChildCount() > 0) {
             throw new IllegalStateException("ScrollView can host only one direct child");
         }
    
         super.addView(child);
     }
    
  • setFillViewport,动态设定ScrollView的填充属性

    public void setFillViewport(boolean fillViewport) {
        if (fillViewport != mFillViewport) {
            mFillViewport = fillViewport;
            //因为要改变子child的高度,自然要重新测量,布局一次啦。
            requestLayout();
        }
    }
    
  • ScrollView的测量入口, onMeasure:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //主要的测量还是调用父类FrameLayout的测量,
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //mFillViewport为true, 才需要重新测量一次
        if (!mFillViewport) {
            return;
        }
    
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //scrollView本身的模式一般不会是这个,除非是scrollView的父容器也是scrollView
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }
    
        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            int height = getMeasuredHeight();
            //如果child的高度小于scrollView的高度啦,
            if (child.getMeasuredHeight() < height) {
                final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                                                                mPaddingLeft + mPaddingRight, lp.width);
                height -= mPaddingTop;
                height -= mPaddingBottom;
                int childHeightMeasureSpec =
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
              //重新测量子child
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
    
    
    • 总结一下, 可以看出ScrollView的本身测量主要是调用父类FrameLayout的测量方法,简单说说FrameLayout的测量,他其实是先遍历所有的子view, 找到一个高度最大的view作为预备的高度,然后通过FrameLayout本身的测量规格(包含规格和父容器的剩余高度),来决定他的高度。比如FrameLayout的onMeasure传递过来的规格是EXACTLY,那么最终高度就是measureSpec解析出来的高度,比如传递过来的是AT_MOST,并且我们的预备高度小于measureSpec解析的高度,那么最终高度就是我们前面计算的预备高度啦, 也就是我们的scrollView的最终高度!
  • ScrollView的测量之二,子Child的测量measureChildWithMargins:

    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
                                           int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      
        //获取child的宽度规格,
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                                                              mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                                                              + widthUsed, lp.width);
        //构建高度的测量规格,可以看到给到size只是一个上下margin, mode是无限制的。根据标准的测量原则
        //可以知道,子child想要多高就给多高啦。
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
            lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
      //测量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    • 总结一下,在测量scrollview的时候,其实是要在FrameLayout中先测量他的子view的宽高的,然后根据子view的高度来设定Scrollview的高度。scrollview重写的measureChildWithMargins方法,他在测量子child时候给child的规格是MeasureSpec.UNSPECIFIED, 这个是很少见的,但原来也是很有用的,他可以让子child得到他想要的任意高度,ScrollView不限制他的子view高度, 在滚动容器中正好是需要的啦!
  • ScrollView的布局

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //依然还是调用frameLayout去布局他的child啦;
        super.onLayout(changed, l, t, r, b);
        //
        mIsLayoutDirty = false;
        // 如果记录了焦点的child,那么就滚到到该child的位置,注意可能不是ScrollView的直接子child哦
        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));
    
            // 范围纠正
            if (mScrollY > scrollRange) {
                mScrollY = scrollRange;
            } else if (mScrollY < 0) {
                mScrollY = 0;
            }
        }
    
        // 重新布局的时候不会丢失原来的滚动位置
        scrollTo(mScrollX, mScrollY);
    }
    
    
    • 总结一下,可以看出,布局的主要手段还是借助了FrameLayout,这个其实也很简单,就一个child. ScrollView不本身做了焦点child的位置滚动,以及还原以前的scroll位置.
  • ScrollView的绘制,draw方法

    public void draw(Canvas canvas) {
        //调用fm.draw绘制自己的内容
        super.draw(canvas);
        if (mEdgeGlowTop != null) {
            final int scrollY = mScrollY;
            if (!mEdgeGlowTop.isFinished()) {//拖到了边界处没有松手
                final int restoreCount = canvas.save();
                final int width = getWidth() - mPaddingLeft - mPaddingRight;
              
                canvas.translate(mPaddingLeft, Math.min(0, scrollY));
                mEdgeGlowTop.setSize(width, getHeight());
                //绘制他的上边界
                if (mEdgeGlowTop.draw(canvas)) {
                    postInvalidateOnAnimation();
                }
                canvas.restoreToCount(restoreCount);
            }
            if (!mEdgeGlowBottom.isFinished()) {//拖到了边界处没有松手
                final int restoreCount = canvas.save();
                final int width = getWidth() - mPaddingLeft - mPaddingRight;
                final int height = getHeight();
    
                canvas.translate(-width + mPaddingLeft,
                                 Math.max(getScrollRange(), scrollY) + height);
                canvas.rotate(180, width, 0);
                mEdgeGlowBottom.setSize(width, height);
                //绘制他的下边界
                if (mEdgeGlowBottom.draw(canvas)) {
                    postInvalidateOnAnimation();
                }
                canvas.restoreToCount(restoreCount);
            }
        }
    }
    
    
    • 总结一下,除了借助Fm去绘制child和本身外,这里主要的内容就是当滚动了边界处的时候,绘制上边界的阴影和下边界的阴影效果。
  • ScollView滚动之拦截,onInterceptTouchEvent, 如果拦截了触摸,那么子view就不能顺利地使用触摸事件啦,比如ScrollView下面的Recyclerview等,看看代码吧:

    public boolean onInterceptTouchEvent(MotionEvent ev) {
           
            final int action = ev.getAction();
          //如果之前拦截了,档次又是move,那么就拦下来,不去后面的计算啦
            if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
                return true;
            }
    
              //没得去滑了,而且还要没滑动过不拦截,奇怪,为什么要getScrollY==?
            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;
                    }
    
                    final int y = (int) ev.getY(pointerIndex);
                    final int yDiff = Math.abs(y - mLastMotionY);
                   
                    //如果move达到了滚动,并且不是嵌套滚动,立即拦截。
                    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();
                    if (!inChild((int) ev.getX(), (int) y)) {
                        //不拦截
                        mIsBeingDragged = false;
                        recycleVelocityTracker();
                        break;
                    }
    
                  ........
                    
                    //如果在fling状态,立即拦截。
                    mIsBeingDragged = !mScroller.isFinished();
                    if (mIsBeingDragged && mScrollStrictSpan == null) {
                        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                    }
                    startNestedScroll(SCROLL_AXIS_VERTICAL);
                    break;
                }
    
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    //取消拦截
                    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;
            }
    
          //拦截与否,主要还是i看这个标记呢,
            return mIsBeingDragged;
        }
    
    • 总结一下,ScrollView有几个地方会去拦截:当在down事件时候,如果当前的ScrollView在滑行状态,会拦截下来不给子view使用,这个时候down事件都并不会下发下去。当move的时候,如果不是嵌套滚动,一般ScrollView也将它拦截下来自己使用, 还有就是之前拦截了,这次又是move事件也会立刻拦截下来,整体上ScrollView就是这么处理拦截的啦,不过拦截了也不是说子view就不能用,毕竟子child可以用requestDisallowInterceptTouchEvent来禁止他的拦截生效。
  • ScollView滚动之触摸,这个算是滚动的最重要的地方之一啦, onTouchEvent, 根据手指的事件一个个地看吧:

    case MotionEvent.ACTION_DOWN: {
        //没有child,不消耗啦,没搞头啊
        if (getChildCount() == 0) {
            return false;
        }
        //如果当前在滚动状态,说明之前自己消耗的,这次也要自己来,请求父容器不要消耗.
        if ((mIsBeingDragged = !mScroller.isFinished())) {
            final ViewParent parent = getParent();
            if (parent != null) {
                parent.requestDisallowInterceptTouchEvent(true);
            }
        }
    
      //这里有意思,当上一次的滚动还在fling状态的时候,手指一放下,那么就会停止滑动啦,不信试试
        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);
        //触发嵌套滑动,ScrollView的嵌套滑动太鸡肋,这里觉得没有分析的必要
        startNestedScroll(SCROLL_AXIS_VERTICAL);
        break;
    }
    
    ........
        
    return true;
    
    
    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);
      //向上滑动,大于0
        int deltaY = mLastMotionY - y;
      //嵌套滚动相关的,在ScrollView中的实现是很鸡肋的,没什么好说的
        if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
            deltaY -= mScrollConsumed[1];
            vtev.offsetLocation(0, mScrollOffset[1]);
            mNestedYOffset += mScrollOffset[1];
        }
    //如果当前没有被认定为scrollview的滚动,会根据情况让scollView去滚动,这也是scrollView实现
    //内容滚动的核心地方。
        if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
            final ViewParent parent = getParent();
            //这里的意思是说,当move传递到了scrollView了,并且达到了滚动距离,就认为该让scrollview去
            //消耗事件了,请求不要拦截后续的事件就交给scrollview处理了。但是这种实现有点诡异,因为如果ScollView
            //的父容器拦截了move,根本就不会走到这里来了,这里的请求不要拦截也不会生效,只有当父容器在move中拦截的条件还没有生效,在这里设定才会起到作用。
            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;
            //子child.height - scrollview的height,得出可以滚动的范围
            final int range = getScrollRange();
            final int overscrollMode = getOverScrollMode();
            //如果是OVER_SCROLL_IF_CONTENT_SCROLLS,那么必须child的内容高度大于scollview的高度才可
            //认为绘制过度滚动视觉阴影
            boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
    
           //overScollBy进行了主要的内容哦你那个滚动啦;
            if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                && !hasNestedScrollingParent()) {
                // Break our velocity if we hit a scroll barrier.
                mVelocityTracker.clear();
            }
          //计算scrolledDeltaY滚动过的距离
            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) {//如果累计的滚动已经小于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;
    
    
    • 总结一下,在onTouchEvent中的move中,处理scrollView的内容滚动的关键地方,同时还根据滑动的位置,去计算将要绘制的上下阴影的图形效果呢。这里要有一个地方注意的是,在scrollView的move中会根据实际滑动的距离来请求他的父容器不要拦截,但是这里我认为这样的设计有太多的风险了,因为他要求父容器的拦截计算要很精确,如果在scrollview的请求不要拦截之前已经预先拦截了,那么后续的move根本不会走到这个请求的逻辑,就凉凉了。所以嵌套scrollview写拦截逻辑,是要小心一点的.......
    • 对了, ScrollView中关键滚动内容的地方在overScrollBy这里,还没有说明呢:
    ----view.java
    
    
    protected boolean overScrollBy(int deltaX, int deltaY,
                                   int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY,
                                   boolean isTouchEvent) {
        //获取scrollView的模式:三种模式啦,if_content, none, alaways;
        final int overScrollMode = mOverScrollMode;
        
        .......
            
            //computeVerticalScrollRange, 高度呢,说白了child的实际高度;
            //computeVerticalScrollExtent---就是scrollView的高度
        final boolean canScrollVertical =
            computeVerticalScrollRange() > computeVerticalScrollExtent();
       
        final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
            (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
       .......
           
       //新的滚动距离,等于老的+这次move移动的距离
        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            maxOverScrollY = 0;
        }
    
        // maxOverScrollY一般都是0,这里没看头,掠过
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -maxOverScrollY;
        final int bottom = maxOverScrollY + scrollRangeY;
    
       .........
    
        //纠正,让我们的新的滚动距离不要超过可滚动的界限
        boolean clampedY = false;
        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }
    
        //这里计算新的滚动距离后就调用scrollView的滚动方法,他重写了这个方法,
        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
      //表示滚动到了边界啦......
        return clampedX || clampedY;
    }
    
    • 总结一下, 这个方法之view中的方法,主要思路是根据scollView当次move的距离,进行一次距离纠正让他不会划出我们子child提供的最大范围, 没什么东西了.
  • 接着再看上面的onOverScrolled方法:

protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
    //.
    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);
    }

    //唤醒scrollbar
    awakenScrollBars();
}

  • 从上面可以看到,如果当次没滚动完,是不会滚动的,要等到下次再滚动哦.如果可以滚动就调用View.scrollTo去滑动内容。
  • 好了,继续把onTouchEvent中的事件阅读完:
 case MotionEvent.ACTION_UP://主手指抬起
    if (mIsBeingDragged) {
        //计算速度
        final VelocityTracker velocityTracker = mVelocityTracker;
        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
      //判断速度能不能达到滑行的标准
        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
            //滑行
            flingWithNestedDispatch(-initialVelocity);
        } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                                        getScrollRange())) {
            postInvalidateOnAnimation();
        }
      //清除手指活动目标
        mActivePointerId = INVALID_POINTER;
        endDrag();
    }
    break;
    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;

  • 总结一下,当主手指抬起来的时候,判断一下要不要去滑行一下。除此之外,这里有多手指操作的判断,他的逻辑是,当第二个手指放下的时候,会将前面记录主手指的第一个手指换成第二个,然后后面的滑动第一个手指就没有效果,滑动第二个才有效果,因为计算只是跟踪主手指。现在第一个手指是副手指,第二个手指是主手指了。还有抬起的时候,如果抬起的是第二个手指即主手指抬起, 那么还要将前面的第一个手指又还原成主手指,后面滑动又跟踪他了,如果抬起的是原来的第一个手指即非主手指,那么就不做什么了....这种多指操作,也是android系统的标准行为。
  • 看看onSecondaryPointerUp的实现吧....
private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
        MotionEvent.ACTION_POINTER_INDEX_SHIFT;
    final int pointerId = ev.getPointerId(pointerIndex);
    //如果当前抬起的是主手指,就要进行主手指的重新定位,如果不是那么就啥都不做.....
    if (pointerId == mActivePointerId) {
        // This was our active pointer going up. Choose a new
        // active pointer and adjust accordingly.
        // TODO: Make this decision more intelligent.
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
        mLastMotionY = (int) ev.getY(newPointerIndex);
        mActivePointerId = ev.getPointerId(newPointerIndex);
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
    }
}
3. 结束
  • 好了, 原来ScollView的滚动还是利用View标准的scrollTo去滚动的呀,对ScrollView的理解就到这里了吧, 说实话感觉代码有点恶心,代码不算太多,但是功能不是特别明确,对应用开发可造成的隐性的问题不少。

你可能感兴趣的:(看一看ScrollView的源码)