ScrollView 觸控事件一覽
ScrollView 繼承於 FrameLayout,屬於 ViewGroup 控制元件。View 樹的觸控事件是從 ViewGroup 的 dispatchTouchEvent 開始分發的。先判斷 ViewGroup 的 onInterceptTouchEvent 是否攔截,同時這裡也可以呼叫 ViewGroup 的 requestDisallowInterceptTouchEvent 讓 ViewGroup 不呼叫 onInterceptTouchEvent,如果事件被攔截,則呼叫 ViewGroup 的超類即 View 的 dispatchTouchEvent,反之,則呼叫子檢視的 dispatchTouchEvent 。
上圖針對的是 ACTION_DOWN 事件。- Activity 接收事件後,由 dispatchTouchEvent進行分發。Activity 的 dispatchTouchEvent 如果不呼叫 super (無論返回 true or false)則事件不會向下分發。所以一般 activity 的 dispatchTouchEvent 需要呼叫 super 才能向下分發。
- ViewGroup 的 dispatchTouchEvent,用來向下分發事件。如果此方法內不呼叫super,直接返回 true 則代表直接消費終止。返回 false 代表不在分發直接交給復層處理。呼叫 super 則會執行 onInterceptTouchEvent 方法。
- onInterceptTouchEvent 方法用來判斷當前 ViewGroup 是否需要攔截此事件。如果攔截返回 true,則直接呼叫當前 ViewGroup 的 onTouchEvent 自己處理。不需要攔截返回 false 或者直接呼叫 super 即可。
- 最下次 View 的 dispatchTouchEvent 接受到事件後,true 代表消費終止,false 則直接呼叫自己的 onTouchEvent處理事件。注意 View 沒有 onInterceptTouchEvent,此方法只有 ViewGroup 有。
- down 事件在哪個View 消費了,那麼 move 和 up 事件就只會從上向下傳遞到這個 view,不會繼續向下傳遞。
View 之 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}
複製程式碼
根據原始碼可知:
- 首先執行 dispatchTouchEvent 方法。
- 在 dispatchTouchEvent 方法中先執行 onTouch 方法,後執行 onClick 方法(onClick方法在onTouchEvent中執行)。
- 如果 onTouch 返回false或者 mOnTouchListener 為null(控制元件沒有設定 setOnTouchListener 方法)或者控制元件不是enable的情況下會調運 onTouchEvent , dispatchTouchEvent 返回值與 onTouchEvent 返回一樣。
- 如果不是enable的設定了 onTouch 方法也不會執行,只能通過重寫控制元件的 onTouchEvent 方法處理, dispatchTouchEvent 返回值與 onTouchEvent 返回一樣。
- 如果是enable且 onTouch 返回true情況下, dispatchTouchEvent 直接返回true,不會呼叫 onTouchEvent 方法。
View 之 onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
複製程式碼
根據原始碼可知:
- onTouchEvent 方法中會在ACTION_UP分支中觸發 onClick 的監聽。
- 當 dispatchTouchEvent 在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action(也就是說dispatchTouchEvent返回true才會進行下一次action派發)。
ViewGroup 之 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
複製程式碼
根據原始碼可知:
- 使用變數intercepted來標記ViewGroup是否攔截Touch事件的傳遞,mGroupFlags可以根據 requestDisallowInterceptTouchEvent 方法來設定是否攔截的標誌 FLAG_DISALLOW_INTERCEPT 。
- FLAG_DISALLOW_INTERCEPT一旦設定之後,ViewGroup將無法攔截除ACTION_DOWN以外的其他點選事件。ViewGroup會在ACTION_DOWN事件到來時做重置狀態的操作。在resetTouchState方法中重置FLAG_DISALLOW_INTERCEPT標記位。因此,子View呼叫requestDisallowInterceptTouchEvent方法並不能影響ViewGroup對ACTION_DOWN事件的處理。當ViewGroup決定攔截事件後,那麼後續的點選事件將預設交給它處理並且不再呼叫它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT標記位的作用是讓ViewGroup不再攔截事件,前提是ViewGroup不攔截ACTION_DOWN事件。
- dispatchTransformedTouchEvent 將Touch事件傳遞給特定的子View。在該方法中為一個遞迴呼叫,會遞迴呼叫 dispatchTouchEvent 方法。在 dispatchTouchEvent 中如果子View為ViewGroup並且Touch沒有被攔截那麼遞迴呼叫 dispatchTouchEvent ,如果子View為View那麼就會呼叫其 onTouchEvent 。 dispatchTransformedTouchEvent 方法如果返回true則表示子View消費掉該事件,同時進入該if判斷。
- dispatchTransformedTouchEvent 方法返回false,即子View的 onTouchEvent 返回false(即Touch事件未被消費)。那麼該子View就無法繼續處理ACTION_MOVE事件和ACTION_UP事件。
- dispatchTransformedTouchEvent 會呼叫遞迴呼叫 dispatchTouchEvent 和 onTouchEvent ,所以 dispatchTransformedTouchEvent 的返回值實際上是由 onTouchEvent 決定的。
ViewGroup 之 dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
...
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
}
...
}
複製程式碼
根據原始碼可知:
- 當child == null時會將Touch事件傳遞給該ViewGroup自身的dispatchTouchEvent()處理,即super.dispatchTouchEvent(event)(也就是View的這個方法,因為ViewGroup的父類是View);當child != null時會呼叫該子view(當然該view可能是一個View也可能是一個ViewGroup)的dispatchTouchEvent(event)處理,即child.dispatchTouchEvent(event)。
- Android事件派發是先傳遞到最頂級的ViewGroup,再由ViewGroup遞迴傳遞到View的。
- 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,預設返回false。
- 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。
常見滑動衝突場景
外部滑動和內部滑動方向不一致
- ViewPager和Fragment配合使用組成的頁面滑動效果。這種衝突的解決方式,一般都是根據水平滑動還是豎直滑動(滑動的距離差)來判斷到底是由誰來攔截事件。
- 外部滑動和內部滑動方向一致。內外兩層同時能上下滑動或者能同時左右滑動。這種一般都是根據業務來進行區分。
- 以上兩種場景的巢狀
滑動衝突的解決方式
外部攔截法
外部攔截法,就是所有事件都先經過父容器的攔截處理,由父容器來決定是否攔截。這種方式需要重寫父容器的onInterceptTouchEvent方法,虛擬碼如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當前點選事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
複製程式碼
- 不攔截ACTION_DOWN事件。一旦父容器攔截ACTION_DOWN,則後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,無法傳遞給子元素。
- ACTION_MOVE事件根據具體需求來決定是否攔截。
- ACTION_UP事件必須返回false,ACTION_UP事件本身沒什麼意義,但如果父容器在ACTION_UP返回true會導致子元素無法接收ACTION_UP事件,無法響應onClick事件。
內部攔截法
內部攔截法是指父容器不攔截任何事件,所有事件都傳遞給子元素。內部攔截法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。這種方式需要重寫子元素的dispatchTouchEvent方法,虛擬碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要當前點選事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
複製程式碼
父元素需要預設攔截除ACTION_DOWN事件以外的其他事件,父元素修改如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction()==MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
複製程式碼
ACTION_DOWN事件並不受FLAG_DISALLOW_INTERCEPT這個標記位的控制。一旦父容器攔截ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去。
ScrollView 觸控事件流程
onInterceptTouchEvent
onInterceptTouchEvent 所進行的處理,即在 ACTION_DOWN 資源初始化,ACTION_MOVE 判斷是否開始拖動手勢,ACTION_CANCEL && ACTION_UP 中進行資源釋放。這裡涉及了多指觸控的處理。
預處理
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
複製程式碼
用 mIsBeingDragged 變數來儲存當前是否已經開始進行拖動手勢,這個後面會講到,同時當前分發事件型別為 ACTION_MOVE,那麼直接返回 true,即攔截事件向子檢視進行分發。
ACTION_MOVE
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
複製程式碼
如果觸控事件沒有作用於子檢視範圍內,那麼就不處理,同時釋放速度跟蹤器(一般用於 fling 手勢的判定)。
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();
startNestedScroll(SCROLL_AXIS_VERTICAL);
複製程式碼
mLastMotionY 記錄按下時的座標資訊,mActivePointerId 記錄當前分發觸控事件的手指 id,這個一般用於多指的處理,initOrResetVelocityTracker 初始化速度跟蹤器,同時使用 addMovement 記錄當前觸控事件資訊,mScroller 是一般用於 fling 手勢處理,這裡的作用是處理上一次的 fling,startNestedScroll 則是巢狀滾動機制的知識了。
ACTION_MOVE
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
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);
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
複製程式碼
對 mActivePointerId 進行是否為有效的判斷,如果有效,則通過 findPointerIndex 獲取作用手指 id 的下標,記錄為 pointerIndex ,為什麼要獲取這個值,我們知道現在的手機螢幕都是支援多指觸控的,所以我們需要根據某個按下的手指的觸控資訊來進行處理。yDiff 是滑動的距離,mTouchSlop 則是 SDK 定義的可作為判定是否開始進行拖動的距離常量,可以通過 ViewConfiguration 的 getScaledTouchSlop 獲取,如果大於這個值,我們可以認為開始了拖動的手勢。 getNestedScrollAxes 這個同樣是用於巢狀滾動機制的。如果開始了拖動手勢,mIsBeingDragged 標記為 true,同樣使用速度跟蹤器記錄資訊,這裡還會呼叫 ViewParent 的 requestDisallowInterceptTouchEvent,防止父檢視攔截了事件,即 onInterceptTouchEvent。
ACTION_CANCEL && ACTION_UP
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
stopNestedScroll();
複製程式碼
進行一些釋放資源的操作,比如 mIsBeingDragged 設定為 false,釋放速度跟蹤器等等。
ACTION_UP 是所有的手指(多指觸控)抬起時分發的事件,而 ACTION_CANCEL 則是觸控取消事件型別,一般什麼時候會分發這個事件呢?舉個例子,如果某個子檢視已經消費了 ACTION_DOWN,即在這個事件分發時,向父檢視傳遞了 true 的返回值,那麼一般情況下,父檢視不會再攔截接下來的事件,比如 ACTION_MOVE 等,但是如果父檢視在這種情況下,還攔截了事件傳遞,即在 onInterceptTouch 中返回了 true,那麼在 ViewGroup 的 dispatchTouchEvent 中會給已經確認消費事件的子檢視分發一個 TOUCH_CANCEL 的事件。
複製程式碼
ACTION_POINTER_UP
多指觸控時,某個手指抬起時分發的事件。
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
複製程式碼
當某個手指抬起時,而這個手指剛好是我們當前使用的,則重新初始化資源。
onTouchEvent
onTouchEvent 和 onInterceptTouchEvent 處理有些相似,主要是在 TOUCH_MOVE 中在判定為拖動手勢後進行真正的業務邏輯處理,同時在 ACTION_UP 中根據速度跟蹤器的獲取的速度,判定是否符合 fling 手勢,如果符合,則使用 Scroller 進行計算。
ACTION_DOWN
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
複製程式碼
onTouchEvent 在 ACTION_DOWN 事件分發中,主要是進行資源初始化,同時也處理上一次的 fling 任務,比如呼叫 Scroller 的 abortAnimation,如果 Scroller 還沒結束 fling 計算,則中止處理。
ACTION_MOVE
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
// 巢狀滾動處理
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
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 (mIsBeingDragged) {
/// 業務邏輯
}
複製程式碼
進行多指處理,獲取指定手指的觸控事件資訊。mIsBeingDragged 為 false,同時會再進行一次拖動手勢的判定,判定邏輯和 onInterceptTouchEvent 中類似,如果 mIsBeingDragged 為 true,則開始進行真正的邏輯處理。
if (mIsBeingDragged) {
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
mVelocityTracker.clear();
}
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) {
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();
}
}
}
複製程式碼
EdgeEffect 是用於拖動時,邊緣的陰影效果。
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);
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
複製程式碼
當手指全部抬起時,可以使用速度跟蹤器進行 fling 手勢的判定,同時釋放資源。通過 getYVelocity 獲取速度,在判斷是否可以作為 fling 手勢處理,mMaximumVelocity 是處理的最大速度,mMinimumVelocity 是處理的最小速度,這兩個值同樣可以通過 ViewConfiguration 的 getScaledMaximumFlingVelocity 和 getScaledMinimumFlingVelocity 獲取。一般情況對 fling 的處理是通過 Scroller 進行處理的,因為這裡涉及複雜的數學知識,而 Scroller 可以幫我們簡化這裡的操作,使用如下:
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
postInvalidateOnAnimation();
複製程式碼
通過傳遞當前拖動手勢速度值來呼叫 fling 進行處理,然後在 computeScrollOffset 方法中,進行真正的滾動處理:
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = mScrollX;
int oldY = mScrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
postInvalidateOnAnimation();
}
}
複製程式碼
Scroller 並不會為我們進行滾動處理,它只是提供了計算的模型,通過呼叫 computeScrollOffset 進行計算,如果返回 true,表示計算還沒結束,然後通過 getCurrX 或 getCurrY 獲取計算後的值,最後進行真正的滾動處理,比如呼叫 scrollTo 等等,這裡需要注意的是,需要呼叫 invalidate 來確保進行下一次的 computeScroll 呼叫,這裡使用的 postInvalidateOnAnimation 其作用是類似的。
ACTION_CANCEL
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
複製程式碼
釋放資源。
ACTION_POINTER_DOWN
當有新的手指按下時分發的事件。
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
複製程式碼
以新按下的手指的資訊重新計算。
ACTION_POINTER_UP
處理和 onInterceptTouch 一致。
SDK 工具類
系統已經提供 GestureDetector 來進行手勢的判定,我們只需要在相應的手勢回撥方法中進行我們的業務邏輯即可。還有更強大的 ViewDragHelper。