Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android滑動控件之ScrollView

android滑動控件之ScrollView

編輯:關於Android編程

前言

ScrollView可以說是android裡最簡單的滑動控件,但是其中也蘊含了很多的知識點。今天嘗試通過ScrollView的源碼來了解ScrollView內部的細節。本文在介紹ScrollView時會忽略以下內容:嵌套滑動,崩潰保存,Accessibility。
ScrollView是一種控件,繼承自 FrameLayout,他的子控件遠遠大於ScrollView本身,所以ScrollView展現出來的只有子控件的一部分,通過滑動的形式來呈現出子控件的內容。

基本用法與功能剖析

先來回顧下ScrollView的基本用法,超級簡單。我們通常在ScrollView內部放一個LinearLayout,然後在LinearLayout放各種元素,ScrollView滾動時就可以看到這些元素。附帶一句,LinearLayout的width通常是match_parent(也可以是warp_content,這裡有個坑,我們暫且不管,後面會提)。

    

        

        
    

從測試的角度來看下,ScrollView的功能是怎麼樣的?

首先滑動的時候有2種情況,如果滑的慢,ScrollView的滑動會隨著手指的離開而停止(簡單滑動);如果滑的快,在手指離開後,ScrollView還會再滑一段時間(這段時間內的狀態我們稱為fling)。

第二,fling的時候,手指碰一下,就立刻停止fling
第三,ScrollView到頂部的時候,下拉有光影效果。底部同理

子窗口大小超出父窗口

我們知道,一般情況下子view都是沒有父view大的,因為measure的時候子view的大小會受到父view的制約,那什麼情況下,子view會超出父view大小呢?

要想子view超出父view大小,大概有2種方式,一種是父view對子view的要求為MeasureSpec.EXACTLY,子view的size設置為某個固定值,另一種是父view對子view的要求為UNSPECIFIED,然後子view就可以隨便搞了。可以參考getChildMeasureSpec代碼就能大概看出來。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);


    int size = Math.max(0, specSize - padding);


    int resultSize = 0;
    int resultMode = 0;


    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
      if (childDimension >= 0) {
         //此時為case1,resultSize可能大於specSize
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // Child wants to be our size. So be it.
        resultSize = size;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size. It can't be
        // bigger than us.
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;


    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
        // Child wants a specific size... so be it
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // Child wants to be our size, but our size is not fixed.
        // Constrain child to not be bigger than us.
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size. It can't be
        // bigger than us.
        resultSize = size;
        resultMode = MeasureSpec.AT_MOST;
      }
      break;


    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
    //此時為case2,parent不做限制,大小就可以亂來了
      if (childDimension >= 0) {
        // Child wants a specific size... let him have it
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
        // Child wants to be our size... find out how big it should
        // be
        resultSize = 0;
        resultMode = MeasureSpec.UNSPECIFIED;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
        // Child wants to determine its own size.... find out how
        // big it should be
        resultSize = 0;
        resultMode = MeasureSpec.UNSPECIFIED;
      }
      break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }

EXACTLY+固定值

對於case1,我們舉個例子,可以這麼寫




    

此時TextView的就比parent的大,這是一種方式讓子view超出了父view的大小。
ScrollView重寫了android.widget.ScrollView#measureChildWithMargins

UNSPECIFIED

而ScrollView的child能比ScrollView本身還大,用的是第二種方法,量的時候把specMode改為UNSPECIFIED,具體代碼如下所示,關鍵看這句
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);
直接把childHeightMeasureSpec變為了MeasureSpec.UNSPECIFIED,此時parent傳過來的高度其實已經毫無意義了。而子view的高度一般寫為wrap_conten,就可以非常大了。

   @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 childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

嵌套滑動(NestedScrolling)

本文雖然不介紹嵌套滑動,但是嵌套滑動的相關代碼頻繁出現在onTouchevent裡面,所以還是要簡單說下。

NestedScrolling 提供了一套父 View 和子 View 滑動交互機制。要完成這樣的交互,父 View 需要實現 NestedScrollingParent 接口,而子 View 需要實現 NestedScrollingChild 接口。
NestedScrollingChild

更多知識可以參考
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0822/3342.html
https://segmentfault.com/a/1190000002873657

ScrollView默認支持了嵌套滑動,既可作為父view,也可作為子view
我們在看代碼的時候暫時忽略和嵌套滑動相關的(帶nest的函數)

簡單滑動

手指在屏幕上滑動會觸發ACTION_DOWN,ACTION_MOVE, ACTION_MOVE沒人處理,就交給ScrollView處理。
這裡我們看到個變量mIsBeingDragged,這個代表的是ScrollView是否正在被拖拽,手指抬起,mIsBeingDragged就會變為false,初始化的時候也為false。看L4可知如果deltaY(滑動的距離)超過mTouchSlop,那就表示觸發了ScrollView的滑動,mIsBeingDragged 置為true,mTouchSlop是一個固定阈值。然後會執行L17 overScrollBy進行滾動。

            case MotionEvent.ACTION_MOVE:
                 ...

                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 (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                            && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

overScrollBy這是View的方法,會觸發onOverScrolled回調。此時只是普通的滑動,所以走L18,就是調super.scrollTo,根據手指滑動的距離進行移動。非常簡單。

   @Override
    protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY) {
        // Treat animating scrolls differently; see #computeScroll() for why.
        if (!mScroller.isFinished()) {
             //fling走這裡
            final int oldX = mScrollX;
            final int oldY = mScrollY;
            mScrollX = scrollX;
            mScrollY = scrollY;
            invalidateParentIfNeeded();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (clampedY) {
                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
            }
        } else {
              //普通的滑動走這裡
            super.scrollTo(scrollX, scrollY);
        }

        awakenScrollBars();
    }

fling(慣性滑動)

怎麼實現手指離開之後,還能滑動一段距離呢?
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;

只要速度超過mMinimumVelocity,那就會調用flingWithNestedDispatch(),實際上就是調用mScroller.fling()。mScroller.fling是一個OverScroller,OverScroller的相關知識可以參考 View的滾動與Scroller

fling的時候點擊一下,立刻停止

這是怎麼做到的?總的來說,是通過onInterceptTouchEvent和onTouchEvent的配合,調用 mScroller.abortAnimation();來停止滾動的。
分2種case來討論

case1 ScrollView內部的LinearLayout的width為match_parent

此時隨便點一下就點到了LinearLayout內部。
先來看fling時的狀態,此時手指已經抬起,endDrag()被調用,mIsBeingDragged為false。此時點擊一下,會到onInterceptTouchEvent()方法。此時在LinearLayout內部,所以inChild返回true,會走到mIsBeingDragged = !mScroller.isFinished();,因為在fling,所以mScroller.isFinished()必定false,所以mIsBeingDragged為true,那麼down事件就被攔截起來了。
下一步會走到onTouchEvent裡。

     case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
                if (!inChild((int) ev.getX(), (int) y)) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                /*
                * If being flinged and user touches the screen, initiate drag;
                * otherwise don't.  mScroller.isFinished should be false when
                * being flinged.
                */
                mIsBeingDragged = !mScroller.isFinished();
                if (mIsBeingDragged && mScrollStrictSpan == null) {
                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                }
                startNestedScroll(SCROLL_AXIS_VERTICAL);
                break;
            }

再來看onTouchEvent如何處理down事件,有下面這段代碼,如果在fling,那麼立刻終止,達到目的。

      /*
                 * If being flinged and user touches, stop the fling. isFinished
                 * will be false if being flinged.
                 */
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    if (mFlingStrictSpan != null) {
                        mFlingStrictSpan.finish();
                        mFlingStrictSpan = null;
                    }
                }

case2 ScrollView內部的LinearLayout的width較小,點擊到LinearLayout外部

此時inChild返回false,那麼onInterceptTouchEvent返回false,不攔截。但是注意,此時點到了LinearLayout外部,那麼這個down事件,沒有child去處理,所以還是交給ScrollView來處理,還是會走到onTouchEvent內,一樣會調用mScroller.abortAnimation();方法

R.attr.scrollViewStyle是什麼

在構造函數裡,我們可以看到這麼一段代碼,默認給ScrollView,配置了scrollViewStyle,這有什麼意義呢?其實就是設置了scrollbars和fadingEdge為vertical。看下邊代碼

  public ScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
    }

attrs.xml內有


themes.xml內有

@style/Widget.ScrollView

styles.xml內有


  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved