看一看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的理解就到這里了吧, 說實話感覺代碼有點惡心,代碼不算太多,但是功能不是特別明確,對應用開發可造成的隱性的問題不少。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380