Android進階學習(三)View的事件體系
文本是閱讀《Android開發藝術探索》的學習筆記記錄,詳細內容可以自行閱讀書本。
View的事件體系
1 View基礎知識
1.1 什麼是View
View是Android中所有控件的基類。不管是簡單的Button和TextView還是複雜的RelativeLayout和ListView,它們的共同基類都是View。還有ViewGroup也繼承了View,它內部可以包含多個View。
1.2 View的位置參數
View的位置主要由它四個頂點確定,分別對應View的四個屬性:top、left、right、bottom。在Android中,它的座標系與我們所看到的有區別,如圖所示。
1.3 MotionEvent和TouchSlop
1)MotionEvent
在手指接觸屏幕後所產生的一系列事件中,典型的事件類型如下幾種:
ACTION_DOWN ——手指剛接觸屏幕
ACTION_MOVE ——手指在屏幕上移動
ACTION_UP ——手指從屏幕上鬆開
2)TouchSlop
TouchSlop是系統所能夠識別出的被認爲是滑動的最小距離。可以通過ViewConfiguration.get(this).getScaledTouchSlop()獲取。
1.4 Scroller
彈性滑動對象,用於實現View的彈性滑動。當使用View的scrollTo/scrollBy時,其過程是瞬間完成的。這個時候就可以用Scroller來實現過渡滑動效果。它需要和View的computeScroll方法配合使用,如下:
scroller = new Scroller(context);
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
2 View的滑動
通過三種方式可以實現View的滑動:第一種是通過View本身提供的scrollTo/scrollBy方法;第二種是通過動畫實現滑動;第三種是通過修改View的LayoutParams使得View重新佈局從而實現滑動。
2.1使用scrollTo/scrollBy
先來看看兩個方法的源碼
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
首先scrollBy方法也是調用了scrollTo方法,scrollBy方法是相對當前位置移動,而scrollTo是指定位置的絕對移動。通過先判斷傳進來的(x, y)值是否和View的X, Y偏移量相等,如果不相等,就調用onScrollChanged()方法來通知界面發生改變,然後重繪界面。
舉個例子,調用兩次scrollTo(-10, 0),View第一次會向左滑動10,第二次不變化。調用兩次scrollBy(-10, 0),View第一次向左滑動10,第二次再向左滑動10。
我們要先理解View 裏面的兩個成員變量mScrollX, mScrollY,X軸方向的偏移量和Y軸方向的偏移量,這個是一個相對距離,相對的不是屏幕的原點,而是View的左邊緣和上邊緣。
變化規律如圖所示:
2.2 使用動畫
上一篇介紹過了,直接使用屬性動畫。
ObjectAnimator.ofFloat(stopWebBt, "translationY", -startWebBt.getHeight()).start();
2.3 改變佈局參數
修改LayoutParams參數
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams)
button.getLayoutParams();
marginLayoutParams.leftMargin += 100;
button.requestLayout();
3 彈性滑動
彈性滑動的方式很多,比如通過Scroller、動畫、延時策略等等
3.1 使用Scroller
//彈性滑動到指定位置
public void smoothScrollto(int destX, int destY) {
int x = getScrollX();
int deltaX = destX - x;
//1000ms內
scroller.startScroll(x, 0, deltaX, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
下面探索一下Scroller的內部工作原理,我們調用Scroller的startScroll方法,其實Scroller內部什麼也沒有做,這是保存了傳遞進去的參數,源碼如下。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
真正推動彈性滑動的是我們的invalidate()方法,它導致View重繪,View重繪時又調用我們實現的computeScroll()方法,其中又調用postInvalidate()方法進行二次重繪,直到滑動結束。源碼如下:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
該方法主要兩個作用,一個是判斷滑動是否結束,一個是如果未結束,根據時間流失的百分比來給CurrX和CurrY賦值,幫助滑動。
3.2 通過動畫
動畫上篇結束了,就不多說了。動畫實現彈性滑動非常輕鬆,也提供了許多豐富的api。
3.3 延時策略
它的核心思想結束通過發送延時消息來達到漸進式的效果,可以使用Handler的postDelayed方法;線程的sleep方法等等。
4 View的事件分發機制
4.1 點擊事件的傳遞
點擊事件的分發由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。
dispatchTouchEvent,進行事件分發,如果事件傳遞給當前View,此方法一定被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent影響,表示是否消耗當前事件。
onInterceptTouchEvent,內部調用,用來攔截某個事件,如果當前View攔截了事件,那麼同一個事件序列中,此方法不再調用,返回結果表示是否攔截當前事件。
onTouchEvent,在dispatchTouchEvent方法中調用,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。
當一個點擊事件產生後,它的傳遞順序:Activity -> Window -> View。
4.2 事件分發的源碼分析
1)Activity對點擊事件的分發過程
當一個點擊操作發生時,事件最先傳遞給當前Activity,由activity的dispatchTouchEvent進行事件分發,具體工作由activity內部的Window來完成。Window會將事件傳遞給decor view,decor view一般就是當前界面的底層容器(即setContentView所設置的View的父容器)。
先從activity的dispatchTouchEvent開始分析
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//Window進行分發
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//未有view消耗事件,則調用activity的onTouchEvent
return onTouchEvent(ev);
}
首先事件交給activity所附屬的Window進行分發,如果返回true,整個事件結束。否則調用activity的onTouchEvent。
2)Window點擊事件的分發過程
由於Window唯一的實現是PhoneWindow,從源碼上看:
@Override
public boolean superDispatchGenericMotionEvent(MotionEvent event) {
return mDecor.superDispatchGenericMotionEvent(event);
}
PhoneWindow將事件直接傳遞給了mDecor ,而mDecor = (DecorView) preservedWindow.getDecorView();也就是事件一定會傳遞到view。
3)頂層View點擊事件的分發過程
頂層ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由頂層ViewGroup處理,這時如果頂層ViewGroup的OnTouchListener被設置,則onTouch會被調用,否則onTouchEvent會被調用。如果頂層ViewGroup不攔截事件,則事件會傳遞給它所在的點擊事件鏈的子View。
先看ViewGroup的攔截源碼
// Check for interception.
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); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
代碼中可以看出,ViewGroup兩種情況下會判斷是否攔截事件:ACTION_DOWN和mFirstTouchTarget。從後面代碼可知子View如果成功處理事件,會將mFirstTouchTarget賦值。即當ViewGroup不攔截事件交給子View處理時,mFirstTouchTarget != null。反之ViewGroup自己處理了該事件後,mFirstTouchTarget == null,onInterceptTouchEvent不再被調用,其餘點擊事件由ViewGroup自己處理。
特殊情況,FLAG_DISALLOW_INTERCEPT標記位,這個是通過子View調用設置。原來控制ViewGroup不攔截除了ACTION_DOWN以外的其他點擊事件。因爲當ACTION_DOWN事件到來時,ViewGroup會重置FLAG_DISALLOW_INTERCEPT標記位。源碼如下:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
從上面源碼分析,得知當ViewGroup決定攔截事件後,後續點擊事件不再調用onInterceptTouchEvent,直接交給它處理。
再看ViewGroup不攔截事件時,事件會向下分發給它的子View處理。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
從上段代碼可以看出,遍歷ViewGroup的子View,判斷子View是否能夠接收到點擊事件。兩點判斷:是否再播放動畫和點擊事件座標是否落在子View區域內。滿足則調用dispatchTransformedTouchEvent方法,部分源碼如下:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
該方法內如果傳入參數child 不爲空,則調用子View的dispatchTouchEvent方法。該方法返回true,則跳出for循環,並在addTouchTarget方法內對mFirstTouchTarget賦值。
4)View對點擊事件的處理過程
View是單獨元素,沒有子View進行傳遞,部分dispatchTouchEvent源碼:
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;
}
首先,判斷是否設置mOnTouchListener,有則返回true,並且不調用onTouchEvent。
5 View的滑動衝突
5.1 常見滑動衝突場景
場景一:外部滑動和內部滑動方向不一致
場景二:外部滑動和內部滑動方向一致
場景三:上面兩種情況的嵌套
5.2 滑動衝突的處理規則
場景一:根據滑動是水平還是豎直來判斷誰來攔截事件
場景二:需要根據業務來判斷,由誰來攔截事件
場景一:需要根據業務來判斷,由誰來攔截事件
5.3 滑動衝突的解決方法
1.外部攔截法
是指經過父類容器的攔截處理,父類容器需要則攔截此事件,反之傳遞給子View處理。
@Override
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;
}
return intercepted;
}