三、Android開發(fā)藝術探索之View的事件體系

雖然在前面寫自定義View的時候有提過事件的傳遞機制,但是并沒有全面系統的學習和記錄巍佑,趁著寫這篇博客的機會茴迁,把View的事件體系好好學習一遍,這篇博客里面不光有書中的內容萤衰,也有我自己的見解堕义。


本文目錄

一、View基礎知識

1. 什么是View

View是Android中所有控件的基類脆栋,不管是類似于Button還是類似于RelativeLayout倦卖,它們的共同基類都是View,所以說椿争,View是界面層的控件的一種抽象怕膛,它代表了一個控件,除了View秦踪,還有ViewGroup,ViewGroup可以翻譯成控件組褐捻,內部包含了許多控件,即一組View椅邓。ViewGroup也繼承了View柠逞,這就意味著,View本身就可以是單個控件景馁,也可以是多個控件組成的一組控件板壮。

2.View的位置參數

View的位置主要是由它的四個頂點決定的,分別對應于View的四個屬性:top合住、left绰精、right、bottom(對應的是左上右下兩個點的坐標)聊疲。需要注意的是茬底,這些坐標都是相對于View的父容器來說的,因此它是一種相對坐標获洲。View的坐標和父容器的關系如圖

View的位置坐標和父容器的關系

我們很容易得出View的寬高和坐標的關系:
width = right - left
height = bottom - top
如何獲得這四個參數呢阱表,很簡單:
left = getLeft(); right = getRight(); top = getTop(); bottom = getBottom();
從Android3.0開始贡珊,View增加了額外的幾個參數:x最爬、y、translationX门岔、translationY 爱致,其中x和y 是View左上角的坐標,而translationX和translationY是View左上角相對于父容器的偏移量寒随。這幾個參數也是相對于父容器的坐標糠悯,并且translationX和translationY的默認值是0帮坚,和View的四個基本位置參數一樣,View也為它們提供了get/set方法互艾,這幾個參數的換算關系如下:
x = left + translationX ;
y = top + translationY ;

View在平移的過程中试和,top和left表示的是原始左上角的位置信息,其值并不會發(fā)生改變纫普,此時發(fā)送改變的是x阅悍,y、translationX和translationY這四個參數昨稼。

3. MotionEvent和TouchSlop

3.1 MotionEvent

在手指接觸屏幕后所產生的一系列事件中节视,典型的事件由如下幾種:
ACTION_DOWN : 手指剛接觸屏幕。
ACTION_MOVE: 手指在屏幕上移動假栓。
ACTION_UP :手指從屏幕上松開的一瞬間寻行。
正常情況下,一次手指觸摸屏幕的行為會觸發(fā)一系列的點擊事件但指,考慮如下幾種情況:
1.點擊屏幕后離開松開寡痰,事件序列為:DOWN -> UP.
2.點擊屏幕滑動一會兒再松開,事件序列為 :DOWN -> MOVE -> ... -> MOVE -> UP.

上述三種情況時典型的事件序列棋凳,同時通過MotionEvent對象我們可以得到點擊事件發(fā)生的x和y坐標拦坠。為此,系統提供了兩組方法: getX/getY 和 getRawX/getRawY剩岳。它們的區(qū)別其實很簡單:getX/getY返回的是相對于當前View左上角的x和y坐標贞滨,而getRawX/getRawY返回的是相對于手機屏幕左上角的x和y坐標。

3.2 TouchSlop

TouchSlop是系統所能識別出的被認為是滑動的最小距離拍棕,當手指在屏幕上滑動的時候晓铆,如果兩次滑動之間的距離小于這個常量,那么系統就不認為你是在進行滑動操作绰播。這是一個常量骄噪,和設備有關,在不同設備上這個值可能是不同的蠢箩,通過如下方式即可獲取這個常量:ViewConfiguration.get(getContext()).getScaledTouchSlop(); 這個值是8dp链蕊。當我們在處理滑動時,可以利用這個常量來進行一些過濾谬泌。如果兩次滑動的距離小于這個值滔韵,那么我們就認為它們不是滑動。

4. 速度追蹤掌实、手勢檢測陪蜻、Scroller

4.1 Velocity Tracker 速度追蹤

用于追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度贱鼻。在View的onTouchEvent方法中追蹤當前單擊事件的速度:

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

接著我們就可以來獲取速度了宴卖,但是獲取速度之前必須先計算速度:

        velocityTracker.computeCurrentVelocity(1000);//在1000ms中的速度
        float xVelocity = velocityTracker.getXVelocity();
        float yVelocity = velocityTracker.getYVelocity();

這里的速度是指一段時間內手指滑過的像素數滋将,比如時間間隔設為1000ms時,在1s內症昏,手指在水平方向從左向右滑過100像素耕渴,那么水平速度就是100。速度可能為負數齿兔,當手指從右向左滑動時,產生的速度就是負數础米,如果時間間隔是100ms分苇,100ms內從左向右滑過100像素,那么速度就是100/0.1s = 1000像素屁桑。
速度 = (終點位置 - 起點位置)/ 時間段 医寿;
當不使用它的時候,需要調用clear方法來重置并回收內存:

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

4.2 GestureDetector 手勢檢測

手勢檢測蘑斧,用于輔助檢測用戶的單擊靖秩,滑動,長按竖瘾,雙擊等行為沟突。
GestureDetector的使用,首先需要創(chuàng)建一個GestureDetector 對象并繼承OnGestureListener和OnDoubleTapListener接口捕传,并接管View的onTouchEvent方法惠拭,在待監(jiān)聽的View的onTouchEvent方法中添加如下實現:

        boolean b = gestureDetector.onTouchEvent(event);
        return b;

然后我們就可以有選擇的實現這兩個接口中的方法了:


圖片.png

在實際開發(fā)中,如果只是監(jiān)聽滑動相關的庸论,建議在onTouchEvent中實現职辅,如果是監(jiān)聽雙擊這種行為,使用GestureDetector聂示。域携。

4.3 Scroller

當我們使用View的scrollTo/scrollBy方法來進行滑動時,其過程是瞬間完成的鱼喉,有了Scroller秀鞭,我們就可以實現有過渡效果的滑動,其過程不是瞬間完成的蒲凶,而是在一定的時間間隔內完成的气筋。使用Scroller進行彈性滑動的代碼是固定寫法的。

Scroller scroller = new Scroller(getContext());

    private void smoothScrollerTo(int destX, int destY){
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        scroller.startScroll(scrollX,0,delta,1000);
        invalidate();//重繪界面
    }

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

二旋圆、View的滑動

在Android設備上宠默,滑動幾乎是應用的標配,通過三種方法可以實現View的滑動:第一種通過View本身提供的ScrollTo/ScrollBy方法來實現滑動灵巧;第二種通過動畫給View施加平移效果來實現滑動搀矫;第三種通過改變View的LayoutParams使得View重新布局從而實現滑動抹沪。

1 使用ScrollTo/ScrollBy

為了實現View的滑動,View提供了專門的方法來實現這個功能瓤球,那就是ScrollTo和ScrollBy融欧,這兩個方法的源碼比較簡單:

/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }


 /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

從源碼中可以看出,scrollBy實際上也是調用了scrollTo方法卦羡,它實現了基于當前位置的相對滑動噪馏,而scrollTo則實現了基于所傳遞參數的絕對滑動。(scrollTo和scrollBy的區(qū)別:scrollTo是滾動到绿饵。滾動到10像素欠肾,-30像素,scrollBy拟赊,在原來的基礎上滾動刺桃,假如上一次滾動到10像素,如果此時使用scrollBy(30,0)就是向右滾動到40像素處吸祟;而假如此時使用scrollBy(-20瑟慈,0)就是滾動到-10像素的位置,即向左滾動到-10像素處)

我們要明白滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規(guī)則屋匕,這兩個屬性可以通過getScrollX和getScrollY方法得到葛碧。在滑動過程中,mScrollX的值總是等于View的左邊緣和View內容左邊緣在水平方向的距離过吻,而mScrollY的值總是等于View上邊緣和View內容上邊緣在豎直方向的距離吹埠。View邊緣指View的位置,由4個頂點組成疮装,而View內容邊緣是指View中內容的邊緣缘琅,scrollTo和scrollBy只能改變

四、View的事件分發(fā)機制

4.1 點擊事件的傳遞規(guī)則

點擊事件廓推,即MotionEvent刷袍,所謂點擊事件的事件分發(fā),就是當一個MotionEvent產生了以后樊展,系統需要把這個事件傳遞給一個具體的View呻纹,而這個傳遞的過程就是分發(fā)的過程,點擊事件的分發(fā)過程有三個很重要的方法來完成专缠,dispatchTouchEvent雷酪、onInterceptTouchEvent和onTouchEvent。

 /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
 public boolean dispatchTouchEvent(MotionEvent event)

dispatchTouchEvent:用來進行事件的分發(fā)涝婉,如果事件能夠傳遞給當前View哥力,那么此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件吩跋。

 /**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
    
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
 public boolean onInterceptTouchEvent(MotionEvent ev) 

onInterceptTouchEvent: 在dispatchTouchEvent內部調用寞射,用來判斷是否攔截某個事件,如果當前View攔截了某個事件锌钮,那么在同一方法序列中桥温,此方法不會被再次調用,返回結果表示是否攔截當前事件

/**
     * Implement this method to handle touch screen motion events.
    
      * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
public boolean onTouchEvent(MotionEvent event)

onTouchEvent:在dispatchTouchEvent中調用梁丘,用來處理點擊事件侵浸,返回結果表示是否消耗當前事件,如果不消耗氛谜,則在同一事件序列中通惫,當前View無法再次接收到事件。

這三個方法的關系我們先用一段偽代碼表示一下:

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

對于一個根ViewGroup來說混蔼,點擊事件產生后,首先會傳遞給它珊燎,這時它的dispatchTouchEvent就會被調用惭嚣,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理悔政,即它的onTouchEvent方法就會被調用晚吞;如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當前事件谋国,這時當前事件就會繼續(xù)傳遞給它的子元素槽地,接著子元素的dispatchTouchEvent方法就會被調用,如此反復直到事件被最終處理芦瘾。

當一個View需要處理事件時捌蚊,如果它設置了OnTouchListener,那么onTouchListener中的onTouch方法會被回調近弟。這時事件如何處理還要看onTouch的返回值缅糟,如果返回false,則當前View的onTouchEvent方法會被調用祷愉;如果返回true窗宦,那么onTouchEvent方法將不會被調用。由此可見二鳄,給View設置的OnTouchListener赴涵,其優(yōu)先級比onTouchEvent要高。

當一個點擊事件產生后订讼,它的傳遞過程遵循如下順序:Activity --> Window --> View髓窜,即事件總是先傳遞給Activity,Activity再傳遞給Window欺殿,最后Window再傳遞給頂級View纱烘,頂級View接收到事件后杨拐,就會按照事件分發(fā)機制去分發(fā)事件±奚叮考慮一種情況哄陶,如果一個View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會被調用哺壶,以此類推屋吨。如果所有的元素都不處理這個事件,那么這個事件將會最終傳遞給Activity處理山宾,即Activity的onTouchEvent方法會被調用至扰。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市资锰,隨后出現的幾起案子敢课,更是在濱河造成了極大的恐慌,老刑警劉巖绷杜,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件直秆,死亡現場離奇詭異,居然都是意外死亡鞭盟,警方通過查閱死者的電腦和手機圾结,發(fā)現死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來齿诉,“玉大人筝野,你說我怎么就攤上這事≡辆纾” “怎么了歇竟?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抵恋。 經常有香客問我途蒋,道長,這世上最難降的妖魔是什么馋记? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任号坡,我火速辦了婚禮,結果婚禮上梯醒,老公的妹妹穿的比我還像新娘宽堆。我一直安慰自己,他們只是感情好茸习,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布畜隶。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪籽慢。 梳的紋絲不亂的頭發(fā)上浸遗,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機與錄音箱亿,去河邊找鬼跛锌。 笑死,一個胖子當著我的面吹牛届惋,可吹牛的內容都是我干的髓帽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼脑豹,長吁一口氣:“原來是場噩夢啊……” “哼郑藏!你這毒婦竟也來了?” 一聲冷哼從身側響起瘩欺,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤必盖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俱饿,有當地人在樹林里發(fā)現了一具尸體歌粥,經...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年稍途,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砚婆。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡械拍,死狀恐怖,靈堂內的尸體忽然破棺而出装盯,到底是詐尸還是另有隱情坷虑,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布埂奈,位于F島的核電站迄损,受9級特大地震影響,放射性物質發(fā)生泄漏账磺。R本人自食惡果不足惜芹敌,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垮抗。 院中可真熱鬧氏捞,春花似錦、人聲如沸冒版。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捆等,卻和暖如春滞造,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背栋烤。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工谒养, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人班缎。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓蝴光,卻偏偏與公主長得像,于是被迫代替她去往敵國和親达址。 傳聞我的和親對象是個殘疾皇子蔑祟,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內容