【Android開發(fā)藝術(shù)探索】View的事件體系

個(gè)人博客:
http://www.milovetingting.cn

1、View基礎(chǔ)知識(shí)

1.1、什么是View

View是Android中所有控件的基類。View是一種界面層的控件的一種抽象,代表了一個(gè)控件。除了View,還有ViewGroup,內(nèi)部包含了許多個(gè)控件,即一組View。

1.2、View的位置參數(shù)

View的位置主要由它的四個(gè)頂點(diǎn)來決定,分別對(duì)應(yīng)于View的四個(gè)屬性:top、left、right、bottom,其中top是左上角縱坐標(biāo),left是左上角橫坐標(biāo),right是右下角橫坐標(biāo),bottom是右下角縱坐標(biāo)。這些坐標(biāo)都是相對(duì)于View的父容器來說的。是一種相對(duì)坐標(biāo)。

03.View的坐標(biāo)位置和父容器的關(guān)系.png

width = right - left

height = bottom - top

獲取這四個(gè)參數(shù)的方法:

left=getLeft();

right=getRight();

top=getTop();

bottom=getBottom();

Android3.0開始,View增加了額外的幾個(gè)參數(shù):x、y、translationX和translationY,其中x和y是View左上角的坐標(biāo),而translationX和translationY是View左上角相對(duì)于父容器的偏移量。這幾個(gè)參數(shù)也是相對(duì)于父容器的坐標(biāo),并且translationX和translationY的默認(rèn)值為0.

x=left+translationX

y=top+translationY

View在平移的過程中,top和left表示的是原始左上角的位置信息,其值并不會(huì)發(fā)生改變,此時(shí)發(fā)生改變的是x、y、transaltionX和translationY這四個(gè)參數(shù)。

03.ScrollX和ScrollY.png

1.3、MotionEvent和ToushSlop

MotionEvent

在手指接觸屏幕后產(chǎn)生的一系列事件中,典型的事件類型有如下幾種:

ACTION_DOWN--手指剛接觸屏幕;

ACTION_MOVE--手指在屏幕上移動(dòng);

ACTION_UP--手指從屏幕上松開的瞬間。

通過MotionEvent可以獲取點(diǎn)擊事件發(fā)生的x和y坐標(biāo):getX/getY和getRawX/getRawY。getX/getY返回的是相對(duì)于當(dāng)前View左上角的x和y坐標(biāo),而getRawX/getRawY返回的是相對(duì)于手機(jī)屏幕左上角的x和y坐標(biāo)。

TouchSlop

TouchSlop是系統(tǒng)所能識(shí)別出來的被認(rèn)為是滑動(dòng)的最小距離。這是一個(gè)常量,和設(shè)備有關(guān),不同設(shè)備上這個(gè)值可能是不同的,可通過如下方式獲取這個(gè)常量:ViewConfiguration.get(getContext().getScaledTouchSlop())。這個(gè)常量定義在frameworks/base/core/res/res/values/config.xml文件中,"config_viewConfigurationTouchSlop"對(duì)應(yīng)的就是這個(gè)常量的定義。

1.4、VelocityTracker、GestureDetector和Scroller

VelocityTracker

速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向的速度。使用方法如下:

首先,在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

接著,當(dāng)我們先知道當(dāng)前的滑動(dòng)速度 時(shí),可用如下方式來獲得當(dāng)前的速度:

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

最后,當(dāng)不需要使用它的時(shí)候,需要調(diào)用clear方法來重置并回收內(nèi)存:

velocityTracker.clear();
velocityTracker.recycle();

速度=(終點(diǎn)位置-起點(diǎn)位置)/時(shí)間段

速度可以為負(fù)數(shù),當(dāng)手指從右往左滑時(shí),水平方向速度即為負(fù)值。當(dāng)手指從左往右滑時(shí),水平方向速度即為正值。

GestureDetector

手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。使用方法如下:

首先,需要?jiǎng)?chuàng)建一個(gè)GestureDetector對(duì)象并實(shí)現(xiàn)OnGestureListener接口,根據(jù)需要還可以實(shí)現(xiàn)OnDoubleTapListener從而能夠監(jiān)聽雙擊行為:

GestureDetector mGestureDetector = new GestureDetector(this);
//解決長(zhǎng)按屏幕后無法手動(dòng)的問題
mGestureDetector.setIsLongpressEnabled(false);

接著,接管目標(biāo)View的onTouchEvent方法,在待監(jiān)聽的onTouchEvent方法中添加如下實(shí)現(xiàn):

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

如果只是監(jiān)聽滑動(dòng)相關(guān),可以自己在onTouchEvent中實(shí)現(xiàn),如果要監(jiān)聽雙擊行為,可以使用GestureDetector。

Scroller
使用View的scrollTo/scrollBy方法來進(jìn)行滑動(dòng)時(shí),其過程是瞬間完成的,這個(gè)沒有過度效果的滑動(dòng),用戶體驗(yàn)不好。這個(gè)時(shí)候可以使用Scroller來實(shí)現(xiàn)有過渡效果的滑動(dòng),其過程不是瞬間完成,而是在一定時(shí)間間隔內(nèi)完成的。Scroller本身無法讓View彈性滑動(dòng),它需要和View的computeScroll方法配合使用才能共同完成這個(gè)功能。使用方法:

Scroller mScroller = new Scroller(mContext);

//緩慢滾到到指定位置
private void smoothScrollTo(int destX,int destY){
    int scrollX = getScrollX();
    int delta = destX-scrollX;
    //1000ms內(nèi)滑向destX,效果就是慢慢滑動(dòng)
    mScroller.startScroll(scrollX,0,delta,0,1000);
    invalidate();
}

@Override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

2、View的滑動(dòng)

通過三種方式可以實(shí)現(xiàn)View的滑動(dòng):第一種是通過View本身提供的scrollTo/scrollBy方法來實(shí)現(xiàn)滑動(dòng);第二種是通過動(dòng)畫給View施加平移效果來實(shí)現(xiàn)滑動(dòng);第三種是改變View的LayoutParams使用View重新布局從而實(shí)現(xiàn)滑動(dòng)。

2.1、使用scrollTo/scrollBy

scrollBy實(shí)際也是調(diào)用了scrollTo方法,它實(shí)現(xiàn)了基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo實(shí)現(xiàn)了基于所傳遞參數(shù)的絕對(duì)滑動(dòng)。在滑動(dòng)過程中,mScrollX的值總是等于View的左邊緣和View內(nèi)容左邊緣在水平方向的距離,而mScrollY的值總等于View的上邊緣和View內(nèi)容上邊緣在豎直方向的距離。scrollTo和scrollBy只能改變View內(nèi)容的位置而不能改變View在布局中的位置。mScrollX和mScrollY的單位為像素,并且當(dāng)View左邊緣在View內(nèi)容左邊緣的右邊時(shí),mScrollX為正值,反之為負(fù)值;當(dāng)View上邊緣在View內(nèi)容上邊緣的下邊時(shí),mScrollY為正值,反之為負(fù)值。換句話說,如果從左向右滑動(dòng),那么mScrollX為負(fù)值,反之為正值;如果從上往下滑動(dòng),那么mScrollY為負(fù)值,反之為正值。

2.2、使用動(dòng)畫

使用動(dòng)畫,主要就是操作View的translationX和translationY屬性,既可以采用View動(dòng)畫,也可以采用屬性動(dòng)畫。

View動(dòng)畫是對(duì)View的影像做操作,它并不能真正改變View的位置參數(shù),包括寬/高,并且如果希望動(dòng)畫后的狀態(tài)得以保留還必須將fillAfter屬性設(shè)置為true,否則動(dòng)畫完成后其動(dòng)畫結(jié)果會(huì)消失,View會(huì)瞬間恢復(fù)到動(dòng)畫前的狀態(tài)。使用屬性動(dòng)畫不會(huì)存在上述問題。

2.3、改變布局參數(shù)

改變布局參數(shù),即改變LayoutParams。簡(jiǎn)單示例如下:

MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);

各種滑動(dòng)方式的對(duì)比

scrollTo/scrollBy:View提供的原生方法,可以比較方便地實(shí)現(xiàn)滑動(dòng)效果并且不影響內(nèi)部元素的單擊事件。缺點(diǎn):只能滑動(dòng)View的內(nèi)容,并不能滑動(dòng)View本身。

動(dòng)畫:如果是Android3.0以上并采用屬性動(dòng)畫,那么這種方式?jīng)]有明顯的缺點(diǎn);如果是使用View動(dòng)畫或者在Android3.0以下使用屬性動(dòng)畫,均不能改變View本身的屬性。如果動(dòng)畫元素不需要響應(yīng)用戶的交互,那么可以用動(dòng)畫來做滑動(dòng),否則不太適合。一些復(fù)雜的效果必須通過動(dòng)畫才能實(shí)現(xiàn)。

改變布局:使用起來麻煩些,沒有明顯的缺點(diǎn)。適用于一些具有交互性的View。

3、彈性滑動(dòng)

3.1、使用Scroller

Scroller本身并不能實(shí)現(xiàn)View的滑動(dòng),它需要配合View的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷地讓View重繪,而每一次重繪距離滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過這個(gè)時(shí)間間隔Scroller就可以得出View當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過scrollTo方法來完成View的滑動(dòng)。View的每一次重繪都會(huì)導(dǎo)致View進(jìn)行小幅度的滑動(dòng),而多次小幅度滑動(dòng)就組成了彈性滑動(dòng),這就是Scroller的工作機(jī)制。

3.2、通過動(dòng)畫

3.3、使用延時(shí)策略

核心思想是通過發(fā)送一系統(tǒng)延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果。具體來說,可以使用Handler或View的postDelayed方式,也可以使用純種的sleep方法。

4、View的事件分發(fā)機(jī)制

4.1、點(diǎn)擊事件的傳遞規(guī)則

所謂點(diǎn)擊事件的事件分發(fā),其實(shí)就是對(duì)MotionEvent事件的分發(fā)過程,即當(dāng)一個(gè)Motion產(chǎn)生了以后,系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View,而這個(gè)傳遞的過程就是分發(fā)的過程。點(diǎn)擊事件的分發(fā)過程由三個(gè)很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

public boolean dispatchTouchEvent(MotionEvent ev)

用來進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法內(nèi)部調(diào)用,用來判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。

public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中調(diào)用,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無法再次接收到事件。

上面三個(gè)方法的關(guān)系可以用以下偽代碼表示:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }
    else{
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

通過上述偽代碼,可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對(duì)于一個(gè)根ViewGroup來說,點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent就會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。

當(dāng)一個(gè)View需要處理事件時(shí),如果它設(shè)置了OnTouchListener,那么OnTouchListener中的onTouch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看onTouch的返回值,如果返回false,則當(dāng)前View的onTouchEvent方法會(huì)被調(diào)用;如果返回true,那么onTouchEvent方法將不會(huì)被調(diào)用。由此可見,給View設(shè)置的OnTouchListener,其優(yōu)先級(jí)比onTouchEvent要高。在onTouchEvent方法中,如果當(dāng)前設(shè)置的有OnClickListener,那么它的onClick方法會(huì)被調(diào)用??梢钥闯?,平時(shí)我們常用的OnClickListener,其優(yōu)先級(jí)最低,即處于事件傳遞的尾端。

當(dāng)一個(gè)點(diǎn)擊事件產(chǎn)生后,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最后Window再傳遞給頂級(jí)View。頂級(jí)View接收到事件后,就會(huì)按照事件分發(fā)機(jī)制去分發(fā)事件。如果一個(gè)View的onTouchEvent返回false,那么父容器的onTouchEvent將會(huì)被調(diào)用,依此類推。如果所有元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給Activity處理,即Activity的onTouchEvent方法會(huì)被調(diào)用。

關(guān)于事件傳遞的機(jī)制,有以下結(jié)論:

1、同一個(gè)事件序列是指從手指接觸屏幕的那一刻起,到手指屏幕的那一刻結(jié)束,在這個(gè)過程中所產(chǎn)生的一系統(tǒng)事件,這個(gè)事件序列以down事件開始,中間含有數(shù)量不定的move事件,最終以u(píng)p事件結(jié)束。

2、正常情況下,一個(gè)事件序列只能被一個(gè)View攔截且消耗。因?yàn)橐坏┮粋€(gè)元素?cái)r截了某些事件,那么同一個(gè)事件序列的所有事件都會(huì)直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過onTouchEvent強(qiáng)行傳遞給其它View處理。

3、某個(gè)View一旦決定攔截,那么這一個(gè)事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),并且它的onInterceptTouchEvent不會(huì)再被調(diào)用。

4、某個(gè)View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不會(huì)再交給它來處理,并且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會(huì)被調(diào)用。

5、如果View不消耗除ACTION_DOWN以外的其它事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent并不會(huì)被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。

6、ViewGroup默認(rèn)不攔截任何事件。

7、View沒有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,它的onTouchEvent方法會(huì)被調(diào)用。

8、View的onTouchEvent默認(rèn)會(huì)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable屬性要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable屬性默認(rèn)為false。

9、View的enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕一個(gè)View是disable狀態(tài)的,只要它的clickable或者longClickable有一個(gè)true,那么它的onTouchEvent返回true。

10、onClick會(huì)發(fā)生的前提是當(dāng)前View是可點(diǎn)擊的,并且收到了down和up的事件。

11、事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。

4.2、事件分發(fā)的源碼解析

1、Activity對(duì)點(diǎn)擊事件的分發(fā)過程

點(diǎn)擊事件用MotionEvent來表示,當(dāng)一個(gè)點(diǎn)擊操作發(fā)生時(shí),事件最先傳遞給當(dāng)前Activity,由Activity的dispatchTouchEvent來進(jìn)行事件派發(fā),具體工作是由Activity內(nèi)部的Window來完成的。Window會(huì)將事件傳遞給decor View,decor view一般就是當(dāng)前界面的底層容器,即setContentView所設(shè)置的View的父容器,通過Activity.getWindow().getDecorView()可以獲得。Activity的dispatchTouchEvent方法如下:

public boolean dispatchTouchEvent(MotionEvent event){
    if(event.getAction()==MotionEvent.ACTION_DOWN){
        onUserInteraction();
    }
    if(getWindow().superDispatchTouchEvent(event)){
        return true;
    }
    return onTouchEvent(event);
}

首先,事件開始交給Activity所附屬的Window進(jìn)行分發(fā),如果返回true,整個(gè)事件循環(huán)就結(jié)束了,返回false意味著事件沒人處理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就會(huì)被調(diào)用。

接下來看Window是如何將事件傳遞給ViewGroup的。通過源碼可以知道,Window是個(gè)抽象類,而Window的superDispatchTouchEvent方法也是個(gè)抽象方法,因此必須找到Window的實(shí)現(xiàn)類才行。window的唯一實(shí)現(xiàn)類是PhoneWindow。phoneWindow的superDispatchTouchEvent方法如下:

public boolean superDispatchTouchEvent(MotionEvent event){
    return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow將事件直接傳遞給了DecorView。通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式就可以獲取Activity所設(shè)置的View,這個(gè)mDecor顯示就是getWindow().getDecorView()返回的View,而我們通過setContentView設(shè)置的View是它的一個(gè)子View。

2、頂級(jí)View對(duì)點(diǎn)擊事件的分發(fā)過程

點(diǎn)擊事件達(dá)到頂級(jí)View(一般是一個(gè)ViewGroup)以后,會(huì)調(diào)用ViewGroup的dispatchTouchEvent方法,然后的邏輯是這樣的:如果頂級(jí)ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由ViewGroup處理,這時(shí)如果ViewGroup的mOnTouchListener被設(shè)置,則onTouch會(huì)被調(diào)用,否則onTouchEvent會(huì)被調(diào)用,也就是說,如果都提供的話,onTouch會(huì)屏蔽掉onTouchEvent。在onTouchEvent中,如果設(shè)置了mOnClickListener,則onClick會(huì)被調(diào)用。如果頂級(jí)ViewGroup不攔截事件,則事件會(huì)傳遞給它所在的點(diǎn)擊事件鏈上的子View.這時(shí)子View的dispatchTouchEvent會(huì)被調(diào)用 。到此為止,事件已經(jīng)從頂級(jí)View傳遞給了下一層View。接下來的傳遞過程和頂級(jí)View是一致的,如此循環(huán),完成整個(gè)事件的分發(fā)。

ViewGroup對(duì)點(diǎn)擊事件的分發(fā)過程:

主要實(shí)現(xiàn)在ViewGroup的dispatchTouchEvent方法中。描述了當(dāng)前View是否攔截點(diǎn)擊事件的邏輯。

final boolean intercepted;
if(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null){
    final boolean disallowIntercept = (mGroupFlags&FLAG_DISALLOW_INTERCEPT)!=0;
    if(!disallowIntercept){
        intercepted=onInterceptTouchEvent(event);
        event.setAction(action);
    }
    else{
        intercepted = false;
    }
}
else{
    intercepted = true;
}

ViewGroup在如下兩種情況下會(huì)判斷是否攔截當(dāng)前事件:事件類型為ACTION_DOWN或者mFirstTouchTarget!=null。當(dāng)事件由ViewGroup的子元素成功處理時(shí),mFirstTouchTarget會(huì)被賦值并指向子元素,即:當(dāng)ViewGroup不攔截事件并將事件交由子元素處理時(shí),mFirstTouchTarget!=null。反過來,一旦事件由當(dāng)前ViewGroup攔截時(shí),mFirstTouchTarget!=null就不成立。那么當(dāng)ACTION_MOVE和ACTION_UP事件到來時(shí),由于(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null)這個(gè)條件為false,將導(dǎo)致ViewGroupr onInterceptTouchEvent不會(huì)再被調(diào)用,并且同一序列中的其他事件都會(huì)默認(rèn)交給它處理。

還有種特殊情況,那就是FLAG_DISALLOW_INTERCEPT標(biāo)記位,這個(gè)標(biāo)記位是通過requestDisallowInterceptTouchEvent方法來設(shè)置的,一般用于子View中。這個(gè)標(biāo)記一旦設(shè)置后,ViewGroup將無法攔截除了ACTION_DOWN以外的其它點(diǎn)擊事件??偨Y(jié)起來兩點(diǎn):第一點(diǎn),onInterceptTouchEvent方法并不是每次事件都被調(diào)用,如果想提前處理所有的點(diǎn)擊事件,要選擇dispatchTouchEvent方法,只有這個(gè)方法確保每次會(huì)調(diào)用,當(dāng)然前提是事件能夠傳遞到當(dāng)前的ViewGroup;另外一點(diǎn),F(xiàn)LAG_DISALLOW_INTERCEPT標(biāo)記位的作用,可以在滑動(dòng)沖突時(shí),可以用這種方法去解決問題。

首先遍歷ViewGroup的所有子元素,然后判斷子元素是否能夠接收點(diǎn)擊事件。是否能夠接收點(diǎn)擊事件,主要由兩點(diǎn)來衡量:子元素是否在播動(dòng)畫和點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)。如果子元素滿足這兩個(gè)條件,那么事件就會(huì)傳遞給它來處理。

View的事件處理:

首先判斷有沒有設(shè)置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不會(huì)被調(diào)用,可見OnTouchListener的優(yōu)先級(jí)高于onTouchEvent,這樣的好處是方便在外界處理點(diǎn)擊事件。

接下來,看onTouchEvent的實(shí)現(xiàn)。當(dāng)View處于不可用狀態(tài)下時(shí),照樣會(huì)消耗點(diǎn)擊事件。如果View設(shè)置有代理,還會(huì)執(zhí)行TouchDelegate的onTouchEvent方法。

通過setClickable和setLongClickable會(huì)分別改變View的CLICKABLE和LONG_CLICKABLE屬性。setOnClickListener會(huì)自動(dòng)將View的CLICKABLE設(shè)為true,setOnLongClickListener會(huì)自動(dòng)將View的LONG_CLICKABLE設(shè)為true。

5、View的滑動(dòng)沖突

5.1、常見的滑動(dòng)沖突場(chǎng)景

常見滑動(dòng)沖突場(chǎng)景可以簡(jiǎn)單分為如下三種:

  • 場(chǎng)景1--外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致

  • 場(chǎng)景2--外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致

  • 場(chǎng)景3--上面兩種情況的嵌套

03.滑動(dòng)沖突的場(chǎng)景.png

5.2、滑動(dòng)沖突的處理規(guī)則

對(duì)于場(chǎng)景1,根據(jù)滑動(dòng)是水平滑動(dòng)還是豎直滑動(dòng)來判斷到底由誰來攔截事件。

對(duì)于場(chǎng)景2,根據(jù)業(yè)務(wù)規(guī)則來決定由誰攔截事件。

對(duì)于場(chǎng)景3,根據(jù)業(yè)務(wù)規(guī)則來決定由誰攔截事件。

5.3、滑動(dòng)沖突的解決方式

1、外部攔截法

點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動(dòng)沖突的問題,這種方法比較符合點(diǎn)擊事件的分發(fā)機(jī)制。外部攔截法需要重寫父容器的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()){
        case MotionEvent.ACTION_DOWN:{
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE:{
            if(父容器需要當(dāng)前的點(diǎn)擊事件){
                intercepted = true;
            }
            else{
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP:{
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

2、內(nèi)部攔截法

內(nèi)部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件,就直接消耗掉,否則就交由父容器進(jìn)行處理,這種方法和Android中的事件分發(fā)機(jī)制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,作用起來較外部攔截法稍顯復(fù)雜。我們需要重寫子元素的dispathTouchEvent方法,偽代碼如下:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean intercepted = false;
    int x = (int)event.getX();
    int y = (int)event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:{
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE:{
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if(父容器需要當(dāng)前的點(diǎn)擊事件){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP:{
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

除了子元素需要做處理以外,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其它事件,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截所需的事件。父元素修改如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    int action = event.getAction();
    if(action==MotionEvent.ACTION_DOWN){
        return false;
    }
    else{
        return true;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容