前言
ScrollView可以說是android里最簡單的滑動控件岖常,但是其中也蘊(yùn)含了很多的知識點(diǎn)。今天嘗試通過ScrollView的源碼來了解ScrollView內(nèi)部的細(xì)節(jié)。本文在介紹ScrollView時(shí)會忽略以下內(nèi)容:嵌套滑動,崩潰保存,Accessibility奋岁。
ScrollView是一種控件,繼承自 FrameLayout荸百,他的子控件遠(yuǎn)遠(yuǎn)大于ScrollView本身闻伶,所以ScrollView展現(xiàn)出來的只有子控件的一部分,通過滑動的形式來呈現(xiàn)出子控件的內(nèi)容够话。
基本用法與功能剖析
先來回顧下ScrollView的基本用法蓝翰,超級簡單光绕。我們通常在ScrollView內(nèi)部放一個(gè)LinearLayout,然后在LinearLayout放各種元素畜份,ScrollView滾動時(shí)就可以看到這些元素诞帐。附帶一句,LinearLayout的width通常是match_parent(也可以是warp_content爆雹,這里有個(gè)坑停蕉,我們暫且不管,后面會提)钙态。
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
從測試的角度來看下慧起,ScrollView的功能是怎么樣的?
第一册倒,滑動的時(shí)候有2種情況蚓挤,如果滑的慢,ScrollView的滑動會隨著手指的離開而停止(簡單滑動)剩失;如果滑的快屈尼,在手指離開后册着,ScrollView還會再滑一段時(shí)間(這段時(shí)間內(nèi)的狀態(tài)我們稱為fling)拴孤。
第二,fling的時(shí)候甲捏,手指碰一下演熟,就立刻停止fling
第三,ScrollView到頂部的時(shí)候司顿,下拉有光影效果芒粹。底部同理
子窗口大小超出父窗口
我們知道,一般情況下子view都是沒有父view大的大溜,因?yàn)閙easure的時(shí)候子view的大小會受到父view的制約化漆,那什么情況下,子view會超出父view大小呢钦奋?
要想子view超出父view大小座云,大概有2種方式,一種是父view對子view的要求為MeasureSpec.EXACTLY,子view的size設(shè)置為某個(gè)固定值付材,另一種是父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) {
//此時(shí)為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:
//此時(shí)為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睬隶,我們舉個(gè)例子锣夹,可以這么寫
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.fish.a.MainActivity">
<TextView
android:id="@+id/aa"
android:layout_width="4000dp"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
此時(shí)TextView的就比parent的大,這是一種方式讓子view超出了父view的大小理疙。
ScrollView重寫了android.widget.ScrollView#measureChildWithMargins
UNSPECIFIED
而ScrollView的child能比ScrollView本身還大晕城,用的是第二種方法,量的時(shí)候把specMode改為UNSPECIFIED,具體代碼如下所示窖贤,關(guān)鍵看這句
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);
直接把childHeightMeasureSpec變?yōu)榱薓easureSpec.UNSPECIFIED砖顷,此時(shí)parent傳過來的高度其實(shí)已經(jīng)毫無意義了。而子view的高度一般寫為wrap_content赃梧,就可以非常大了滤蝠。
@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)
本文雖然不介紹嵌套滑動,但是嵌套滑動的相關(guān)代碼頻繁出現(xiàn)在onTouchevent里面授嘀,所以還是要簡單說下物咳。
NestedScrolling 提供了一套父 View 和子 View 滑動交互機(jī)制。要完成這樣的交互蹄皱,父 View 需要實(shí)現(xiàn) NestedScrollingParent 接口览闰,而子 View 需要實(shí)現(xiàn) NestedScrollingChild 接口。
更多知識可以參考
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0822/3342.html
https://segmentfault.com/a/1190000002873657
ScrollView默認(rèn)支持了嵌套滑動巷折,既可作為父view压鉴,也可作為子view
我們在看代碼的時(shí)候暫時(shí)忽略和嵌套滑動相關(guān)的(帶nest的函數(shù)),后面我會寫篇文章專門介紹嵌套滑動
滑動觸發(fā)
首先看下锻拘,怎么觸發(fā)ScrollView的滑動呢油吭?有2條路徑。
滑動觸發(fā)前-down事件
我們先從down事件開始看署拟,對照android事件分發(fā)里的down的流程圖來看婉宰,ScrollView會少幾個(gè)分支。
down事件分發(fā)到ScrollView之后推穷,會走ScrollView的dispatchTouchEvent()心包,然后進(jìn)入onInterceptTouchEvent(),onInterceptTouchEvent里面關(guān)于down的代碼馒铃,我們看一下蟹腾,此時(shí)必定返回false.分析下,如果L4的inChild為false,那么就直接break骗露,返回mIsBeingDragged岭佳,此時(shí)必定false;如果inChild為true萧锉,那就會到L24珊随,mIsBeingDragged必定是false,所以還是返回false。所以無論inChild是true還是false叶洞,此時(shí)onInterceptTouchEvent必定返回false鲫凶,因此onInterceptTouchEvent返回true的分支就被剪掉了。
...
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;
}
...
return mIsBeingDragged;
圖中還有一個(gè)很明顯的分支被減掉了衩辟,那就是p:super.dispatchTouchEvent()返回false的分支螟炫,為什么這里不可能返回false呢?我們知道ScrollView的super.dispatchTouchEvent()會調(diào)用onTouchEvent艺晴,我們在看看onTouchEvent的代碼昼钻,down事件下一般都返回true。(只有g(shù)etChildCount為0封寞,返回false)
所以ScrollView處理down事件之后然评,必定返回true,mFirstTouchTarget可能空狈究,也可能非空碗淌。說的直白一點(diǎn),那就是down事件傳遞到ScrollView之后抖锥,如果他的子view消費(fèi)了亿眠,那ok,如果子view不消費(fèi)磅废,那ScrollView自己消費(fèi)纳像。
滑動觸發(fā)中-MOVE事件
前面說了down事件后的結(jié)果,這是滑動觸發(fā)的一個(gè)前置條件还蹲,真正觸發(fā)滑動肯定是MOVE引起的爹耗,那么MOVE如何引起滑動呢耙考?down事件的結(jié)果是谜喊,要么ScrollView的子類消費(fèi)掉,要么ScrollView消費(fèi)掉倦始。我們對照著2種情況分別分析
ScrollView親自消費(fèi)down事件
此時(shí)ScrollView親自消費(fèi)了down事件斗遏,那么ScrollView的mFirstTouchTarget為null,(對照android事件分發(fā)的move流程圖分析) 此時(shí)move事件進(jìn)入ScrollView直接被攔截鞋邑,傳遞給ScrollView的onTouchEvent诵次。來看onTouchEvent的move
這里我們看到個(gè)變量mIsBeingDragged,這個(gè)代表的是ScrollView是否正在被拖拽枚碗,手指抬起逾一,mIsBeingDragged就會變?yōu)閒alse,初始化的時(shí)候也為false肮雨∽穸拢看L4可知如果deltaY(滑動的距離)超過mTouchSlop,那就表示觸發(fā)了ScrollView的滑動,mIsBeingDragged 置為true陌宿,mTouchSlop是一個(gè)固定閾值锡足。然后會執(zhí)行L17 overScrollBy進(jìn)行滾動。
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的方法爽蝴,會觸發(fā)onOverScrolled回調(diào)沐批。此時(shí)只是普通的滑動,所以走L18蝎亚,就是調(diào)super.scrollTo珠插,根據(jù)手指滑動的距離進(jìn)行移動。非常簡單颖对。
@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();
}
ScrollView子類消費(fèi)down事件
此時(shí)ScrollView的子view消費(fèi)了down事件捻撑,那么ScrollView的mFirstTouchTarget非空,(對照android事件分發(fā)的move流程圖分析) 此時(shí)move事件進(jìn)入ScrollView會執(zhí)行onInterceptTouchEvent缤底,如果返回false就交給子view處理顾患。如果返回true就向子view發(fā)一個(gè)cancel消息,并且把mFirstTouchTarget設(shè)置為null个唧,這樣下次move事件來就會直接攔截并進(jìn)入onTouchEvent江解。那什么情況下,onInterceptTouchEvent會返回true呢徙歼?下面是onInterceptTouchEvent的move部分的代碼犁河,其實(shí)跟前面類似的,yDiff > mTouchSlop 觸發(fā)滑動
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
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);
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;
}
return mIsBeingDragged;
滑動觸發(fā)小結(jié)
滑動觸發(fā)的地方可能是在onTouchEvent也可能在onInterceptTouchEvent內(nèi)魄梯。
觸發(fā)的原因就是手指移動的距離超過了mTouchSlop
可能是一次move超過了mTouchSlop桨螺,也可能是多次move加起來超過了mTouchSlop。
多次move是怎么樣的呢酿秸?注意灭翔,這里說的多次move是在一個(gè)cycle內(nèi)的,舉個(gè)例子比如mTouchSlop21辣苏,第一次move了10肝箱,第二次move了15,第三次move了5稀蟋,會怎么樣呢煌张?
第一次move了10,此時(shí)未達(dá)到mTouchSlop退客,所以不會觸發(fā)滑動
第二次move了15骏融,此時(shí)10+15>21,所以會觸發(fā)滑動,滾多少呢绎谦?滾的距離為10+15-21=4管闷,為啥,看下邊這段代碼,第一次觸發(fā)滾動窃肠,滾的距離要減掉一個(gè)mTouchSlop包个。
然后第三次滾動距離5,那ScrollView滾動5冤留,后面的move都跟第三次一致
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
fling(慣性滑動)
怎么實(shí)現(xiàn)手指離開之后碧囊,還能滑動一段距離呢?
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纤怒,那就會調(diào)用flingWithNestedDispatch()糯而,實(shí)際上就是調(diào)用mScroller.fling()。mScroller.fling是一個(gè)OverScroller泊窘,OverScroller的相關(guān)知識可以參考 View的滾動與Scroller
fling的時(shí)候點(diǎn)擊一下熄驼,立刻停止
這是怎么做到的?總的來說烘豹,是通過onInterceptTouchEvent和onTouchEvent的配合瓜贾,調(diào)用 mScroller.abortAnimation();來停止?jié)L動的。
分2種case來討論
case1 ScrollView內(nèi)部的LinearLayout的width為match_parent
此時(shí)隨便點(diǎn)一下就點(diǎn)到了LinearLayout內(nèi)部携悯。
先來看fling時(shí)的狀態(tài)祭芦,此時(shí)手指已經(jīng)抬起,endDrag()被調(diào)用憔鬼,mIsBeingDragged為false龟劲。此時(shí)點(diǎn)擊一下,會到onInterceptTouchEvent()方法轴或。此時(shí)在LinearLayout內(nèi)部昌跌,所以inChild返回true,會走到mIsBeingDragged = !mScroller.isFinished();侮叮,因?yàn)樵趂ling避矢,所以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,那么立刻終止烫扼,達(dá)到目的曙求。
/*
* 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內(nèi)部的LinearLayout的width較小,點(diǎn)擊到LinearLayout外部
此時(shí)inChild返回false,那么onInterceptTouchEvent返回false悟狱,不攔截静浴。但是注意,此時(shí)點(diǎn)到了LinearLayout外部挤渐,那么這個(gè)down事件苹享,沒有child去處理,所以還是交給ScrollView來處理浴麻,還是會走到onTouchEvent內(nèi)得问,一樣會調(diào)用mScroller.abortAnimation();方法
R.attr.scrollViewStyle是什么
在構(gòu)造函數(shù)里,我們可以看到這么一段代碼软免,默認(rèn)給ScrollView宫纬,配置了scrollViewStyle,這有什么意義呢膏萧?其實(shí)就是設(shè)置了scrollbars和fadingEdge為vertical漓骚。看下邊代碼
public ScrollView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
}
attrs.xml內(nèi)有
<attr name="scrollViewStyle" format="reference" />
themes.xml內(nèi)有
<item name="scrollViewStyle">@style/Widget.ScrollView</item>
styles.xml內(nèi)有
<style name="Widget.ScrollView">
<item name="scrollbars">vertical</item>
<item name="fadingEdge">vertical</item>
</style>
其他
- 因?yàn)橛昧薕verScroller,所以mScrollY可能是負(fù)值
- Scrollview到頂部的時(shí)候下拉的暈影效果榛泛,主要是用EdgeEffect實(shí)現(xiàn)
- 我們會在下篇文章從0開始寫一個(gè)ScrollView