Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View的事件體系

Android View的事件體系

編輯:關於Android編程

1.View基本知識

(1)view的層次結構:ViewGroup也是View;
(2)view的位置參數:top、left、right、bottom,分別對應View的左上角和右下角相對於父容器的橫縱坐標值。
從Android 3.0開始,view增加了x、y、translationX、translationY四個參數,這幾個參數也是相對於父容器的坐標。x和y是左上角的坐標,而translationX和translationY是view左上角相對於父容器的偏移量,默認值都是0。
x = left + translationX
y = top + translationY

\

 

(3)MotionEvent是指手指接觸屏幕後所產生的一系列事件,主要有ACTION_UP、ACTION_DOWN、ACTION_MOVE等。正常情況下,一次手指觸屏會觸發一系列點擊事件,主要有下面兩種典型情況:
1.點擊屏幕後離開,事件序列是ACTION_DOWN->ACTION_UP;
2.點擊屏幕後滑動一會再離開,事件序列是ACTION_DOWN->ACTION_MOVE->ACTION_MOVE-> … ->ACTION_UP;
通過MotionEvent可以得到點擊事件發生的x和y坐標,其中getX和getY是相對於當前view左上角的x和y坐標,getRawX和getRawY是相對於手機屏幕左上角的x和y坐標。
(4)TouchSlope是系統所能識別出的可以被認為是滑動的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()。
(5)VelocityTracker用於追蹤手指在滑動過程中的速度,包括水平和垂直方向上的速度。
速度計算公式:速度 =(終點位置- 起點位置)/ 時間段
速度可能為負值,例如當手指從屏幕右邊往左邊滑動的時候。此外,速度是單位時間內移動的像素數,單位時間不一定是1秒鐘,可以使用方法computeCurrentVelocity(xxx)指定單位時間是多少,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度,手指在1s中滑動了100個像素,那麼速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來獲取速度,在100ms內手指只是滑動了10個像素,那麼速度是10,即10(像素/100ms)。

VelocityTracker的使用方式:

//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//獲取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用

(6)GestureDetector用於輔助檢測用戶的單擊、滑動、長按、雙擊等行為。GestureDetector的使用比較簡單,主要也是輔助檢測常見的觸屏事件。作者建議:如果只是監聽滑動相關的事件在onTouchEvent中實現;如果要監聽雙擊這種行為的話,那麼就使用GestureDetector。

2.View的滑動

(1)常見的實現view的滑動的方式有三種:
第一種是通過view本身提供的scrollTo和scrollBy方法:操作簡單,適合對view內容的滑動;
第二種是通過動畫給view施加平移效果來實現滑動:操作簡單,適用於沒有交互的view和實現復雜的動畫效果;
第三種是通過改變view的LayoutParams使得view重新布局從而實現滑動:操作稍微復雜,適用於有交互的view。
(2)scrollTo和scrollBy方法只能改變view內容的位置而不能改變view在布局中的位置。scrollBy是基於當前位置的相對滑動,而scrollTo是基於所傳參數的絕對滑動。通過View的getScrollX和getScrollY方法可以得到滑動的距離。
(3)使用動畫來移動view主要是操作view的translationX和translationY屬性,既可以使用傳統的view動畫,也可以使用屬性動畫。
使用動畫還存在一個交互問題:在android3.0以前的系統上,view動畫和屬性動畫,新位置均無法觸發點擊事件,同時,老位置仍然可以觸發單擊事件。從3.0開始,屬性動畫的單擊事件觸發位置為移動後的位置,view動畫仍然在原位置。
(4)動畫兼容庫nineoldandroids中的ViewHelper類提供了很多的get/set方法來為屬性動畫服務,例如setTranslationX和setTranslationY方法,這些方法是沒有版本要求的。

3.彈性滑動

(1)Scroller的工作原理:Scroller本身並不能實現view的滑動,它需要配合view的computeScroll方法才能完成彈性滑動的效果,它不斷地讓view重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出view的當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成view的滑動。就這樣,view的每一次重繪都會導致view進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作原理。

Scroller是可以實現平滑效果的,它的實現原理很簡單,其實就是不斷調用scrollTo和scrollBy方法來實現view的平滑移動,因為人眼的視覺暫留特性看起來就是平滑的。
使用Scroller主要有三個步驟:
1.初始化Scroller對象,一般在view初始化的時候同時初始化scroller;
2.重寫view的computeScroll方法,computeScroll方法是不會自動調用的,只能通過invalidate->draw->computeScroll來間接調用,實現循環獲取scrollX和scrollY的目的,當移動過程結束之後,Scroller.computeScrollOffset方法會返回false,從而中斷循環;
3.調用Scroller.startScroll方法,將起始位置、偏移量以及移動時間(可選)作為參數傳遞給startScroll方法。

例如,書中給出的例子,子view在被拖動之後會自動平滑移動到原來的位置

private void ininView(Context context) {
    setBackgroundColor(Color.BLUE);
    // 初始化Scroller
    mScroller = new Scroller(context);
}

@Override
public void computeScroll() {
    super.computeScroll();
    // 判斷Scroller是否執行完畢
    if (mScroller.computeScrollOffset()) {
        ((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
        // 通過重繪來不斷調用computeScroll
        invalidate();//很重要
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastX = (int) event.getX();
            lastY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            int offsetX = x - lastX;
            int offsetY = y - lastY;
            ((View) getParent()).scrollBy(-offsetX, -offsetY);
            break;
        case MotionEvent.ACTION_UP:
            // 手指離開時,執行滑動過程
            View viewGroup = ((View) getParent());
            mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(),
                    -viewGroup.getScrollX(), -viewGroup.getScrollY());
            invalidate();//很重要
            break;
    }
    return true;
}

(2)使用延時策略來實現彈性滑動,它的核心思想是通過發送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法。

4.Android事件傳遞機制

Android 的view結構是樹形的結構,也就是說,View可以放在ViewGroup裡面,通過不同的組合來實現不同的形式。那麼問題就來了,View放在ViewGroup裡面,這個ViewGroup又放在另一個ViewGroup裡面,甚至還可能繼續嵌套,一層層地疊起來。可我們的觸摸事件就一個,到底該分給誰呢?同一個事件, 子View和父ViewGroup都可能想要進行處理。因此就產生了‘事件攔截’這個霸氣的稱呼。

(1) 所有 Touch事件都被封裝成了MotionEvent對象,包括Touch的位置、時間、歷史記錄以及第幾個手指(多指觸摸)等。

(2)事件類型分為ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每個事件都是以ACTION_DOWN開始ACTION_UP結束。

(3) 對事件的處理包括三類,分別為傳遞——dispatchTouchEvent()函數、攔截——onInterceptTouchEvent()函數、消費——onTouchEvent()函數和OnTouchListener

2、傳遞流程

(1) 事件從 Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的View(ViewGroup)開始一直往下(子View)傳遞。子View可以通過onTouchEvent()對事件進行處理。

(2) 事件由父 View(ViewGroup)傳遞給子View,ViewGroup可以通過onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。

(3) 如果事件從上往下傳遞過程中一直沒有被停止,且最底層子 View沒有消費事件,事件會反向往上傳遞,這時父View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最後會到Activity的onTouchEvent()函數。

(4) 如果 View沒有對ACTION_DOWN進行消費,之後的其他事件不會傳遞過來。

(5) OnTouchListener 優先於 onTouchEvent()對事件進行消費。
上面的消費即表示相應函數返回值為 true。

對於ViewGroup來說重寫了dispatchTouchEvent()onInterceptTouchEvent()OnTouchEvent()三個方法。

\

(1)事件分發過程的三個重要方法
public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發。如果事件能夠傳遞給當前view,那麼此方法一定會被調用,返回結果受當前view的onTouchEvent和下級view的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,如果當前view攔截了某個事件,那麼在同一個事件序列當中,此方法不會再被調用,返回結果表示是否攔截當前事件。
若返回值為True事件會傳遞到自己的onTouchEvent();
若返回值為False傳遞到子view的dispatchTouchEvent()。

public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法內部調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前view無法再次接收到事件。
若返回值為True,事件由自己處理,後續事件序列讓其處理;
若返回值為False,自己不消耗事件,向上返回讓其他的父容器的onTouchEvent接受處理。

三個方法的關系可以用下面的偽代碼表示:

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

(2)OnTouchListener的優先級比onTouchEvent要高
如果給一個view設置了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,那麼OnTouchListener中的onTouch方法不會被回調,就輪到當前view的onTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。
在onTouchEvent方法中,如果當前view設置了OnClickListener,那麼它的onClick方法會被調用,由此可見我們平時常用的OnClickListener的優先級最低,處於事件傳遞的尾端。
(3)當一個點擊事件發生之後,傳遞過程遵循如下順序:Activity -> Window -> View。
如果一個view的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent方法將會被調用,依此類推,如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理(調用Activity的onTouchEvent方法)。
(4)正常情況下,一個事件序列只能被一個view攔截並消耗,因為一旦某個元素攔截了某個事件,那麼同一個事件序列內的所有事件都會直接交給它處理,並且該元素的onInterceptTouchEvent方法不會再被調用了,意思就是這個事件老子到這就處理了,以後不用再詢問我是否處理了,已經整完了。
(5)某個view一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一事件序列的其他事件都不會再交給它來處理,並且事件將重新交給它的父容器去處理(調用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那麼這個點擊事件會消失,父容器的onTouchEvent方法不會被調用,當前view依然可以收到後續的事件,但是這些事件最後都會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件,因為它的onInterceptTouchEvent方法默認返回false。

(7)View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable都為false)。view的longClickable默認是false的,clickable則不一定,Button默認是true,而TextView默認是false。
(8)View的enable屬性不影響onTouchEvent的默認返回值。哪怕一個view是disable狀態,只要它的clickable或者longClickable有一個是true,那麼它的onTouchEvent就會返回true。
(9)事件傳遞過程總是先傳遞給父元素,然後再由父元素分發給子view,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外,即當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件。
ViewGroup的dispatchTouchEvent方法中有一個標志位FLAG_DISALLOW_INTERCEPT,這個標志位就是通過子view調用requestDisallowInterceptTouchEvent方法來設置的,一旦設置為true,那麼ViewGroup不會攔截該事件。
(10)以上結論均可以在書中的源碼解析部分得到解釋。Window的實現類為PhoneWindow,獲取Activity的contentView的方法

((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

5.view的滑動沖突

(1)常見的滑動沖突的場景:
1.外部滑動方向和內部滑動方向不一致,例如viewpager中包含listview;
2.外部滑動方向和內部滑動方向一致,例如viewpager的單頁中存在可以滑動的bannerview;
3.上面兩種情況的嵌套,例如viewpager的單個頁面中包含了bannerview和listview。
(2)滑動沖突處理規則
可以根據滑動距離和水平方向形成的夾角;或者根絕水平和豎直方向滑動的距離差;或者兩個方向上的速度差等
(3)解決方式
1.外部攔截法:點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要就不攔截。該方法需要重寫父容器的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: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        if (父容器需要攔截當前點擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
            intercepted = true;
        } else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }

    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

2.內部攔截法:父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器來處理。這種方法和Android中的事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。(不建議使用)

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved