Android藝術(shù)探索學(xué)習(xí)筆記:第3章View的事件體系

一.View的基礎(chǔ)知識

1.View的位置參數(shù)

View的位置主要由它的四個(gè)定點(diǎn)來決定,分別對應(yīng)View的四個(gè)屬性:top、left、right、bottom,   
這下坐標(biāo)都是相對父容器而言的。從3.0開始View增加了x、y、translationX、translationY;  
x和y是View左上角的坐標(biāo),translationX和translationY是View左上方相對父容器的偏移量。
x = left + translationX;  
y = top + translationY;
View平移的過程中,top和left表示的是原始左上角的位置信息;其值并不會改變,  
此時(shí)發(fā)生改變的是x、y、translationX和translationY。

2.MotionEvent

點(diǎn)擊屏幕后松開,事件序列            DOWN->UP  
點(diǎn)擊屏幕滑動(dòng)一會再松開,事件序列為   DOWN->MOVE->...->MOVE->UP。

getX/getY  獲取相對當(dāng)前View左上角的x和y坐標(biāo)  
getRawX/getRawY  獲取相對手機(jī)屏幕左上角的x和y坐標(biāo)。

3.TouchSlop

是系統(tǒng)能識別滑動(dòng)的最小距離
ViewConfiguration.get(getContext()).getScaledTouchSlop()

4.VelocityTracker

用于追蹤手指在滑動(dòng)過程中的速度。使用前在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度。

VelocityTracker velocityTracker = VelocityTracker.obtain(); 
velocityTracker.addMovement(event); 
//想知道滑動(dòng)速度時(shí)
//獲取速度前需計(jì)算速度  參數(shù) 時(shí)間間隔 單位ms
velocityTracker.computeCurrentVelocity(1000);  
//獲取速度
int xVelocity = (int)velocityTracker.getXVelocity(); 
int yVelocity = (int)velocityTracker.getYVelocity();

5.GestureDetector

用于輔助檢測用戶的單擊、滑動(dòng)、長按、雙擊等行為;建議:如果只是監(jiān)聽滑動(dòng)相關(guān)的推薦  
在onTouchEvent中實(shí)現(xiàn),如果需要監(jiān)聽雙擊,使用GeststureDetector。

eg: GestureDetector mGes = new GestureDetecotr(this);
    在View的OnTouchEvent方法中傳遞給mGes對象:
    return mGes.onTouchEvent(event);

6.Scroller

用來實(shí)現(xiàn)View的彈性滑動(dòng),View的scrollTo/scrollBy是瞬間完成的,  
使用Scroller配合View的computeScroll方法配合使用達(dá)到彈性滑動(dòng)的效果

二.View的滑動(dòng)

3種滑動(dòng)方式:
1.scrollTo和scrollBy
2.動(dòng)畫來移動(dòng)View
3.改變控件位置

1.ScrollTo 和 ScrollBy

scrollTo和scrollBy只能改變View內(nèi)容而不能改變View本身的位置。scrollBy內(nèi)部 
也是調(diào)用了scrollTo,它是基于當(dāng)前位置的相對滑動(dòng),scrollTo是基于所傳遞參數(shù) 
的絕對滑動(dòng)。在滑動(dòng)過程中mScrollX/mScrollY總是等于View邊緣與View內(nèi)容邊緣  
的距離,這兩個(gè)屬性用getScrollX/getScrollY方法獲取

2.使用動(dòng)畫

幀動(dòng)畫 View動(dòng)畫 屬性動(dòng)畫

使用動(dòng)畫來移動(dòng)View,主要是操作View的translationX和translationY屬性。需要  
注意的是,View動(dòng)畫只是對View的影像做操作,它并不能真正改變View的位置參  
數(shù),如果這個(gè)View設(shè)置了點(diǎn)擊事件,點(diǎn)擊動(dòng)畫后的新位置無法觸發(fā)點(diǎn)擊事件的,  
使用屬性動(dòng)畫沒有此問題,但3.0之前系統(tǒng)無屬性動(dòng)畫。

eg: xml設(shè)置
  <scale  
        android:fromXScale="float"  
        android:toXScale="float"  
        android:fromYScale="float"  
        android:toYScale="float"  
        android:pivotX="float"  
        android:pivotY="float" />  
    代碼中設(shè)置
       ScaleAnimation sAnima = new ScaleAnimation(0, 5, 0, 5);
       scale.startAnimation(sAnima); 
       
屬性動(dòng)畫:
        ObjectAnimator//  
         .ofFloat(view, "rotationX", 0.0F, 360.0F)//  
         .setDuration(500)//  
         .start();  

3.改變布局 參數(shù)

改變布局參數(shù)實(shí)現(xiàn)滑動(dòng),即改變LayoutParams,如想將一個(gè)View右平移100px,  
只需要將該View的LayoutParams里的marginLeft增加100px即可

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

==總結(jié):==
scrollTo/scrollBy: 操作簡單,適合對View內(nèi)容的滑動(dòng)
動(dòng)畫: 操作簡單,適合沒有交互的View和實(shí)現(xiàn)負(fù)責(zé)的動(dòng)畫效果
改變布局參數(shù):操作稍微復(fù)雜,適合有交互的View

三.彈性滑動(dòng)

1.使用Scroller原理: startScroll()保存了我們傳遞的幾個(gè)參數(shù) ——> invalidate()會導(dǎo)致View重繪 ——> draw() ——> computeScroll()該方法為空實(shí)現(xiàn),我們內(nèi)部調(diào)用scrollTo(x,y)實(shí)現(xiàn)滑動(dòng) 和 postInvalidate() 繼續(xù)重繪,反復(fù)下去完成彈性滑動(dòng)。

Scroller scroller=new Scroller(context);
//緩慢滾動(dòng)到指定位置
private void smoothScrollTo(int destX,int destY){
    int scrollx=getScrollX();
    int deltaX=destX-scrollX;
    //1000ms內(nèi)滑向destX,效果就是慢慢滑動(dòng)
    scroller.startScroll(scrollX,0,deltaX,0,1000);
    invalidate();
}
@Override
public void computeScroll(){
    if(scroller.computeScrollOffset()){
    scrollTo(scroller.getCurrX(),scroller.getCurrY());
    postInvalidate();
    }
}

2.通過動(dòng)畫可以直接實(shí)現(xiàn)彈性滑動(dòng)

3.使用延時(shí)策略完成滑動(dòng),核心思想 就是通過發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果。用Handler或View的postDelayed方法,postDelayed發(fā)送延時(shí)消息,然后消息中進(jìn)行View滑動(dòng),接連不斷的發(fā)送這種延時(shí)消息,達(dá)到彈性滑動(dòng)的效果。也可以使用線程的sleep方法來實(shí)現(xiàn)。

四.View的事件分發(fā)機(jī)制

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

dispatchTouchEvent(MotionEvent event) 用來處理事件的分發(fā),返回結(jié)果受當(dāng)前View的onTouchEvent和下級View的  
dispatchTouchEvent方法影響,表示是否消耗該事件。  

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

onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗  
,在同一事件序列里,當(dāng)前View無法再次接收到事件三者關(guān)系可以用如下偽代碼表示

==原理==:dispatchTouchEvent方法中判斷是否需要攔截,需要的話,就不再調(diào)用onInterceptTouchEvent,并調(diào)用onTouchEvent方法,如果不攔截,就會調(diào)用子VIew的dispatchTouchEvent方法。

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

事件順序: OnTouListener的OnTouch > onTouchEvent > onClickListener
如果OnTouch中返回true,就不會調(diào)用onTouchEvent方法,返回false就會調(diào)用,如果onTouchEvent中存在onClickListener,onClick一定會被調(diào)用。除非設(shè)置該VIew的onClickible 和 onLongClickible都為false。

1.對于一個(gè)根ViewGroup,點(diǎn)擊事件產(chǎn)生后,首先會傳遞給它,這時(shí)他的dispatchTouchEvent會調(diào)用,
如果它的onInterceptTouchEvent返回true表示要攔截當(dāng)前事件,接下來事件會交給這個(gè)ViewGroup  
處理,它的onTouchEvent就會被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent返回false,  
則事件會繼續(xù)傳遞給子元素,子元素的dispatchTouchEvent會調(diào)用,如此反復(fù)直到事件被處理。  

view事件傳遞


image
2.當(dāng)一個(gè)View需要處理事件時(shí),如果設(shè)置了OnTouchListener,那么OnTouchListener的onTouch方法  
會回調(diào),如果onTouch返回false,則當(dāng)前View的onTouchEvent方法會被調(diào)用;如果返回true,那么  
onTouchEvent方法將不會調(diào)用。由此可見,OnTouchListener優(yōu)先級高于onTouchEvent。  
OnClickListener優(yōu)先級處在事件傳遞的尾端。

3.一個(gè)點(diǎn)擊事件產(chǎn)生后,傳遞順序:Activity->Window->View;如果一個(gè)View的onTouchEvent返回false,  
那么它的父容器的onTouchEvent會被調(diào)用,以此類推,所有元素都不處理該事件,最終將傳遞給Activity  
處理,即Activity的onTouchEvent會被調(diào)用。

4.同一個(gè)事件序列是指從手指觸摸屏幕那一刻開始,到手指離開屏幕那一刻(down->move...move->up)

5.一個(gè)事件序列只能被一個(gè)View攔截且消耗,同一個(gè)事件序列所有事件都會直接交給它處理,  
并且它的onInterceptTouchEvent不會再被調(diào)用。


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

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

8.ViewGroup默認(rèn)不攔截任何事件,ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false。  

9.View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。  

10.View的onTouchEvent方法默認(rèn)消耗事件(返回true),除非他是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。  
View的longClickable屬性默認(rèn)都為false,clickable屬性分情況,Button默認(rèn)為true,TextView默認(rèn)為false。
onClick發(fā)生的前提是View可點(diǎn)擊,并且它收到了down和up事件。

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

2.頂級View對點(diǎn)擊事件的分發(fā)過程

①.ViewGroup對事件攔截的處理。

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;
}

原理:如果事件類型是MotionEvent.ACTION_DOWN 或者 已經(jīng)由子元素成功處理時(shí),既mFirstTouchTarget指向子元素,不為null,當(dāng)ViewGroup攔截事件時(shí),當(dāng)ACTION_MOVE 和 ACTION_UP來時(shí), 將不會再去調(diào)用onInterceptTouchEvent。并且其他的事件都會交給它處理。

1.ViewGroup會在down事件到來時(shí)重置狀態(tài),會將FLAG_DISALLOW_INTERCEPT重置,所以這個(gè)標(biāo)志不能攔截down事件。
2.FLAG_DISALLOW_INTERCEPT可以請求不要攔截事件,(move 和  up),前提是ViewGroup不攔截down事件。

②.ViewGroup不攔截事件,分發(fā)給子View的處理
子View接受點(diǎn)擊事件的標(biāo)準(zhǔn):
1子View在播放動(dòng)畫 2.點(diǎn)擊事件坐標(biāo)落在子View內(nèi)

子View如果設(shè)置了onClick事件,則會默認(rèn)設(shè)置onClickable為true 長按事件一樣,

3.View的滑動(dòng)沖突

1.常見滑動(dòng)沖突場景

  • 場景1 —— 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致,比如ViewPager中包含ListView;
  • 場景2 —— 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致,比如ScrollView中包含ListView;
  • 場景3 —— 上面兩種情況的嵌套
image

2.攔截方法: 外部攔截 內(nèi)部攔截

外部攔截:

q.png

在這里,首先down事件父容器必須返回false ,因?yàn)槿羰欠祷豻rue,也就是攔截了down事件,那么后續(xù)的move和up事件就都會傳遞給父容器,子元素就沒有機(jī)會處理事件了。

其次是up事件也返回了false,一是因?yàn)閡p事件對父容器沒什么意義,其次是因?yàn)槿羰录亲釉靥幚淼模瑓s沒有收到up事件會讓子元素的onClick事件無法觸發(fā)。

內(nèi)部攔截:

w.png

修改父類的攔截方法:

e.png

總結(jié):外部優(yōu)先于內(nèi)部攔截

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

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

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