Android 開(kāi)發(fā)中經(jīng)常涉及 View 的滾動(dòng),例如類(lèi)似于 ScrollView 的滾動(dòng)手勢(shì)和滾動(dòng)動(dòng)畫(huà),例如用 ListView 模仿 iOS 上的左滑刪除 item,例如 ListView 的下拉刷新。這些都是常見(jiàn)的需求,同時(shí)也都涉及 View 滾動(dòng)的相關(guān)知識(shí)。
本文將解析 Android 中 View 的滾動(dòng)原理,并介紹與滾動(dòng)相關(guān)的兩個(gè)輔助類(lèi) Scroller 和 VelocityTracker,并通過(guò) 3 個(gè)逐漸深入的例子來(lái)加深理解。
注:
- 本文沒(méi)有嘗試實(shí)現(xiàn)上述幾種功能,只闡述基本原理和基礎(chǔ)類(lèi)的使用方法。
- 文中的例子只是截取了與 View 相關(guān)的代碼,完整的示例代碼請(qǐng)見(jiàn)DEMO
- 本文的源碼分析基于 Android API Level 21,并省略掉部分與本文關(guān)系不大的代碼。
View 的滾動(dòng)原理
在了解 View 的滾動(dòng)原理之前,我們先來(lái)想象一個(gè)場(chǎng)景:我們坐在一個(gè)房間里,透過(guò)一扇窗戶看窗外的風(fēng)景。窗戶是有大小限制的,而風(fēng)景是沒(méi)有大小限制的。
把上述的場(chǎng)景對(duì)應(yīng)到 Android 的 View 顯示原理上來(lái):當(dāng)一個(gè) View 顯示在界面上,它的上下左右邊緣就圍成了這個(gè) View 的可視區(qū)域,我們可以稱(chēng)這個(gè)區(qū)域?yàn)椤翱梢暣翱凇?,我們平時(shí)看到的 View 的內(nèi)容,都是透過(guò)這個(gè)可視窗口中看到的“風(fēng)景”。View 的大小內(nèi)容可以無(wú)窮大,不受可視窗口大小的限制。
另外,如果在窗外的風(fēng)景中,有一個(gè)人出現(xiàn)在窗戶右邊很遠(yuǎn)的地方,那么我們?cè)诜块g里就看不到那個(gè)人;如果那個(gè)人站在窗戶正對(duì)著出去的地方,那么我們就可以透過(guò)窗戶看到他。對(duì)應(yīng)到 View 上面來(lái),只有出現(xiàn)在“可視窗口”中的那部分內(nèi)容可以被看到。
View 的 scroll 相關(guān)
在 View 類(lèi)中,有兩個(gè)變量 mScrollX 和 mScrollY,它們記錄的是 View 的內(nèi)容的偏移值。mScrollX 和 mScrollY 的默認(rèn)值都是 0,即默認(rèn)不偏移。另外我們需要知道一點(diǎn),向左滑動(dòng),mScrollX 為正數(shù),反正為負(fù)數(shù)。假設(shè)我們令 mScrollX = 10,那么該 View 的內(nèi)容會(huì)相對(duì)于原來(lái)向左偏移 10px。 看看系統(tǒng)的 View 類(lèi)中的源碼:
// View.java
public class View {
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
protected int mScrollY;
// ...
}
通常我們比較少直接設(shè)置 mScrollX 和 mScrollY,而是通過(guò) View 提供的兩個(gè)方法來(lái)設(shè)置。
// 瞬時(shí)滾動(dòng)到某個(gè)位置
public void scrollTo(int x, int y)
// 瞬時(shí)滾動(dòng)某個(gè)距離
public void scrollBy(int x, int y)
看看兩個(gè)方法的源碼:
// View.java
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
首先看 scrollTo(int x, int y) 方法,它除了設(shè)置 mScrollX 和 mScrollY 兩個(gè)變量,還會(huì)觸發(fā)自己重新繪制,另外還會(huì)通過(guò) onScrollChanged 觸發(fā)回調(diào)。而 scrollBy 方法其實(shí)也是調(diào)用 scrollTo 方法。
明顯,兩個(gè)方法的區(qū)別在于 scrollTo 方法是滾動(dòng)到特定位置,參數(shù) x、y 代表“絕對(duì)位置”,而 scrollBy 方法是在當(dāng)前位置基礎(chǔ)上滾動(dòng)特定距離,參數(shù) x、y 代表“相對(duì)位置”。
另外,View 還提供了 mScrollX 和 mScrollY 的 getter:
// 獲取 mScrollX
public final int getScrollX()
// 獲取 mScrollY
public final int getScrollY()
看看源碼中這兩個(gè)方法的注釋?zhuān)梢愿玫乩斫?scroll 的概念。
// View.java
/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
return mScrollY;
}
例子1
為了更好地理解 mScrollX 和 mScrollY,也為后續(xù)介紹的知識(shí)做準(zhǔn)備,我們先看一個(gè)例子:
/**
* 示例:自定義 ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,
* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 調(diào)用 moveToIndex 方法會(huì)調(diào)用 scrollTo 方法,從而瞬時(shí)滾動(dòng)到某一位置
*/
public class Case1ViewGroup extends ViewGroup {
public static final int CHILD_NUMBER = 6;
private int mCurrentIndex = 0;
public Case1ViewGroup(Context context) {
super(context);
init();
}
public Case1ViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Case1ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 添加幾個(gè)子 View
for (int i = 0; i < CHILD_NUMBER; i++) {
TextView child = new TextView(getContext());
int color;
switch (i % 3) {
case 0:
color = 0xffcc6666;
break;
case 1:
color = 0xffcccc66;
break;
case 2:
default:
color = 0xff6666cc;
break;
}
child.setBackgroundColor(color);
child.setGravity(Gravity.CENTER);
child.setTextSize(TypedValue.COMPLEX_UNIT_SP, 46);
child.setTextColor(0x80ffffff);
child.setText(String.valueOf(i));
addView(child);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// 每個(gè)子 View 都與自己一樣大
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 子 View 一字排開(kāi)
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(getWidth() * i, 0, getWidth() * (i + 1), b - t);
}
}
/**
* 瞬時(shí)滾動(dòng)到第幾個(gè)子 View
* @param targetIndex 要移動(dòng)到第幾個(gè)子 View
*/
public void moveToIndex(int targetIndex) {
if (!canMoveToIndex(targetIndex)) {
return;
}
scrollTo(targetIndex * getWidth(), getScrollY());
mCurrentIndex = targetIndex;
invalidate();
}
/**
* 判斷移動(dòng)的子 View 下標(biāo)是否合法
* @param index 要移動(dòng)到第幾個(gè)子 View
* @return index 是否合法
*/
public boolean canMoveToIndex(int index) {
return index < CHILD_NUMBER && index >= 0;
}
public int getCurrentIndex() {
return mCurrentIndex;
}
}
將以上這個(gè)自定義的 ViewGroup 放到 Activity 中,調(diào)用它的 moveToIndex(int targetIndex) 就可以實(shí)現(xiàn)瞬時(shí)滾動(dòng)到第 n 個(gè)子 View 了。(完整示例代碼見(jiàn)DEMO)
Scroller 類(lèi) —— 計(jì)算滾動(dòng)位置的輔助類(lèi)
到目前為止,我們已經(jīng)能通過(guò) View 提供的方法設(shè)置 mScrollX、mScrollY,來(lái)使 View “滾動(dòng)”。但這種滾動(dòng)都是瞬時(shí)的,換句話說(shuō),這種滾動(dòng)都是無(wú)動(dòng)畫(huà)的。實(shí)際上我們想要做到的滾動(dòng)是平滑的、有動(dòng)畫(huà)的,就像我們不希望窗戶外面的那個(gè)人突然出現(xiàn)在窗戶中間,這樣會(huì)嚇到我們,我們更希望那個(gè)人能有一個(gè)“慢慢走進(jìn)視覺(jué)范圍”的過(guò)程。
Scroller 類(lèi)就是幫助我們實(shí)現(xiàn) View 平滑滾動(dòng)的一個(gè)輔助類(lèi),使用方法通常是在 View 中作為一個(gè)成員變量,用 Scroller 類(lèi)來(lái)記錄/計(jì)算 View 的滾動(dòng)位置,再?gòu)?Scroller 類(lèi)中讀取出計(jì)算結(jié)果,設(shè)置到 View 中。這里注意一點(diǎn):在 Scroller 中設(shè)置和計(jì)算 View 的滾動(dòng)位置并不會(huì)影響 View 的滾動(dòng),只有從 Scroller 中取出計(jì)算結(jié)果并設(shè)置到 View 中時(shí),滾動(dòng)才會(huì)實(shí)際生效。
Scroller 提供了一系列方法來(lái)執(zhí)行滾動(dòng)、計(jì)算滾動(dòng)位置,以下列出幾個(gè)重要方法:
// 開(kāi)始滾動(dòng),并記下當(dāng)前時(shí)間點(diǎn)作為開(kāi)始滾動(dòng)的時(shí)間點(diǎn)
public void startScroll(int startX, int startY, int dx, int dy, int duration)
// 停止?jié)L動(dòng)
public void abortAnimation()
// 計(jì)算當(dāng)前時(shí)間點(diǎn)對(duì)應(yīng)的滾動(dòng)位置,并返回動(dòng)畫(huà)是否還在進(jìn)行
public boolean computeScrollOffset()
// 獲取上一次 computeScrollOffset 執(zhí)行時(shí)的滾動(dòng) x 值
public final int getCurrX()
// 獲取上一次 computeScrollOffset 執(zhí)行時(shí)的滾動(dòng) y 值
public final int getCurrY()
// 根據(jù)當(dāng)前的時(shí)間點(diǎn),判斷動(dòng)畫(huà)是否已結(jié)束
public final boolean isFinished()
有了這幾個(gè)方法,我們?nèi)菀紫氲饺绾螌?shí)現(xiàn) View 的平滑滾動(dòng)動(dòng)畫(huà):
- 在開(kāi)始動(dòng)畫(huà)時(shí)調(diào)用
startScroll方法,傳入動(dòng)畫(huà)開(kāi)始位置、移動(dòng)距離、動(dòng)畫(huà)時(shí)長(zhǎng); - 每隔一段時(shí)間,調(diào)用
computeScrollOffset方法,計(jì)算當(dāng)前時(shí)間點(diǎn)對(duì)應(yīng)的滾動(dòng)位置; - 如果上一步返回 true,代表動(dòng)畫(huà)仍在進(jìn)行,則調(diào)用
getCurrX和getCurrY方法獲取當(dāng)前位置,并調(diào)用 View 的scrollTo方法使 View 滾動(dòng); - 不斷循環(huán)進(jìn)行第 2 步,直到返回 false,代表動(dòng)畫(huà)結(jié)束。
這里提到“每隔一段時(shí)間”,從直覺(jué)上我們可能覺(jué)得應(yīng)該有個(gè)循環(huán),但實(shí)際上我們可以借助 View 的 computeScroll 方法來(lái)實(shí)現(xiàn)。先看看 computeScroll 方法的源碼:
// View.java
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
public void computeScroll() {
}
看注釋可知該方法天生就是用來(lái)計(jì)算 View 的 mScrollX 和 mScrollY 值,該方法會(huì)在父 View 調(diào)用該 View 的 draw 方法之前被自動(dòng)調(diào)用,View 類(lèi)中默認(rèn)沒(méi)有實(shí)現(xiàn)任何內(nèi)容,我們需要自己實(shí)現(xiàn)。所以我們只需要在該方法中,用 Scroller 計(jì)算并設(shè)置 mScrollX 和 mScrollY 的值,并判斷如果動(dòng)畫(huà)沒(méi)結(jié)束則讓該 View 失效(調(diào)用 postInvalidate() 方法),觸發(fā)下一次 computeScroll,就可以實(shí)現(xiàn)上述循環(huán)。
例子2
這個(gè)例子的 ViewGroup 繼承自例子 1 的 ViewGroup,擁有同樣的子 View,區(qū)別只在于例子 2 是通過(guò) Scroller 來(lái)滾動(dòng),實(shí)現(xiàn)了滾動(dòng)的動(dòng)畫(huà),而不再是瞬時(shí)滾動(dòng)。
/**
* 示例:自定義一個(gè) ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,
* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 通過(guò) Scroller 實(shí)現(xiàn)滾動(dòng)。
* 調(diào)用 moveToIndex 方法會(huì)觸發(fā) Scroller 的 startScroller,開(kāi)始動(dòng)畫(huà),并使 View 失效。
* 并在 computeScroll 方法中判斷動(dòng)畫(huà)是否在進(jìn)行,進(jìn)而計(jì)算當(dāng)前滾動(dòng)位置,并觸發(fā)下一次 View 失效。
*/
public class Case2ViewGroup extends Case1ViewGroup {
// 滾動(dòng)器
protected Scroller mScroller;
public Case2ViewGroup(Context context) {
super(context);
initScroller();
}
public Case2ViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initScroller();
}
public Case2ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initScroller();
}
private void initScroller() {
mScroller = new Scroller(getContext());
}
/**
* 通過(guò)動(dòng)畫(huà)滾動(dòng)到第幾個(gè)子 View
* @param targetIndex 要移動(dòng)到第幾個(gè)子 View
*/
@Override
public void moveToIndex(int targetIndex) {
if (!canMoveToIndex(targetIndex)) {
return;
}
mScroller.startScroll(
getScrollX(), getScrollY(),
targetIndex * getWidth() - getScrollX(), getScrollY());
mCurrentIndex = targetIndex;
invalidate();
}
public void stopMove() {
if (!mScroller.isFinished()) {
int currentX = mScroller.getCurrX();
int targetIndex = (currentX + getWidth() / 2) / getWidth();
mScroller.abortAnimation();
this.scrollTo(targetIndex * getWidth(), 0);
mCurrentIndex = targetIndex;
}
}
/**
* 在 ViewGroup.dispatchDraw() -> ViewGroup.drawChild() -> View.draw(Canvas,ViewGroup,long) 時(shí)被調(diào)用
* 任務(wù):計(jì)算 mScrollX & mScrollY 應(yīng)有的值,然后調(diào)用scrollTo/scrollBy
*/
@Override
public void computeScroll() {
boolean isNotFinished = mScroller.computeScrollOffset();
if (isNotFinished) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
將以上這個(gè)自定義的 ScrollerViewGroup 放到 Activity 中,調(diào)用它的 moveToIndex(int targetIndex) 就可以實(shí)現(xiàn)滾動(dòng)到第 n 個(gè)子 View 了。(在 Activity 中使用的完整示例代碼見(jiàn)DEMO)
VelocityTracker —— 計(jì)算滾動(dòng)速度的輔助類(lèi)
到目前為止,我們已經(jīng)可以實(shí)現(xiàn) View 平滑的滾動(dòng)動(dòng)畫(huà),那么如果我們還想根據(jù)用戶手指在 View 上滑動(dòng)的速度和距離來(lái)控制 View 的滾動(dòng),應(yīng)該怎么做?Android 系統(tǒng)提供了另一個(gè)輔助類(lèi) VelocityTracker 來(lái)實(shí)現(xiàn)類(lèi)似功能。
VelocityTracker 是一個(gè)速度跟蹤器,通過(guò)用戶操作時(shí)(通常在 View 的 onTouchEvent 方法中)傳進(jìn)去一系列的 Event,該類(lèi)就可以計(jì)算出用戶手指滑動(dòng)的速度,開(kāi)發(fā)者可以方便地獲取這些參數(shù)去做其他事情?;蛘呤种富瑒?dòng)超過(guò)一定速度并松手,就觸發(fā)翻頁(yè)。
看看 VelocityTracker 類(lèi)提供的幾個(gè)常用的方法,這些方法分為幾類(lèi):
-
初始化和銷(xiāo)毀:
// 由系統(tǒng)分配一個(gè) VelocityTracker 對(duì)象,而不是 new 一個(gè) static public VelocityTracker obtain()- // 使用完畢時(shí)調(diào)用該方法回收 VelocityTracker 對(duì)象 public void recycle() -
添加 Event 以供追蹤:
// 不斷調(diào)用該方法傳入一系列 event,記錄用戶的操作 public void addMovement(MotionEvent event) -
計(jì)算速度:
// 計(jì)算調(diào)用該方法的時(shí)刻對(duì)應(yīng)的速度,傳入的是速度的計(jì)時(shí)單位 public void computeCurrentVelocity(int units)// 調(diào)用 computeCurrentVelocity 方法后就可以通過(guò)該方法獲取之前計(jì)算的 x 方向速度 public float getXVelocity()// 調(diào)用 computeCurrentVelocity 方法后就可以通過(guò)該方法獲取之前計(jì)算的 y 方向速度 public float getYVelocity()
例子3
下面通過(guò)一個(gè)例子來(lái)看看 VelocityTracker 的用法。該例子的 ViewGroup 繼承自例子 2 的 ViewGroup,擁有同樣的子 View,區(qū)別在于除了可以用動(dòng)畫(huà)來(lái)滾動(dòng),還可以用手勢(shì)來(lái)拖動(dòng)滾動(dòng)。重點(diǎn)看該 ViewGroup 的 onTouchEvent 方法:
/**
* 示例:自定義一個(gè) ViewGroup,包含幾個(gè)一字排開(kāi)的子 View,
* 每個(gè)子 View 都與該 ViewGroup 一樣大。
* 通過(guò) VelocityTracker 監(jiān)控手指滑動(dòng)速度。
*/
public class Case3ViewGroup extends Case2ViewGroup {
// 速度監(jiān)控器
private VelocityTracker mVelocityTracker;
public Case3ViewGroup(Context context) {
super(context);
}
public Case3ViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public Case3ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 非滑動(dòng)狀態(tài)
private static final int TOUCH_STATE_REST = 0;
// 滑動(dòng)狀態(tài)
private static final int TOUCH_STATE_SCROLLING = 1;
// 表示當(dāng)前狀態(tài)
private int mTouchState = TOUCH_STATE_REST;
// 上一次事件的位置
private float mLastMotionX;
// 觸發(fā)滾動(dòng)的最小滑動(dòng)距離,手指滑動(dòng)超過(guò)該距離才認(rèn)為是要拖動(dòng),防止手抖
private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
// 最小滑動(dòng)速率,手指滑動(dòng)超過(guò)該速度時(shí)才會(huì)觸發(fā)翻頁(yè)
private static final int VELOCITY_MIN = 600;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
//表示已經(jīng)開(kāi)始滑動(dòng)了,不需要走該 ACTION_MOVE 方法了。
if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
return true;
}
final float x = ev.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_MOVE:
final int xDiff = (int) Math.abs(mLastMotionX - x);
//超過(guò)了最小滑動(dòng)距離,就可以認(rèn)為開(kāi)始滑動(dòng)了
if (xDiff > mTouchSlop) {
mTouchState = TOUCH_STATE_SCROLLING;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchState = TOUCH_STATE_REST;
break;
}
return mTouchState != TOUCH_STATE_REST;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
// 速度監(jiān)控器,監(jiān)控每一個(gè) event
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
// 觸摸點(diǎn)
final float eventX = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 如果滾動(dòng)未結(jié)束時(shí)按下,則停止?jié)L動(dòng)
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// 記錄按下位置
mLastMotionX = eventX;
break;
case MotionEvent.ACTION_MOVE:
// 手指移動(dòng)的位移
int deltaX = (int)(eventX - mLastMotionX);
// 滾動(dòng)內(nèi)容,前提是不超出邊界
int targetScrollX = getScrollX() - deltaX;
if (targetScrollX >= 0 &&
targetScrollX <= getWidth() * (CHILD_NUMBER - 1)) {
scrollTo(targetScrollX, 0);
}
// 記下手指的新位置
mLastMotionX = eventX;
break;
case MotionEvent.ACTION_UP:
// 計(jì)算速度
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();
if (velocityX > VELOCITY_MIN && canMoveToIndex(getCurrentIndex() - 1)) {
// 自動(dòng)向右邊繼續(xù)滑動(dòng)
moveToIndex(getCurrentIndex() - 1);
} else if (velocityX < -VELOCITY_MIN && canMoveToIndex(getCurrentIndex() + 1)) {
// 自動(dòng)向左邊繼續(xù)滑動(dòng)
moveToIndex(getCurrentIndex() + 1);
} else {
// 手指速度不夠或不允許再滑
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
moveToIndex(targetIndex);
}
// 回收速度監(jiān)控器
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
//修正 mTouchState 值
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
}
return true;
}
}
在該例子中,在 View 的 onTouchEvent 方法中,在 ACTION_MOVE 手指移動(dòng)中不斷調(diào)用 scrollTo 方法,實(shí)現(xiàn) View 跟隨手指移動(dòng);同時(shí),將 Event 不斷地添加到 mVelocityTracker 速度監(jiān)控器中,并在 ACTION_UP 手指抬起時(shí)從速度監(jiān)控器中獲取速度,當(dāng)速度達(dá)到某一閾值時(shí)自動(dòng)滾動(dòng)到上一頁(yè)或下一頁(yè)。
總結(jié)
至此,我們已經(jīng)了解了 View 的滾動(dòng)原理,并兩個(gè)輔助類(lèi)來(lái)幫助控制 View 的滾動(dòng)位置和滾動(dòng)速度??偨Y(jié)一下:
- View 的顯示可以理解為透過(guò)“視覺(jué)窗口”來(lái)看內(nèi)容,內(nèi)容可以無(wú)限大,改變 View 的
mScrollX和mScrollY可以看到不同的內(nèi)容,實(shí)現(xiàn)瞬時(shí)滾動(dòng)。 - 調(diào)用 View 的
scrollTo或scrollBy方法可以瞬時(shí)滾動(dòng) View。 - Scroller 輔助類(lèi)可以協(xié)助實(shí)現(xiàn) View 的滾動(dòng)動(dòng)畫(huà),實(shí)現(xiàn)方法是:調(diào)用
startScroll方法開(kāi)始滾動(dòng),并在 View 的computeScroll方法中不斷改變mScrollX和mScrollY來(lái)滾動(dòng) View。 - VelocityTracker 輔助類(lèi)可以協(xié)助追蹤 View 的滾動(dòng)速度,通常是在 View 的
onTouchEvent方法中將 Event 傳進(jìn)該類(lèi)中來(lái)追蹤。調(diào)用該類(lèi)的computeCurrentVelocity方法之后,就可以調(diào)用getXVelocity和getYVelocity方法分別獲取 x 方向和 y 方向的速度。
有了上述的知識(shí)和工具后,我們就能實(shí)現(xiàn)很多與滾動(dòng)相關(guān)的效果。
以上,感謝閱讀。