編輯:關於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); }
對於case1,我們舉個例子,可以這麼寫
此時TextView的就比parent的大,這是一種方式讓子view超出了父view的大小。
ScrollView重寫了android.widget.ScrollView#measureChildWithMargins
而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); }
本文雖然不介紹嵌套滑動,但是嵌套滑動的相關代碼頻繁出現在onTouchevent裡面,所以還是要簡單說下。
NestedScrolling 提供了一套父 View 和子 View 滑動交互機制。要完成這樣的交互,父 View 需要實現 NestedScrollingParent 接口,而子 View 需要實現 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(); }
怎麼實現手指離開之後,還能滑動一段距離呢?
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
這是怎麼做到的?總的來說,是通過onInterceptTouchEvent和onTouchEvent的配合,調用 mScroller.abortAnimation();來停止滾動的。
分2種case來討論
此時隨便點一下就點到了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; } }
此時inChild返回false,那麼onInterceptTouchEvent返回false,不攔截。但是注意,此時點到了LinearLayout外部,那麼這個down事件,沒有child去處理,所以還是交給ScrollView來處理,還是會走到onTouchEvent內,一樣會調用mScroller.abortAnimation();方法
在構造函數裡,我們可以看到這麼一段代碼,默認給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內有
這方面的知識不是孤立的,其中有關於,Socket編程,多線程的操作,以及I/O流的操作。當然,實現方法不止一種,這只是其中一種,給同是新手一點點思路。如果有什麼推薦的話,
前言用scrollTo()和scrollBy()方法實現了View的滑動,但是實現的效果非常的生硬,用戶體驗很差。這一篇繼續在原有基礎上,擴展下View的彈性滑動。下面詳
前面分析那麼多系統源碼了,也該暫停下來休息一下,趁昨晚閒著看見一個有意思的需求就操練一下分析源碼後的實例演練—-自定義控件。這個實例很適合新手入門自定義控件。先看下效果圖
加入代碼混淆器,主要是加入proguard-project.txt文件的規則進行混淆,之前新建Android程序是proguard.cfg文件 可以看一下我采用的通用規則