1. 程式人生 > >我的“View的事件體系”知識點總結

我的“View的事件體系”知識點總結

文章總結自《android開發藝術探索》一書。

3.1 View基礎知識

1、View是Android中所有控制元件的基類,是一種介面層的控制元件的一種抽象,也代表了一種控制元件。

2、在Android中View呈現出樹的結構,ViewGroup繼承了View,這代表View本身可以是單個控制元件也可以是多個控制元件組成的控制元件組

3、View中的四個屬性:top、left、right、bottom,top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角縱座標;Android3.0開始增加了x、y、translationX、translationY

四個引數,x、y是View左上角的座標,另外兩個代表View左上角相對於父容器的偏移量,View在平移的時候top、left不會變,x、y等4個引數變化;另外View中座標系X軸正方向向右,Y軸正方向向下。

4、獲取屬性的方法:

  • Left =getLeft(); 剩下幾個類比;
  • getX/getY,返回的是控制元件或事件相對於當前View左上角的座標
  • getRawX/getRawY,返回的是控制元件或事件相對於手機螢幕左上角的座標

5、 MotionEvent典型事件型別:

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP

6、 TouchSlop:滑動最小距離,即兩次滑動之間的距離小於這個值,系統預設不是滑動操作,獲取方式:ViewConfiguration.get(getContext()).getScaledTouchSlop()

7、 VelocityTracker,速度追蹤器,追蹤手指在滑動過程中的速度,使用方法:

//build
VelocityTracker vt = VelocityTracker.obtain();
VelocityTracker.addMovement(event);

//use
vt.computeCurrentVelocity(1000);
int Vx = (int) vt.getXVelocity();
int Vy = (int) vt.getYVelocity();

//recyle
vt.clear();
vt.recyle();

8、GestureDetector,手勢檢測器,輔助檢測單擊、滑動等事件,用法:

//build
GestureDetector mGestureDetector = new GestureDetector(this);

//implement Interface: OnGestureListener || OnDoubleTapListener
    ....

//code in Method: mView.onTouchEvent
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

9、Scroller,用於實現View的彈性滑動。View的srcollTo/scrollBy過程是瞬間的,Scroller本身無法滑動View,需要和View的computeScroll配合使用,用法:

Scroller mScroller = new Scroller(mContext);

private void smoothScrollTo(int destX,int destY){
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    //slide slowly to destX in 1000ms
    mScroller.startScroll(scrollX,0,delta,0,1000);
    invalidate();
}

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

startScroll方法只是做一些賦值計算的工作,實際上起作用的是下面的invalidate,該方法導致View重繪,View的draw方法中會去呼叫computeScroll方法,computeScroll去獲取Scroller當前的屬性,然後開始滑動,再通知重繪,如此反覆。

computeScrollOffset()方法是根據當前時間流逝的百分比來計算scrollX和scrollY改變的百分比並計算當前值,類似於屬性動畫中的插值器和估值器。返回值決定滑動是否結束。

3.2 View的滑動

三種方式實現View的滑動:

  • View自身提供的scrollTo/scrollBy方法
  • 動畫給View施加平移效果
  • 改變View的LayoutParams使View重新佈局

1、scrollTo/scrollBy
scrollBy呼叫了scrollTo,前者是基於當前距離的相對滑動,後者是絕對滑動。其原始碼中的mScrollX總是等於View左邊緣和View內容左邊緣的水平距離,mScrollY上邊緣和內容上邊緣垂直距離,從左往右滑,mScrollX為負,從上往下滑,mScrollY為負,並且只能改變View內容的位置而不能改變View在佈局中的位置。

2、使用動畫
android3.0以上可以使用屬性動畫,基本沒缺點,3.0以下版本的View動畫也可以移動,但是不能改變物件的屬性,所以onClick等事件需要解決。

3、改變佈局引數
改變LayoutParams中的marginXXX引數的值或者在需要的地方放上空的View,需要時改變View的寬度高度用來“擠走”目標控制元件,例如:

MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams();
params.width += 100;
params.height += 100;
mButton.requestlayout();

4、三者比較
scrollTo/scrollBy唯一缺點:只能滑動內容,不能改變View本身。
使用動畫:3.0以上屬性動畫沒有明顯缺點,3.0以下View動畫不能改變View的屬性。動畫缺點:不適用於與使用者互動的控制元件。
改變佈局:操作複雜,沒有明顯缺點。

5、使用延時策略
傳送一系列的延時訊息,可以使用Handler或View的postDelayed方法,也可以使用執行緒的sleep方法。

3.4 View的事件分發機制

1、針對MotionEvent的事件分發,主要由三個方法完成:

  • public boolean dispatchTouchEvent(MotionEvent ev),用來進行當前事件的分發,如果事件能傳遞給當前View,此方法一定會呼叫,返回結果受當前View的onTouchEvent和下級的dispatchTouchEvent方法影響,表示是否消耗該事件
  • public boolean onInterceptTouchEvent(MotionEvent event),在上述方法內部呼叫,用來表示是否攔截該事件,如果View攔截了該事件,那麼同一事件序列的所有事件都交給該View處理,並且此方法不會再被呼叫
  • public boolean onTouchEvent(MotionEvent event),在dispatchTouchEvent方法中呼叫,處理點選事件,返回結果表示是否消耗該事件。

關係虛擬碼:

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

2、點選事件的激發順序:onTouchListener—>onTouchEvent—>onClickListener

3、點選事件的傳遞順序: Activity—>Window—>View,頂層View拿到事件以後進行分發。分發時如果子View無法處理事件,會上拋事件,父View的onTouchEvent會被呼叫,如果所有元素都不處理這件事,就會拋給Activity,Activity的onTouchEvent會被呼叫。

4、同一事件序列:指的是從手指觸控式螢幕幕的那一刻,中間經歷了若干個move事件,到手指離開螢幕的那一刻,中間產生的所有事件。

5、正常情況下,同一事件序列的所有事件只能被一個View攔截並且消耗。

6、某個View一旦開始處理事件,如果onTouchEvent返回false,即不消耗事件,那麼同一事件序列中剩下的所有事件都不交給它處理,轉交給父View去處理。

7、ViewGroup預設不攔截任何事件,View沒有onInterceptTouchEvent方法,事件傳給它直接呼叫onTouchEvent。

8、View的onTouchEvent預設都會消耗事件,除非是不可點選的,View的enable屬性不影響onTouchEvent的預設返回值。

9.onClick事件發生的前提是當前View可點選,並且接收到了downup事件。

10、可以通過requestDisallowInterceptTouchEvent方法在子元素中干預父元素的分發過程,但是ACTION_DOWN事件除外。

3.5 View的滑動衝突

1、常見的滑動衝突場景

  • 外部滑動方向和內部滑動方向不一致,比如外層一個ScrollView,內層一個ListView
  • 外部滑動方向和內部滑動方向一致,比如內外層同時能上下滑動或者左右滑動
  • 上面兩種情況的巢狀

第一種的處理規則是:根據使用者不同的滑動方向,讓不同層的View攔截點選事件。具體來說根據滑動過程中兩個點的座標來判斷,比如說兩點座標的豎直距離差與水平距離差的大小比較,水平豎直方向的速度差等。

第二、三種的處理規則:根據業務邏輯,在View的攔截中加上相應的業務邏輯判斷是否攔截處理。

2、衝突的解決方式

外部攔截法
點選事件都先經過父容器的攔截處理,如果父容器需要就攔截,不需要就不攔截。需要重寫父容器的onInterceptTouchEvent方法。

典型邏輯程式碼:

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(父容器需要當前點選事件){
                intercepted = true;
            }else{
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP:{
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

ACTION_DOWN事件一定不能攔截,否則後續事件都交給父容器處理,如果父容器處理不好會上拋給上層父容器,不會傳遞到其子元素,另外ACTION_UP事件沒有多大意義,也要返回false。

內部攔截法
父容器不攔截任何事件,所有事件都傳遞給子元素,如果子元素需要此事件就消耗掉,否則就上拋給父容器進行處理。需要配合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 MotionEvent.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.dispatchTouchEvent(event);
}

父元素預設攔截除了ACTION_DOWN以外的其他事件:

public boolean onInterceptTouchEvent(MotionEvent event){
    int action = event.getAction();
    if(action==MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}