3_View的事件體系

3.1.1 Veiw的基礎知識

什麼是View:Android 開發中,Activity承擔可視化的功能,同時Android系統提供了很多基礎控件,View是Android中所有控件的基類,不管是基礎的Button和TextView還是複雜的RelativeLayout和RecycleView,它們共同的基類都是View,View是一種界面層的控件的一種抽象,它代表了一個控件。除了View還有ViewGroup,ViewGroup內有許多控件,即一組View,ViewGroup也繼承View,這就意味着View可以是單個控件也可以是多個控件組成的一組控件。

3.1.2 View 的位置參數

View的位置主要由它四個頂點決定,分別對應View的四個屬性:left、top、right、bottom,left左上角橫座標,top左上角縱座標,right是右下角橫座標,bottom是右下角橫座標。這些座標是相對於它的父容器來說的,因此它是一種相對座標。

如何得到這四個參數:

Left = getLeft(); Top = getTop(); right = getRight(); Bottom = getBottom; 從android3.0開始,View增加了額外的幾個參數:x、y、translationX、translationY,其中x,y是View左上角的座標,translationX和tanslationY是左上角相對於父容器的偏移量。這幾個參數也是相對於父容器的座標。

x = left + translationX; y = top = translationY;

View在平移的過程,left和top表示是原始左上角的位置信息。其值不會發生改變。

3.1.4 VelocityTracker、GestureGetector和Scroller

1. VelocityTracker 速度追蹤,用於追蹤手指在屏幕滑動的速度。包括在水平方向和豎直方向。首先在View的OnTouchEvent事件中追蹤。

//追蹤當前點擊事件的速度 VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); //這裏的速度指的是手指在一定的時間內滑過的像素數 float xVelocity = velocityTracker.getXVelocity(); float yVelocity = velocityTracker.getYVelocity();

在android座標系中,手指逆着座標系正方向滑行,速度爲負值,當不需要使用的時候可以調用clear方法來重置和回收內存。

velocityTracker.clear();

velocityTracker.recycle();

2 GestureDetector,手勢檢測,用於輔助檢測用戶的單擊,雙擊,滑動,長按等事件。根據需要可以實現onDoubleTabListener監聽雙擊行爲

首先創建一個GestureDetector對象並實現OnGestrueListener接口,接着,接管目標View或目標界面的onTouchEvent方法 在onTouchEvent方法中添加如下實現:

boolean b = mGestureDetector.onTouchEvent(event); returen b;

3 Scroller 彈性滑動對象,用於實現View的彈性滑動,Scroller本身無法讓View本身進行彈性滑動,它需要和View的computeScroll方法進行配合使用才能共同完成這個功能

3.2 View的滑動,通過三種方式實現View的滑動:

1 View本身提供的scrollTo/scrollBy方法來實現滑動

2 通過動畫來實現View的滑動效果

3 改變View的LayoutPararms使得View重新佈局從而實現滑動。

3.2.1 使用scrollTo/scrollBy, scrollTo是相對當前參數的絕對滑動,scrollBy是基於當前位置的相對滑動。實際scrollBy也調用了scrollTo方法。同時,scrollBy和scrollTo只能改變View內容的位置不能改變View在佈局中的位置
3.2.2通過動畫可以實現View的平移,主要是操作ViewtranslationX和translationY屬性,採用屬性動畫時,爲了兼容android3.0需要採用開源動畫庫nineoldandroids。View動畫(非屬性動畫)是對View的影像做操作,並不能真的改變view的位置參數,如果希望動畫後的狀態得已保存還必須將fillAfter屬性設置爲true。屬性動畫並不會存在上述問題。
3.2.3 改變佈局參數,即改變LayoutPararms。

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

3.2.4 各種滑動方式的對比
1 scrollTo/scroBy是View提供的原生的方法,其作用專門用於滑動的,可以方便的實現滑動效果而不影響內部元素的單擊事件。缺點是隻能滑動view的內容,不能滑動view本身。
2 通過動畫實現View的滑動,在android3.0以上使用屬性動畫無明顯缺點,使用非屬性動畫或者在3.0以下使用屬性動畫,均不能改變view本身的屬性,如果不影響用戶交互的情況下,非屬性動畫是合適的,否則不太合適。
3 改變佈局方式實現滑動,無明顯缺點,操作稍微複雜。

3.3 彈性滑動有一個共同的思想,將一次大的滑動分成若干個小的滑動並在一個時間段內完成。

3.3.1使用Scroller,這裏的滑動是指View內容的滑動而非View本身位置的滑動,僅僅調用Scroller是無法讓View滑動的,因爲它內部並沒有做滑動相關的事。因爲invalidate方法,invalidate方法會導致view的重繪,在View的draw方法中又會去調用computeScroll方法,computeScroll方法在view中是一個空實現,需要我們自己去實現。正是這個computeScroll這個方法,View才能實現彈性滑動。

View重繪後會在draw方法調用computerScroll方法,computeScroll方法中又會向Scroller獲取當前scrollX和scrollY,然後通過scrollTo方法實現滑動,接着又調用postInvalidate方法第二次重繪,這一次重繪和上次重繪一樣,都是通過scrollTo滑到新的位置,如此反覆,調用Scroller的computeScrollOffet當返回false時整個滑動結束。

3.3.2 通過動畫,動畫本身是一種漸進的過程,通過動畫實現的滑動,天然的具有彈性效果,讓一個view在100ms內移動100個像素:

ObjectAnimator.ofFloat(tartgetView,"translationX",0,100).setDuration(100).start();
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDouration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @Override
    public void onAnimationUpdate(ValueAnimator animator){
        float fraction = animator.getAnimatedFraction();
        mButton1.scrollTo(startX + (int)(deltax * fraction),0);
    }
})

在上述代碼中動畫本質上沒有作用到任何對象上,只是在一千毫秒內完成動畫的過程,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,然後再根據這個比例計算出當前View所要滑動的距離。

###3.3.3使用延時策略,它的核心思想是通過發送一系列延時消息從而達到一種漸進式效果,具體可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法

3.4 View的事件分發機制

3.4.1 點擊事件的傳遞規則

點擊事件分析的對象就是MotionEvent, 所謂點擊事件的分發其實是對MotionEvent事件的分發過程,即當一MotionEvent產生以後,系統需要把這個事件傳遞給一個具體的View,而這個過程就是分發過程,分發過程由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)

用來進行事件的分發。如果事件能夠傳遞給當前的View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatTouchEvent方法的影響。

public boolean onInterceptTouchEvent (MotionEvent ev)

用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

public boolean onTouEvent(MotionEvent ev)

在dispatchTouchEvent方法中調用,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

當一個View需要處理事件時,如果它設置了OnTouchListener,那麼OntouchListener中的onTouch方法會被調用,如果返回false,onTouchEvent方法會被調用,返回true的話onTouchEvent方法不會被調用,因此OnTouchListener優先級高於onTouchEvent方法,在onTouchEvent方法中,如果當前View設置了OnClicklistener,那麼它的onClick方法會被調用,因此OnClickListener優先級最低,處於事件傳遞的尾端。

事件產生後的傳遞過程:

Activity-->Window-->View,事件總是先傳給Activity,activity在傳遞給window,最後window再傳給頂級View。如果一個View的onTouchEvent返回false,那麼它的父容器的onTouchEvent將會被調用,如果所有的元素都不處理這個事件,那麼這個事件最終會傳遞給activity處理,即activity的onTouchEvent會被調用。

正常情況下,一個事件序列只能被一個View攔截和消耗,因爲一旦一個元素攔截了此事件,那麼同一個事件序列內所有事件都會直接交給它處理,因此同一個事件序列不能分別由兩個View同時處理,但是通過特殊手段可以做到,一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理

ViewGroup默認不攔截任何事件,onInterCepTouchEvent方法默認返回false。

View沒有onInterceptTouchEvent方法,一旦事件傳遞給它,那麼它的onTouchEvnet方法一定會被調用。

View的onTouchEvent默認都會消耗事件,除非該View是不可點擊的(clickable和longClickable同時爲false)。View的longClickablse屬性都默認爲false

View的enable屬性不影響onTouchEvent默認返回值,只跟clickable和longClickable有關。

事件傳過程是由外向內的,事件總是先傳遞給父元素,再由父元素分發給子元素,但是通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發過程,但是ACTION_DOWN除外

3.4.2 事件分發的源碼分析

點擊事件最先傳遞給當前activity,由activity的dispatchTouchEvent來進行事件派發,具體由activity內部的Window來完成的。Window會將事件傳遞給decor view ,decor view一般就是當前界面的底層容器(即setContentView所設置View的父容器)。

Window是個抽象類,Window的superDispatchTouchEvent也是抽象方法,Window的實現類是PhoneWindow,看源碼知道,PhoneWindow會將事件直接傳遞給DecorView,可以通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式獲取activity所設置的頂級View,設置的View的父容器就是DecorView,所以最終事件會傳遞給View。

ViewGroup在兩種情況下會判斷是否要攔截當前事件:

事件類型爲ACTION_DOWN或者mFirstTouchTarget != null ,當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget會被賦值並指向子元素,也就是VeiwGoup不攔截事件並將事件交由子元素處理時,mFirstTouchTarget != null成立,反之不成立。不成立的話,那麼當ACTION_DOWN和ACTION_UP到來時事件類型爲ACTION_DOWN或者mFirstTouchTarget != null這個條件不成立將導致ViewGroup的onInterceptTouchEvent不會被再調用。並且同一序列的其他事件都會默認交給它處理。

*FLAG_DISALLOW_INTERCEP這個標記位是通過requestDisallowIntercepTouchEvent方法來設置的,一般用於子元素中,一旦設置後,ViewGroup將無法攔截除ACTION_DOWN以外的其他事件,ViewGroup在分發事件時,如果是ACTION_DOWN,將導致子View中設置的這個FLAG_DISALLOW_INTERCEPT標記位無效。因此,當面對ACTION_DOWN事件時,ViewGrop總是會調用自己的onInterceptTouchEvent。

第一點:onInterceptTouchEvent不是每次事件都會被調用的。如果想提前處理所有的事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都會調用。

第二點:FLAG_DISALLOW_INTERCEPT標記位可以解決滑動衝突。

ViewGroup不攔截事件時,首先會遍歷ViewGroup的所有子元素,判斷子元素是否能夠接收到事件。是否能夠接收事件主要由兩點判斷:

子元素是否在播動畫和點擊事件的座標是否落在子元素的區域內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給它來處理。

mFirstTouchTarget真正的賦值是在addTouchTarget內部完成的,如果遍歷所有的子元素後事件都沒有被合適地處理,這包含兩種情況:

第一種是ViewGroup沒有子元素;第二種是子元素處理了點擊事件,但是在dispatchTouchEvent中返回了false,這一般是因爲子元素在onTouchEvent中返回了false。在這兩種情況下,ViewGroup會自己處理點擊事件。

3.5 View的滑動衝突:在界面中只要內外兩層同時可以滑動,,這個時候就會產生滑動衝突

3.5.1 常見的滑動衝突

1 外部滑動方向和內部滑動方向不一致;

2 外部滑動方向和內部滑動方向一致;

3 上面兩種情況的嵌套;

3.5.2 滑動衝突的處理規則

image

場景1:當用戶左右滑動時,需要讓外部攔截點擊事件,當用戶上下滑動時,需要讓內部View攔截點擊事件,水平滑動和豎直滑動可以依據滑動路徑和水平方向所形成的夾角也可以依據水平方向和豎直方向的距離差來判斷,某些特殊時候還可以依據水平和豎直方向的速度差來做判斷。這裏我們可以通過水平和豎直方法的距離差來判斷,比如豎直方向滑動的距離大就判斷爲豎直滑動,否則判斷爲水平滑動

場景2 無法通過滑動的角度、距離差和速度差來做判斷,一般這個時候可業務狀態來判斷,比如當處於某種狀態需要外部View響應用戶的滑動,而處於另一種狀態則需要內部View來響應用戶的滑動,依據這種業務狀態也能得出相應的處理規則

image

場景3來說同場景2一樣無法通過角度、距離差、速度差來判斷,同樣還是隻能從業務上找到突破點。

3.5.3 滑動衝突的解決方式

1 外部攔截

所謂外部攔截是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截反之不攔截,這樣可以解決滑動衝突的問題。這種方法比較符合點擊事件的分發機制,外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截即可。

public boolean oInterceptTouchEvent(MotionEvent event){
    boolean intercept = false;
    int x = (int)event.getX();
    int y = (int)event.getY();
    case MotionEvent.ACTION_DOWN:
        interceptd = false;
    break;
    
    case MotionEvemt.ACTION_MOVE:
        if(父容器需要當前點擊事件){
            interceptd = true;
        } else{
            interceptd = false;
        }
    break;
    
    case MotionEvent.ACTION_UP:
        interceptd = false;
    break;
    default;
    break;
    mLasXIntercept = x;
    mLastYIntecept = y;
    return intercepted;
}

在onInterceptTouchEvent方法中,首先是ACTION_DOWN這個事件,父容器必須返回false,即不攔截ACTION-DOWN事件,,因爲一旦攔截了ACTION_DOWN,那麼後續的ACTION-MOVE和ACTION_UP事件都會直接交由父容器處理,無法再將事件傳遞給子元素;其次是ACTION_MOVE事件,這個事件可以根據需要決定是否需要攔截,如果父容器需要攔截就返回true,最後是ACTION_UP事件,這裏必須返回false,因爲ACTION_UP事件本身沒有太多意義。(如果事件交由子元素處理,這時父容器在ACTION_UP中返回true,就會導致子元素無法接收到ACTION_UP事件,子元素的onClick事件就無法觸發。)

2 內部攔截法 內部攔截法是指父容器不攔截任何事件,所有的事件都交由子元素,如果子元素需要攔截事件就直接處理,否則交由父容器處理,這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,需要重寫子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event){
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getAction()){
       case MotionEvent.ACTION_DOWN:
        parent.requestDisallowInterceptTouchEvent(true);
    break;
    
    case MotionEvemt.ACTION_MOVE:
        int deltax = x - mLastX;
        int deltay = y - mLastY;
        if(父容器需要此類點擊事件{
        parent.requestDisallowInterceptTouchEvent(false);
        }
    break;
    
    case MotionEvent.ACTION_UP:
    break;
    default;
    break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispathcTouchEvent(event);
}

上述代碼是內部攔截法的典型代碼,當面對不同的滑動策略時只需要修改裏面大的條件即可,其他不需要改動而且也不能改動。除了子元素需要做處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其他的事件。

考慮一種情況,如果此時用戶正在水平滑動,但是在水平滑動停止之前如果用戶再迅速進行豎直滑動,就會導致界面在水平方向無法滑動到終點從而處於一種中間狀態。因此,但水平方向正在滑動時,下一個序列的點擊事件仍然交給父容器處理,這樣水平方向就不會停留在中間狀態了。

發佈了35 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章