1. Touch事件和繪制事件的異同之處
Touch事件和繪制事件很類似,都是由ViewRoot派發(fā)下來的,但是不同之處在繪制事件是由應(yīng)用中的某個View發(fā)起請求,一層一層上傳到ViewRoot,再有ViewRoot下發(fā)繪制,傳遞canvas給所有子View讓其繪制自身,繪制好后,再通知WMS進(jìn)行畫到屏幕上。而Touch事件是由硬件捕獲到觸摸后由系統(tǒng)傳遞給應(yīng)用的ViewRoot,再由ViewRoot往下一層一層傳遞。
他們的處理過程都是自上而下的分發(fā),但是繪制多了一層自下往上的請求。
事件存在消耗,事件的處理方法都會返回一個boolean值,如果該值為true,則本次事件下發(fā)將會終止。
2. MotionEvent
2.1 MotionEvent對象的產(chǎn)生
系統(tǒng)有一個線程在循環(huán)收集屏幕硬件信息,當(dāng)用戶觸摸屏幕時,該線程會把從硬件設(shè)備收集到的信息封裝成一個MotionEvent對象,然后把該對象存放到一個消息隊列中。
系統(tǒng)的另一個線程循環(huán)的讀取消息隊列中的MotionEvent,然后交給WMS去派發(fā),WMS把該事件派發(fā)給當(dāng)前處于活動的Activity,即處于活動棧最頂端的Activity。
這就是一個先進(jìn)先出的消費(fèi)者和生產(chǎn)者的模板,一個線程不停的創(chuàng)建MotionEvent對象放入隊列中,另一個線程不斷的從隊列中取出MotionEvent對象進(jìn)行分發(fā)。
當(dāng)用戶的手指從接觸屏幕到離開屏幕,是一個完整的觸摸事件,在該事件中,系統(tǒng)會不斷收集事件信息封裝成MotionEvent對象。收集的間隔時間取決于硬件設(shè)備,例如屏幕的靈敏度以及cpu的計算能力。目前的手機(jī)一般在20毫秒左右。
MotionEventCompat.getActionMasked()
2.2 MotionEvent對象詳解
MotionEvent對象包含了觸摸事件的時間、位置、面積、壓力、以及本次事件的Dwon發(fā)生的時間。
MotionEvent常用的Action分為5種:Down 、Up、Move、Cancel、OutSide
MotionEvent中我們常用的方法就是獲取點(diǎn)擊的坐標(biāo),因為這是與我們操作息息相關(guān)的。獲取坐標(biāo)有兩種方式:
- getX和getY用于獲取以該View左上角為坐標(biāo)原點(diǎn)的坐標(biāo)
- getRowX和getRowY用于獲取以屏幕左上角為坐標(biāo)原點(diǎn)的坐標(biāo)
2.3 5種Touch事件
- Down:一次觸摸事件的第一個MotionEvent對象,即手指初次接觸屏幕。
- Up:通常為一次觸摸事件的最后一個MotionEvent對象,即手指離開屏幕。
- Move:通常多次發(fā)生在一次觸摸事件之中。表示觸摸點(diǎn)發(fā)生了移動,我們通常把手指放到屏幕上,實(shí)際也會觸發(fā)該事件,因為人手總是在輕微抖動的。
- Cancel:常用于取消某個觸摸事件,一般是由程序邏輯來指定該事件,用于取消某次觸摸事件。
- OutSide:當(dāng)觸摸點(diǎn)發(fā)生在響應(yīng)事件的View之外時,傳遞的事件,通常由程序邏輯來指定。
在上面5種事件中,Down為最重要的事件,因為這是一個觸摸事件的起始點(diǎn),程序的很多邏輯判斷,都需要根據(jù)該事件做處理,例如分發(fā)攔截。一次觸摸事件必須要有Down事件,這也是MotionEvent對象中都包含了本次觸摸事件的Down事件發(fā)生的時間點(diǎn)這個屬性。其次是Move和Up,通過這3個事件的邏輯處理,就構(gòu)建出來滑動,點(diǎn)擊,長按,雙擊等多種效果。
2.4 創(chuàng)建一個MotionEvent對象
public static MotionEvent obtain(
long downTime, //當(dāng)用戶最初按下開始一連串的位置事件。這必須得到SystemClock.uptimeMillis()
long eventTime, //當(dāng)這個特定的事件是生成的。這必須得到SystemClock.uptimeMillis()
int action, //該次事件的Action
float x, //該次事件的x坐標(biāo)
float y, //該次事件的y坐標(biāo)
float pressure, //該次事件的壓力,通常感覺標(biāo)準(zhǔn)壓力,從0-1取值
float size, //點(diǎn)擊的區(qū)域大小,通常根據(jù)特定標(biāo)準(zhǔn)范圍從0-1取值
int metaState, //一個修飾性的狀態(tài),好像一直都是0
float xPrecision, //x坐標(biāo)的精確度
float yPrecision, //y坐標(biāo)的精確度
int deviceId, //觸屏設(shè)備id,如果是0,說明這個事件不是來自物理設(shè)備
int edgeFlags //系統(tǒng)默認(rèn)都是返回0,程序在傳遞時,可以通過邏輯判斷加入方向位置
)
或者一個更簡單的方式:
public static MotionEvent obtain(
long downTime,
long eventTime,
int action,
float x,
float y,
int metaState)
也可以通過一個MotionEvent來創(chuàng)建一個新的
public static MotionEvent obtain(MotionEvent event)
通過以上的方式,我們知道,我們也可以通過代碼來構(gòu)建一個虛假的MotionEvent,并分發(fā)下去。
view.dispatchTouchEvent(
MotionEvent.obtain(SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_DOWN,100,100,0));
然后通過延遲以此往下派發(fā)Move和Up時間,形成一個完整的觸摸操作。
3. dispatchTouchEvent觸摸事件分發(fā)
之前我們知道觸摸事件是被包裝成MotionEvent進(jìn)行傳遞的,而該對象是繼承了Parcelable接口,正因為如此,才可以從系統(tǒng)中傳遞到我們的應(yīng)用中。系統(tǒng)通過跨進(jìn)程通知ViewRoot,ViewRoot會調(diào)用DecorView的dispatchTouchEvent下發(fā)。
這里有一個和其他事件傳遞不同的地方,DecorView會優(yōu)先傳遞給Activity,而不是它的子View。而Activity如果不處理又會回傳給DecorView,DecorView才會再將事件傳給子View。
dispatchTouchEvent就是觸摸事件傳遞的對外接口,無論是DecorView傳給Activity,還是ViewGroup傳遞給子View,都是直接調(diào)用對方的dispatchTouchEvent方法,并傳遞MotionEvent參數(shù)。
我們首先來看看Activity中的dispatchTouchEvent邏輯:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
//這是一個空實(shí)現(xiàn)的方法,以便子類實(shí)現(xiàn),該方法在Key事件和touch事件的dispatch方法中都被調(diào)用,
// 就是方便用戶在事件被傳遞之前做一下自己的處理。
}
//這才是事件真正的分發(fā)
if (getWindow().superDispatchTouchEvent(ev)) {
//superDispatchTouchEvent是一個抽象方法,但是getWindow()獲取的對象實(shí)際是FrameWork層的
// PhoneWindow,該對象實(shí)現(xiàn)了這個方法,內(nèi)部是直接調(diào)用DecorView的superDispatchTouchEvent
// 是直接調(diào)用dispatchTouchEvent,這樣就傳遞到子View中了
return true;
}
//如果上面事件沒有被消費(fèi)掉,那么就調(diào)用Activity的onTouchEvent事件。
return onTouchEvent(ev);
}
//PhoneWindow的superDispatchTouchEvent方法直接調(diào)用了mDecor的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//mDecor即為Activity真正的根View,我們通過setContentView所添加的內(nèi)容就是添加在該View上,
// 它實(shí)際上就是一個FrameLayout
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
}
至此我們已經(jīng)至少明白了以下幾點(diǎn):
1、我們可以重載Activity的onUserInteraction方法,在Down事件觸發(fā)傳遞前,實(shí)現(xiàn)我們的一些需求,實(shí)際上源碼中有很多這樣的方法,再某個方法體的第一行提供一個空實(shí)現(xiàn)的回調(diào)方法,在某個方法的最后一行提供一個空實(shí)現(xiàn)的回調(diào)方法,以便子類去實(shí)現(xiàn)自己的邏輯,例如AsyncTask就有類似的方式。這些技巧都能很好的提高我們代碼的擴(kuò)展性。
2、Activity會間接的調(diào)用根View的dispatchTouchEvent,并通過if判斷返回值,如果為true,即向上層返回true,也就是調(diào)用Activity的dispatchTouchEvent的WMS,即操作系統(tǒng)。
3、如果if判斷為false,即根View和根View下的所有子View均為消費(fèi)掉該事件,那么下面的代碼就有執(zhí)行機(jī)會,即Activity的onTouchEvent,并把該方法的返回值作為結(jié)果返回給上層。
3.1 View的dispatchTouchEvent
View中的處理相當(dāng)簡單明了,因為不涉及到子View,所以只在自身內(nèi)部進(jìn)行分發(fā)。首先判斷是否設(shè)置了觸摸監(jiān)聽,并且可以響應(yīng)事件,就交由監(jiān)聽的onTouch處理。如果上述條件不成立,或者監(jiān)聽的onTouch事件沒有消費(fèi)掉該事件,則交由onTouchEvent進(jìn)行處理,并把返回結(jié)果交給上層。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
//判斷mOnTouchListener是否存在,并且控件可點(diǎn)的情況下,執(zhí)行onTouch,如果onTouch返回true,就消耗該事件
return true;
}
//如果以上條件都不成立,則把事件交給onTouchEvent來處理
return onTouchEvent(event);
}
3.2 ViewGroup的dispatchTouchEvent
3.3 Down事件
- 通過onInterceptTouchEvent方法判斷是否要攔截事件,默認(rèn)fasle
- 根據(jù)scroll換算后的坐標(biāo)找出所接受的子View。有動畫的子View將不接受觸摸事件。
- 找到能接受的子View后把event中的坐標(biāo)轉(zhuǎn)換成子View的坐標(biāo)
- 調(diào)用子View的dispatchTouchEvent把事件傳遞給子View。
- 如果子View消費(fèi)了該事件,則把target記錄為子View,方便后面的Move和Up事件的傳遞。
- 如果子View沒有消費(fèi),則繼續(xù)尋找下一個子View。
- 如果沒找到,或者找到的子View都不消費(fèi),就會調(diào)用View的dispatchTouchEvent的邏輯,也就是判斷是否有觸摸監(jiān)聽,有的話交給監(jiān)聽的onTouch處理,沒有的話交給自己的onTouchEvent處理
接下來我們來研究ViewGroup的dispatchTouchEvent,這是稍微復(fù)雜的分發(fā)邏輯。
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();//獲取事件
final float xf = ev.getX();//獲取觸摸坐標(biāo)
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;//獲取當(dāng)前需要偏移的偏移量量
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect; //當(dāng)前ViewGroup的視圖矩陣
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止攔截
if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件
if (mMotionTarget != null) { //判斷接受事件的target是否為空
//不為空肯定是不正常的,因為一個事件是由DOWN開始的,而DOWN還沒有被消費(fèi),所以目標(biāo)也不是不可能被確定,
//造成這個的原因可能是在上一次up事件或者cancel事件的時候,沒有把目標(biāo)賦值為空
mMotionTarget = null; //在此處挽救
}
//不允許攔截,或者onInterceptTouchEvent返回false,也就是不攔截。注意,這個判斷都是在DOWN事件中判斷
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
//從新設(shè)置一下事件為DOWN事件,其實(shí)沒有必要,這只是一種保護(hù)錯誤,防止被篡改了
ev.setAction(MotionEvent.ACTION_DOWN);
//開始尋找能響應(yīng)該事件的子View
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {//如果child可見,或者有動畫,獲取該child的矩陣
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// 設(shè)置系統(tǒng)坐標(biāo)
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
if (child.dispatchTouchEvent(ev)) {//調(diào)用child的dispatchTouchEvent
//如果消費(fèi)了,目標(biāo)就確定了,以便接下來的事件都傳遞給child
mMotionTarget = child;
return true; //事件消費(fèi)了,返回true
}
}
}
}
//能到這里來,證明所有的子View都沒消費(fèi)掉Down事件,那么留給下面的邏輯進(jìn)行處理
}
}
//判斷是不是up或者cancel事件
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
//如果是取消,把禁止攔截這個標(biāo)志位給取消
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
//判斷該值是否為空,如果為空,則沒找到能響應(yīng)的子View,那么直接調(diào)用super的dispatchTouchEvent,也就是View的dispatchTouchEvent
ev.setLocation(xf, yf);
return super.dispatchTouchEvent(ev);
}
//能走到這里來,說明已經(jīng)有target,那也說明,這里不是DOWN事件,因為DOWN事件如果有target,已經(jīng)在前面返回了,執(zhí)行不到這里
if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果有目標(biāo),又非要攔截,則給目標(biāo)發(fā)送一個cancel事件
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);//該為cancel
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
//調(diào)用子View的dispatchTouchEvent,就算它沒有消費(fèi)這個cancel事件,我們也無能為力了。
}
//清除目標(biāo)
mMotionTarget = null;
//有目標(biāo),又?jǐn)r截,自身也享受不了了,因為一個事件應(yīng)該由一個View去完成
return true;//直接返回true,以完成這次事件,好讓系統(tǒng)開始派發(fā)下一次
}
if (isUpOrCancel) {//取消或者UP的話,把目標(biāo)賦值為空,以便下一次DOWN能重新找,此處就算不賦值,下一次DOWN也會先把它賦值為空
mMotionTarget = null;
}
//又不攔截,又有目標(biāo),那么就直接調(diào)用目標(biāo)的dispatchTouchEvent
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
//也就是說,如果是DOWN事件,攔截了,那么每次一次MOVE或者UP都不會再判斷是否攔截,直接調(diào)用super的dispatchTouchEvent
//如果DOWN沒攔截,就是有其他View處理了DOWN事件,那么接下來的MOVE或者UP事件攔截了,那么給目標(biāo)View發(fā)送一個cancel事件,告訴它touch被取消了,并且自身也不會處理,直接返回true
//這是為了不違背一個Touch事件只能由一個View處理的原則。
}
3.4 Move和Up事件
判斷事件是否被取消或者事件是否要攔截住,是的話,給Down事件找到的target發(fā)送一個取消事件。如果不取消,也不攔截,并且Down已經(jīng)找到了target,則直接交給target處理,不再遍歷子View尋找合適的View了。這種處理事件是正確的,我們用手機(jī)經(jīng)??梢泽w會到,當(dāng)我手指按在一個拖動條上之后,在拖動的時候手指就算移出了拖動條,依然會把事件分發(fā)給拖動條控制它的拖動。
4. onInterceptTouchEvent
ViewGroup的方法,事件攔截,return true表示攔截觸摸事件,事件就不往下傳遞
子View可以調(diào)用getParent().requestDisallowInterceptTouchEvent( true ) 請求父控件不攔截touch事件
5. View的onTouchEvent
從View的dispatchTouchEvent可以看出,事件最終的處理無非是交給TouchListener的onTouch方法或者是交由onTouchEvent處理,由于onTouch默認(rèn)是空實(shí)現(xiàn),由程序員來編寫邏輯,那么我們來看看onTouchEvent事件。View只能響應(yīng)click和longclick,不具備滑動等特性。
Down時,設(shè)置按壓狀態(tài),發(fā)送一個延遲500毫秒的長按事件。
Move時,判斷是否移出了View,移出后移除按壓狀態(tài),長按事件。
Up時,取消按壓,并判斷它是否可以通過觸摸獲取焦點(diǎn),是的話設(shè)置焦點(diǎn),判斷長按事件是否執(zhí)行了,如果還沒執(zhí)行,就刪除,并執(zhí)行點(diǎn)擊事件。
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//先判斷標(biāo)示位是否為disable,也就是無法處理事件。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}//如果是UP事件,并且狀態(tài)為按壓,取消按壓。
//系統(tǒng)源碼解釋:雖然是disable,但是還是可以消費(fèi)掉觸摸事件,只是不觸發(fā)任何click或者longclick事件。
//根據(jù)是否可點(diǎn)擊,可長按來決定是否消費(fèi)點(diǎn)擊事件。
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
//先檢查觸摸的代理對象是否存在,如果存在,就交由代理對象處理。
// 觸摸代理對象是可以進(jìn)行設(shè)置的,一般用于當(dāng)我們手指在某個View上,而讓另外一個View響應(yīng)事件,另外一個View就是該View的事件代理對象。
if (mTouchDelegate.onTouchEvent(event)) {//如果代理對象消費(fèi)了,則返回true消費(fèi)該事件
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//如果是可點(diǎn)擊或者長按的標(biāo)識位執(zhí)行下面的邏輯,這些標(biāo)志位可以設(shè)置,也可以設(shè)置了對應(yīng)的listener后自動添加
//因為作為一個View,它只能單純的接受處理點(diǎn)擊事件,像滑動之類的復(fù)雜事件普通View是不具備的。
switch (event.getAction()) {
case MotionEvent.ACTION_UP://處理Up事件
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包含臨時按壓狀態(tài)
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//如果本身處于被按壓狀態(tài)或者臨時按壓狀態(tài)
//臨時按壓狀態(tài)會在下面的Move事件中說明
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
//如果它可以獲取焦點(diǎn),并且可以通過觸摸來獲取焦點(diǎn),并且現(xiàn)在不是焦點(diǎn),則請求獲取焦點(diǎn),因為一個被按壓的View理論上應(yīng)該獲取焦點(diǎn)
focusTaken = requestFocus();
}
if (prepressed) {
//如果是臨時按壓,則設(shè)置為按壓狀態(tài),PFLAG_PREPRESSED是一個非常短暫的狀態(tài),用于在某些時候短時間內(nèi)表示Pressed狀態(tài),但不需要繪制
setPressed(true);//設(shè)置為按壓狀態(tài),是因為臨時按壓不會繪制,這個時候強(qiáng)制繪制一次,確保用戶能夠看見按壓狀態(tài)
}
if (!mHasPerformedLongPress) {
//是否執(zhí)行了長按事件,還沒有的話,這個時候可以移除長按的回調(diào)了,因為UP都已經(jīng)觸發(fā),說明從按下到UP的時間不足以觸發(fā)longPress
//至于longPress,會在Down事件中說明
removeLongPressCallback();
if (!focusTaken) {//如果是焦點(diǎn)狀態(tài),就不會觸摸click,這是為什么呢?因為焦點(diǎn)狀態(tài)一般是交給按鍵處理的,
//pressed狀態(tài)才是交給觸摸處理,如果它是焦點(diǎn),那么它的click事件應(yīng)該由按鍵來觸發(fā)
if (mPerformClick == null) { //封裝一個Runnable對象,這個對象中實(shí)際就調(diào)用了performClick();
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {//向消息隊列發(fā)生該runnabel,如果發(fā)送不成功,則直接執(zhí)行該方法。
performClick();//這個方法內(nèi)部會調(diào)用clickListner
}
//為什么不直接執(zhí)行呢?如果這個時候直接執(zhí)行,UP事件還沒執(zhí)行完,發(fā)送post,可以保障在這個代碼塊執(zhí)行完畢之后才執(zhí)行
}
}
if (mUnsetPressedState == null) {//仍舊是創(chuàng)建一個Runnabel對象,執(zhí)行setPressed(false)
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
//如果是臨時按壓狀態(tài),之前的Down和move都還未觸發(fā)按壓狀態(tài),只在up時設(shè)置了,這個狀態(tài)才剛剛繪制,為了保證用戶能看到,發(fā)生一個64秒的延遲消息,來取消按壓狀態(tài)。 postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
//這是一個64毫秒的短暫時間,這是為了讓這個按壓狀態(tài)持續(xù)一小段時間,以便手指離開時候,還能看見View的按壓狀態(tài)
} else if (!post(mUnsetPressedState)) {//如果不是臨時按壓,則直接發(fā)送,發(fā)送失敗,則直接執(zhí)行
mUnsetPressedState.run();
}
removeTapCallback();
//移除這個callBack,這個callBack內(nèi)部就是把臨時按壓狀態(tài)設(shè)置成按壓狀態(tài),因為這個已經(jīng)沒必要了,手指已經(jīng)up了
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
//按下事件把長按事件執(zhí)行的變量設(shè)置為false,代表還沒執(zhí)行長按,因為才按下,表示新的一個長按事件可以開始計算了
if (performButtonActionOnTouchDown(event)) {
//先把這個事件交由該方法,該方法內(nèi)部會判斷是否為上下文的菜單按鈕,或者是否為鼠標(biāo)右鍵,如果是就彈出上下文菜單。
//現(xiàn)在有些手機(jī)的上下文菜單按鈕也是在屏幕觸屏上的
break;
}
//這個方法會一直往上找父View,判斷自身是否在一個可以滾動的容器中
boolean isInScrollingContainer = isInScrollingContainer();
//如果是在一個滾動的容器中,那么按壓事件將會被推遲一段時間,如果這段時間內(nèi),發(fā)生了Move,那么按壓狀態(tài)講不會被顯示,直接滾動父視圖
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED; //先添加臨時的按壓狀態(tài),該狀態(tài)表示按壓,但不會繪制
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
//創(chuàng)建一個runnable對象,這個runnable內(nèi)部會取消臨時按壓狀態(tài),設(shè)置為按壓狀態(tài),并啟動長按的延遲事件
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
//向消息機(jī)制發(fā)生一個64毫秒的延遲時間,該事件會取消臨時按壓狀態(tài),設(shè)置為直接按壓,并啟動長按時間的計時
} else {
//如果不在一個滾動的容器中,則直接設(shè)置按壓狀態(tài),并啟動長按計時
setPressed(true);
checkForLongClick(0);
//長按事件就是向消息機(jī)制發(fā)送一個runnable對象,封裝的就是我們在lisner中的代碼,延遲500毫秒執(zhí)行,也就是說長按事件在我們按下的時候發(fā)送,在up的時候檢查一下執(zhí)行了嗎?如果沒執(zhí)行,就取消,并執(zhí)行click
}
break;
case MotionEvent.ACTION_CANCEL: //如果是取消事件,那就好辦了,把我們之前發(fā)送的幾個延遲runnable對象給取消掉
setPressed(false); //設(shè)置為非按壓狀態(tài)
removeTapCallback(); //取消mPendingCheckForTap,也就是不用再把臨時按壓設(shè)置為按壓了
removeLongPressCallback(); //取消長按事件的延遲回調(diào)
break;
case MotionEvent.ACTION_MOVE: //move事件
final int x = (int) event.getX(); //取觸摸點(diǎn)坐標(biāo)
final int y = (int) event.getY();
// 用于判斷是否在View中,為什么還要判斷呢?
//這是因為父View是在Down事件中判斷是否在該View中的,如果在,以后的Move和up都會傳遞過來,不再進(jìn)行范圍判斷
if (!pointInView(x, y, mTouchSlop)) {
//mTouchSlop是一個常量,數(shù)值為8,也就是說,就算你的落點(diǎn)超出了View的8像素位置,也算在View中。
//是因為人的手指觸摸點(diǎn)比較大,有可能你感覺點(diǎn)在某個控件的邊緣,但是實(shí)際落點(diǎn)已經(jīng)超出這個View,所以這里給了8像素的范圍
removeTapCallback();//如果在范圍外,就移除這些runnable回調(diào)
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//如果是按壓狀態(tài),就取消長按,設(shè)置為非按壓狀態(tài),為什么這個時候取消呢,因為在Down的時候,我們可以知道,只有是按壓狀態(tài),才會設(shè)置長按
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true; //至此,可以返回true,消費(fèi)該事件
}
return false; //如果不可點(diǎn)擊,也不可長按,則返回false,因為View只具備消費(fèi)點(diǎn)擊事件
}
從上面的代碼我們總結(jié)一下View對觸摸事件的處理:
1、是否為diabale,如果是,直接根據(jù)是否設(shè)置了click和longclick來返回。
2、是否設(shè)置了觸摸代理對象,如果有,把事件傳遞給觸摸代理對象,交由其處理,如果消費(fèi)了,直接返回
3、是否為click或者longclick的,如果是,返回true,不是返回false。
而View對click和longclick的處理如下:
Down:
- 判斷是否可以觸摸上下文菜單。
- 是否在可以滑動的容器中,如果是先設(shè)置臨時按壓,再發(fā)送一個延遲消息把臨時按壓改為按壓,并發(fā)送一個延遲500毫秒的事件去執(zhí)行長按代碼
- 如果不在滾動容器中,直接設(shè)置按壓狀態(tài),并發(fā)送一個延遲500毫秒的事件去執(zhí)行長按代碼。
Move:
- 取觸摸點(diǎn)坐標(biāo)判斷是否在View中(額外增加了8像素的范圍)
- 如果在,不用做任何事。
- 如果不在,取消臨時按壓到按壓回調(diào),取消長按延遲回調(diào),設(shè)置為非按壓狀態(tài)
Up
- 判斷是否為按壓或者臨時按壓狀態(tài)
- 如果不是,不做任何處理
- 如果是先判斷其是否可以獲取焦點(diǎn),然后請求焦點(diǎn)。
- 如果是臨時按壓狀態(tài),設(shè)置臨時按壓狀態(tài)為按壓狀態(tài)。保證界面被繪制成按壓狀態(tài),讓用戶可以看見。
- 如果長按回調(diào)還未觸發(fā),取消長按回調(diào),如果不是焦點(diǎn)狀態(tài),觸發(fā)click事件。
- 如果是臨時按壓狀態(tài),發(fā)送一個延遲取消按壓狀態(tài)的,保證按壓狀態(tài)持續(xù)一段時間,讓用戶可見。
- 如果不是臨時按壓狀態(tài),直接發(fā)送消息取消按壓狀態(tài)。發(fā)送失敗,直接取消按壓狀態(tài)。
- 取消把臨時按壓設(shè)置按壓的回調(diào)。
從中我們知道View的onTouchEvent主要處理了click和longclick事件,當(dāng)按下時,向消息機(jī)制發(fā)送一個延遲500毫秒的長按回調(diào)事件,當(dāng)移動時候判斷是否移出了View的范圍,超出則取消事件。當(dāng)離開時,判斷長按事件是否觸發(fā)了,如果沒觸發(fā)且不是焦點(diǎn),就觸發(fā)click事件。
在這里最繞的就是臨時按壓和按壓狀態(tài),臨時按壓是為了處理滑動容器的,讓處于滑動容器中,按下時,我們先設(shè)置的是臨時按壓,持續(xù)64毫秒,是為了判斷接下來的時間內(nèi)是否發(fā)生了move事件,如果發(fā)生了,將不會再出發(fā)按壓狀態(tài),這樣不會讓用戶看到listView滾動時,item還處于按壓狀態(tài)。在離開時,我們再次判斷是否處于臨時按壓,如果是在64毫秒內(nèi)觸發(fā)了down和up,說明按壓狀態(tài)還沒來得急繪制,則強(qiáng)制設(shè)置為按壓狀態(tài),保證用戶能看到,并在取消回調(diào)的方法上加上64毫秒的延遲
6. onTouch與onClick
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("iv_image---onTouch--" + event.getAction());
return false;
}
});
點(diǎn)擊ImageView的時候只會打印一次,因為onTouch()返回false,只傳遞down事件,不會傳遞up事件
System.out: iv_image---onTouch--0
// ImageView天生不能被點(diǎn)擊,沒有點(diǎn)擊事件
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("iv_image---onTouch--" + event.getAction());
return true; // 把返回值改為true
}
});
把onTouch()方法返回值改為true,點(diǎn)擊ImageView會打印兩次(down and up)
System.out: iv_image---onTouch--0
System.out: iv_image---onTouch--1
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("iv_image---onTouch--" + event.getAction());
return true;
}
});
//添加click事件
iv_image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("iv_image---onClick");
}
});
還是打印兩次,onTouch()返回true,click事件并不會得到執(zhí)行
ImageView iv_image = (ImageView) findViewById(R.id.iv_image);
iv_image.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("iv_image---onTouch--" + event.getAction());
return false;
}
});
iv_image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("iv_image---onClick");
}
});
打印三次,兩次touch事件(down and up)和一次click事件
Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("button---onTouch--" + event.getAction());
return false;
}
});
點(diǎn)擊Button會打印兩次
Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("button---onTouch--" + event.getAction());
return true;
}
});
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("button---onClick");
}
});
打印兩次,因為onTouch()返回true,不會執(zhí)行onTouchEvent(),而click事件是在onTouchEvent()中執(zhí)行,所以也不會執(zhí)行click事件
Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("button---onTouch--" + event.getAction());
return false;
}
});
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("button---onClick");
}
});
打印三次
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
}
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
a 判斷mOnTouchListener是否為null
b 判斷當(dāng)前的控件是否可用
c 判斷view的onTouch。
d 如果以上一個返回為false。那么就會調(diào)用onTouchEvent
首先判斷mOnTouchListener不為null,并且view是enable的狀態(tài),然后 mOnTouchListener.onTouch(this, event)返回true,這三個條件如果都滿足,直接return true ; 也就是下面的onTouchEvent(event)不會被執(zhí)行了。如果我們設(shè)置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不會被執(zhí)行了
onTouch是優(yōu)先于onClick執(zhí)行, onClick的調(diào)用在onTouchEvent(event)方法中
view的事件分發(fā)
- 返回true,說明可以響應(yīng)down事件和up事件
- 返回false,只會響應(yīng)down事件。不會響應(yīng)up事件。在down事件如果能消費(fèi)(處理)當(dāng)前事件。那么在up的時候也會把事件傳遞給當(dāng)前的view,在down事件處理不了當(dāng)前事件。那么在up的時候。也不會把事件傳遞給當(dāng)前的view
7. ScrollView的onTouchEvent
普通的ViewGroup并沒有對onTouchEvent事件做處理,只有可以滾動的才有,我們可以分析一下ScrollView
Down時,判斷落點(diǎn)是否在子View中,不再就不處理,因為ScrollView只有一個子View。
Move時,通過對比本次手指的位置和上一次的位置的距離,計算出Y方向的差值,然后用scorllBy進(jìn)行滾動視圖
Up時,通過速度進(jìn)行fling,這里利用了兩個幫助類,一個是計算速度的幫助類VelocityTracker,一個是滾動的幫助類Scroller
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
//如果是down事件,并且觸摸到邊緣,就不處理EdgeFlags代表是否為邊緣,其值是1/2/4/8。代表上下左右
return false;
}
if (mVelocityTracker == null) {
//這是一個追蹤觸摸事件,并計算速度的幫助類,實(shí)現(xiàn)原理就是用三個數(shù)組分別記錄每次觸摸的x/y和時間
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {//與上ff,去掉高位有關(guān)多點(diǎn)的信息
case MotionEvent.ACTION_DOWN: {//如果是down
final float y = ev.getY();//獲取y坐標(biāo)
if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//判斷是否開始拖動
//原理就是判斷落點(diǎn)是否在child中,ScrollView只能由一個child,如果在,返回true,反之false
//也就是說落點(diǎn)在child中,就是準(zhǔn)備開始拖動,不在,就直接返回,這可能是因為設(shè)置了padding之類的緣故造成的
return false;
}
if (!mScroller.isFinished()) {//判斷滾動是否完成
mScroller.abortAnimation();//如果沒完成,停止?jié)L動
//對應(yīng)上一次用戶手指離開時候處理fling狀態(tài),這次按下手指,直接停止?jié)L動
}
//記錄y坐標(biāo),以便下次事件來對比
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);//記住多點(diǎn)的id,下次取值時只取該點(diǎn)的
break;
}
case MotionEvent.ACTION_MOVE:
if (mIsBeingDragged) {//可以看出,如果down的時候落點(diǎn)在child外,則以后就算滑進(jìn)了child也不處理
//根據(jù)上次記錄的多點(diǎn)id,找到對應(yīng)的點(diǎn),取y值
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final float y = ev.getY(activePointerIndex);
final int deltaY = (int) (mLastMotionY - y);//計算位移
mLastMotionY = y;//重新記錄y值
scrollBy(0, deltaY);//滾動指定的距離,這也說明了ScrollView只具備縱向滑動
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {//如果是離開事件
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//計算最后1秒鐘內(nèi)的速度,并給定一個最大速度進(jìn)行限制
//這個最大速度是根據(jù)屏幕密度不同而不同的,所以大家也沒事別使勁滑動屏幕,因為有這個最大速度限制
//獲取y方向的速度
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
//如果有子View,并且計算出來的y的速度比最小速度要大,執(zhí)行fling狀態(tài)
//手指滑動的方向和屏幕移動的方向是相反的,所以這里加-
fling(-initialVelocity);
}
mActivePointerId = INVALID_POINTER;//給mActivePointerId重新賦值為-1,防止下次事件找到了錯誤的點(diǎn)
mIsBeingDragged = false;//恢復(fù)默認(rèn)值
if (mVelocityTracker != null) {//清空速度計算幫助類
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {//判斷條件,只有這2個條件成立,才會發(fā)生滾動事件,下面的值才會被改變,才需要恢復(fù)默認(rèn)
mActivePointerId = INVALID_POINTER;
mIsBeingDragged = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
break;
case MotionEvent.ACTION_POINTER_UP://多點(diǎn)觸摸時,不是最后一個點(diǎn)離開
onSecondaryPointerUp(ev);
break;
}
return true;
}
//用于應(yīng)對先按下1點(diǎn),然后按下2點(diǎn),1點(diǎn)離開后,2點(diǎn)仍能繼續(xù)滑動的邏輯
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先對高位進(jìn)行與操作,然后右移8位,獲取其高位代表index的值
final int pointerId = ev.getPointerId(pointerIndex);//取出該點(diǎn)的id
if (pointerId == mActivePointerId) {//如果這個id對應(yīng)的就是第一個按下的點(diǎn)
//理論上pointerIndex應(yīng)該是0,所以用第二個按下的點(diǎn),即1index的點(diǎn)代替
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = ev.getY(newPointerIndex);//取出新點(diǎn)的y坐標(biāo)
mActivePointerId = ev.getPointerId(newPointerIndex);//記錄新點(diǎn)的id
if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是說最后的速度只計算該點(diǎn)產(chǎn)生的
mVelocityTracker.clear();
}
}
}
通過以上分析,我們得出以下知識:
- 在down事件的時候先判斷觸摸是否處于邊緣,如果是,則不處理
- 在down事件中判斷落點(diǎn)是否在子View中,如果不在,不處理
- 在down事件中判斷是否仍在滑動,如果是,先停止
- 記錄第一個按下點(diǎn)的索引值
- 每次事件都記錄住當(dāng)前的y值
- 在move事件中通過記錄的索引值找到對應(yīng)的點(diǎn),獲取y坐標(biāo)
- 與上一次y坐標(biāo)進(jìn)行比對,scrollBy兩次的差值
- 在up事件的時候計算最后一秒鐘的速度,并且有最大速度進(jìn)行限制,當(dāng)計算的速度大于系統(tǒng)默認(rèn)的最小速度時,只想fling
- up和cancel事件還原變量為默認(rèn)值
- 如果為多點(diǎn)離開,進(jìn)行多點(diǎn)離開的處理
- 該處理方式時:如果離開的是第一個按下的點(diǎn),那么由第二個按下的點(diǎn)代替其進(jìn)行y值偏移計算的基點(diǎn),并清空速度計算的幫助類,重新記錄MotionEvnet
8. Layout和Scroll的區(qū)別
- Layout中設(shè)置的是自身在父View中的顯示區(qū)域
- Scroll是調(diào)整自己的顯示區(qū)域
- 當(dāng)父View滾動或者layout變化后,自身在屏幕上的位置會發(fā)生變化。
當(dāng)自身Scroll滾動后,在屏幕上的顯示位置是不變的,變的只是自身的顯示內(nèi)容。 - Scroll滾動不會影響Layout,只是在draw的時候影響畫布偏移和觸摸時的坐標(biāo)計算。