第三章 View的事件體系
學(xué)習(xí)清單:
-
View的事件體系
View的位置參數(shù)
View的觸控參數(shù)
View的滑動
-
View的事件分發(fā)機(jī)制
- 點(diǎn)擊事件傳遞規(guī)則
-
View的滑動沖突
產(chǎn)生原因
常見的滑動沖突場景
處理規(guī)則
解決方案
簡介
在Android的世界中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個View控件
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個界面的控件形成了一個樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測量與繪制下層的控件,并傳遞交互事件
在每棵控件樹的頂部都存在著一個ViewParent對象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對整個視圖進(jìn)行整體控制

一. View 的事件體系
1. View的位置參數(shù)
a. Q:如何確定一個View的位置?
A: View的位置主要通過它的四個頂點(diǎn)來決定, 分別是:
top: 左上角縱坐標(biāo)
left: 左上角橫坐標(biāo)
right: 右下角橫坐標(biāo)
-
bottom: 右下角縱坐標(biāo)
image-20200605112730079.png
b. View的寬高和坐標(biāo)的關(guān)系:
width = right - left;
height = bottom - top;
// 獲取這四個參數(shù)的方法
Left = getLeft();
Right = getRight();
Top = getTop()
Bottom = getBottom();
c. 從Android3.0開始, View增加了額外的四個參數(shù): x, y, translationX 和 translationY, 其中x 和 y 是View左上角坐標(biāo), 而translationX 和 translationY 是View左上角相對于父容器的偏移量, 這幾個參數(shù)也是相對于父容器的坐標(biāo), 關(guān)系如下圖:

- 換算關(guān)系: x = left + translationX, y = top + translationY
- X由此可見, x和left不同體現(xiàn)在:left是View的初始坐標(biāo), 在繪制完畢后就不會再改變;而x是View偏移后的實(shí)時坐標(biāo), 是實(shí)際坐標(biāo). y和top的區(qū)別同理
2. View的觸控參數(shù)
a. MotionEven 和 TouchSlop:
-
MotionEven的觸摸事件:
ACTION_DOWN : 手指放接觸屏幕
ACTION_MOVE : 手指在屏幕上移動
ACTION_UP : 手指從屏幕上松開的一瞬間
正常情況下, 一次手指觸碰屏幕的行為可能觸發(fā)一系列點(diǎn)擊事件, 如:
- 點(diǎn)擊屏幕后立刻松開: DOWN -> UP;
- 點(diǎn)擊屏幕后一會再松開: DOWN -> MOVE -> ... -> MOVE -> UP;
-
通過MotionEven對象我們可以得到事件發(fā)生的 x 和 y 坐標(biāo):
getX() / getY(): 返回相對于當(dāng)前View左上角的 x, y 坐標(biāo)
getRawX() / getRawY(): 返回相對于手機(jī)屏幕左上角的 x , y 坐標(biāo)
-
TouchSlop的使用:
TouchSlop: 是系統(tǒng)所能識別出的滑動最小距離, 是一個常量, 不同的設(shè)備上這個值可能是不同的
-
通過 ViewConfiguration.get(getContext()).getScaledTouchSlop()可以獲得這個常量
使用建議:
- 通過TouchSlop, 可以對用戶的一些操作進(jìn)行過濾, 提高用戶使用體驗(yàn)
b. VelocityTracker 和 GestureDetector:
-
VelocityTracker速度追蹤:
功能: 用于追蹤手指在滑動過程中的速度, 包括水平和豎直方向的速度
-
使用:
- 在View的onTouchEvent()方法中追蹤當(dāng)前單擊事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event);- 獲取滑動速度
int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity();注意:
- 獲取速度之前必須調(diào)用computerCurrentVelocity方法
- 獲取到的速度指的是在規(guī)定時間內(nèi)劃過的像素?cái)?shù)
- 獲取到的速度可以為負(fù)數(shù), 從右向左 / 從下向上 獲取到的就是負(fù)數(shù)
- 速度的公式 : 速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時間段
- 在不使用VelocityTracker的時候需要調(diào)用clear方法來重置并回收內(nèi)存, recycle方法重新調(diào)用
-
GestureDetector手勢檢測
功能: 用于檢測用戶的單擊, 滑動, 長按, 雙擊等行為
-
使用:
創(chuàng)建一個GestureDetector對象
-
根據(jù)不同的需求實(shí)現(xiàn)OnGestureListener接口或OnDoubleTapListener接口
方法名 描述 所屬接口 onDown 手指輕觸屏幕的一瞬間, 由1個ACTION_DOWN觸發(fā) OnGestureListener onShowPress 手指輕觸屏幕尚未松開或移動 OnGestureListener onSingleTapUp 手指輕觸屏幕后松開, 伴隨1個ACTION_UP觸發(fā) OnGestureListener onScroll 手指輕觸屏幕并拖動 OnGestureListener onLongPress 長按屏幕不放 OnGestureListener onFling 觸摸屏幕快速滑動后松開 OnGestureListener onDoubleTap 雙擊, 不能和onSingleTapConfirmed共存 OnDoubleTapLinstener onSingleTapConfirmed 嚴(yán)格單擊行為, 指不能是雙擊中的一次單擊 OnDoubleTapLinstener onDoubleTapEvent 發(fā)生了雙擊行為, 在雙擊期間移動也會觸發(fā) OnDoubleTapLinstener
注意: 在實(shí)際開發(fā)中, 如果需要監(jiān)聽雙擊事件, 則使用GestureDetector, 否則可以在View的onTouchEvent方法中實(shí)現(xiàn)
3. View的滑動
a. 通過View本身的scrollTo / scrollBy實(shí)現(xiàn):
-
方法:
scrollTo: 基于所傳遞參數(shù)的絕對滑動
scrollBy: 實(shí)際上是通過調(diào)用scroolTo方法實(shí)現(xiàn), 傳遞的是偏移量
注意: 通過scrollTo / scrollBy只能改變View的內(nèi)容, 不能改變View在當(dāng)前布局中的位置
b. 使用動畫
-
使用
xml文件的方式:- xml代碼:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" > <translate android:fromXDelta="0" android:fromYDelta="0" android:toXDelta="100" android:toYDelta="100" android:duration="2000" /> </set>- java代碼:
Animation animation = AnimationUtils.loadAnimation(this, R.anim.translate); view.startAnimation(animation);推薦閱讀: Animation補(bǔ)間動畫
-
注意: 這種動畫只能改變View的內(nèi)容所在的位置, 真身仍在原來的位置
關(guān)于動畫的內(nèi)容, 會在第7章詳細(xì)說明
c. 改變布局參數(shù)
說明: 通過改變LayoutParams來實(shí)現(xiàn), 或例如在Button旁邊放置一個View, 通過改變這個View的大小來實(shí)現(xiàn)
注意: 在修改了LayoutParams后記得使用
requestLayout()方法更新
d. 三種方式的優(yōu)缺點(diǎn):
scrollTo / scrollBy : 操作簡單, 適合對View內(nèi)容的滑動;
動畫: 操作簡單, 主要適用于不與用戶交互, 復(fù)雜的動畫效果
改變布局參數(shù): 操作稍微復(fù)雜, 但適用與有交互的View
4.View的彈性滑動
View的滑動效果顯得太過生硬, Android中還提供了許多彈性滑動的方法, 下面記錄一下Android中的彈性滑動
a. 使用Scroller
-
使用:
創(chuàng)建Scroller的實(shí)例
調(diào)用startScroll()方法來初始化滾動數(shù)據(jù)并刷新界面
重寫computeScroll()方法,并在其內(nèi)部完成平滑滾動的邏輯
-
慣用代碼:
? private void smoothScrollTo(int dstX, int dstY) { int scrollX = getScrollX();//View的左邊緣到其內(nèi)容左邊緣的距離 int scrollY = getScrollY();//View的上邊緣到其內(nèi)容上邊緣的距離 int deltaX = dstX - scrollX;//x方向滑動的位移量 int deltaY = dstY - scrollY;//y方向滑動的位移量 scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動 invalidate(); //刷新界面 } ? @Override//計(jì)算一段時間間隔內(nèi)偏移的距離,并返回是否滾動結(jié)束的標(biāo)記 public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurY()); postInvalidate();//通過不斷的重繪不斷的調(diào)用computeScroll方法 } }其中startScroll源碼如下,可見它并沒有進(jìn)行實(shí)際的滑動操作,而是通過后續(xù)invalidate()方法去做滑動動作
public void startScroll(int startX,int startY,int dx,int dy,int duration){ mMode = SCROLL_MODE; mFinished = false; mDuration = duration;//滑動時間 mStartTime = AnimationUtils.currentAminationTimeMills();//開始時間 mStartX = startX;//滑動起點(diǎn) mStartY = startY;//滑動起點(diǎn) mFinalX = startX + dx;//滑動終點(diǎn) mFinalY = startY + dy;//滑動終點(diǎn) mDeltaX = dx;//滑動距離 mDeltaY = dy;//滑動距離 mDurationReciprocal = 1.0f / (float)mDuration; }具體過程:在MotionEvent.ACTION_UP事件觸發(fā)時調(diào)用startScroll方法->馬上調(diào)用invalidate/postInvalidate方法->會請求View重繪,導(dǎo)致View.draw方法被執(zhí)行->會調(diào)用View.computeScroll方法,此方法是空實(shí)現(xiàn),需要自己處理邏輯。具體邏輯是:先判斷computeScrollOffset,若為true(表示滾動未結(jié)束),則執(zhí)行scrollTo方法,它會再次調(diào)用postInvalidate,如此反復(fù)執(zhí)行,直到返回值為false。如圖所示:

- 原理: 原理:Scroll的computeScrollOffset()根據(jù)時間的流逝動態(tài)計(jì)算一小段時間里View滑動的距離,并得到當(dāng)前View位置,再通過scrollTo繼續(xù)滑動。即把一次滑動拆分成無數(shù)次小距離滑動從而實(shí)現(xiàn)彈性滑動。
b. 使用動畫:
動畫本身就是一種漸近的過程,故可通過動畫來實(shí)現(xiàn)彈性滑動
-
代碼:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
c. 使用延時策略:
描述: 通過Handler / View的postDelayed方法發(fā)送一系列延時消息從而打到一種漸進(jìn)式的效果, 也可以用線程的sleep方法
注意: 對彈性滑動完成總時間有精確要求的使用場景下, 使用延時策略是一個不太合適的選擇
二. View的事件分發(fā)機(jī)制
事件分發(fā)機(jī)制是View的核心知識點(diǎn), 通過學(xué)習(xí)事件分發(fā)機(jī)制可以解決滑動沖突難題, 鞏固我們對View的掌握
1. 點(diǎn)擊事件傳遞規(guī)則
a. 事件分發(fā)本質(zhì):
就是對MotionEvent事件分發(fā)的過程。即當(dāng)一個MotionEvent產(chǎn)生了以后,系統(tǒng)需要將這個點(diǎn)擊事件傳遞到一個具體的View上
-
傳遞順序:
- Activity(Window) -> ViewGroup -> View
補(bǔ)充: 如果所有元素都不處理這個事件, 那么這個事件最終會由Activity處理, 即Activity的onTouchEvent方法會被調(diào)用

b. 核心方法:
-
public boolean dispatchTouchEvent(MotionEvent ev):
用于進(jìn)行事件的分發(fā), 如果事件能傳遞給當(dāng)前View, 則此方法一定調(diào)用. 返回結(jié)果受到當(dāng)前View的onTouchEvent和下級dispatchTouchEvent影響, 表示是否消耗當(dāng)前事件
-
public boolean onInterceptTouchEvent(MotionEvent event):
在dispatchTouchEvent方法中調(diào)用, 用于判斷是否攔截當(dāng)前事件, 如果當(dāng)前View攔截了某個事件, 則同一個任務(wù)序列中此方法不會再被調(diào)用(只有ViewGroup有這個方法)
-
public boolean onTouchEvent(MotionEvent event):
在dispatchTouchEvent方法中調(diào)用, 用于處理點(diǎn)擊事件, 返回結(jié)果表示是否消耗當(dāng)前事件, 如果不消耗, 則在同一個任務(wù)序列中, 當(dāng)前View無法再次接受到事件
補(bǔ)充閱讀: Android事件分發(fā)機(jī)制(源碼)
三. View的滑動沖突
a. 產(chǎn)生原因:
- 一般情況下,在一個界面里存在內(nèi)外兩層可同時滑動的情況時,會出現(xiàn)滑動沖突現(xiàn)象
b. 常見的滑動沖突場景:
外部滑動方向和內(nèi)部滑動方向不一致;
外部滑動方向和內(nèi)部滑動方向一致;
上述兩種情況的嵌套;
c. 處理規(guī)則:
對于場景一: 左右滑動時, 讓外部View攔截事件. 上下滑動時, 讓內(nèi)部View攔截事件
對于場景二: 根據(jù)相應(yīng)的業(yè)務(wù)情景做出相應(yīng)的操作
對于場景三: 將組合問題根據(jù)場景拆分成若干個小問題, 逐一解決
Q: 如何判斷是左右滑動還是上下滑動:
- 根據(jù)滑動路徑與水平方向上的夾角
- 根據(jù)水平和豎直方向上的速度差
- 根據(jù)水平和豎直方向上的距離差
d. 解決方案:
-
外部攔截法:
含義: 先經(jīng)過父容器, 如果需要就攔截, 不需要再分發(fā)到子View
方法: 重寫父容器的onInterceptTouchEvent方法, 在內(nèi)部做相應(yīng)的攔截
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { //對于ACTION_DOWN事件必須返回false,一旦攔截后續(xù)事件將不能傳遞給子View case MotionEvent.ACTION_DOWN: intercepted = false; break; //對于ACTION_MOVE事件根據(jù)需要決定是否攔截 case MotionEvent.ACTION_MOVE: if (父容器需要當(dāng)前事件){ intercepted = true; } else{ intercepted = flase; break; } //對于ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會觸發(fā) case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; } -
內(nèi)部攔截法
含義: 父容器不攔截任何事件, 如果子元素不需要就交由父容器處理
方法: 重寫子元素的dispatchTouchEvent方法, 再配合requestDisallowInterceptTouchEvent方法,
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// parent.requestDisallowInterceptTouchEvent()可以理解為:
// 告訴(request)父容器(parent)
// 不再(disallow)攔截(intercept)觸摸事件(touchEvent)嗎(boolean)
// 當(dāng)requestDisallowInterceptTouchEvent(ture)時
// 父容器不再攔截接下來的一系列事件
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點(diǎn)擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父View需要重寫onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
