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

學(xué)習(xí)內(nèi)容

  • View 基礎(chǔ)
  • 滑動(dòng)
  • 事件分發(fā)機(jī)制
  • 滑動(dòng)沖突

1. View 基礎(chǔ)知識(shí)

  1. View 定義

    1. View 是 Android 種所有控件的基類,是一種界面層的控件的一種抽象冲秽,代表了一個(gè)控件
    2. ViewGroup 繼承 View,其內(nèi)部包含了許多個(gè)控件,即一組 View
    3. ViewGroup 內(nèi)部是可以有子 View 的,而這個(gè)子 View 同樣還可以是 ViewGroup
  2. View 位置參數(shù)

    1. Android 種蒸痹,坐標(biāo)系的 x 軸和 y 軸的正方向分別是右和下。
    2. View 的位置由其四個(gè)頂點(diǎn)決定呛哟,分別對(duì)應(yīng)四個(gè)屬性:top(左上角縱坐標(biāo))叠荠、left(左上角橫坐標(biāo))、right(右下角橫坐標(biāo))扫责、bottom(右下角縱坐標(biāo))榛鼎,這些坐標(biāo)相對(duì)于父容器來說的。
    3. Android 3.0 以后,加入 x者娱、y抡笼、translationX、translationY黄鳍,其中 x推姻、y是 View 左上角的坐標(biāo),而 translationX际起、translationY 是 View 左上角相對(duì)于父容器的偏移量
  3. MotionEvent 和 TouchSlop

    1. MotionEvent 是手指接觸屏幕后所產(chǎn)生的一系列事件拾碌。
    2. 一般通過 MotionEvent 對(duì)象可以得到點(diǎn)擊事件發(fā)生的 x 和 y 坐標(biāo)
      1. getX / getY:相對(duì)坐標(biāo)
      2. getRawX / getRawY:絕對(duì)坐標(biāo)
    3. TouchSlop 指系統(tǒng)能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離,通過 ViewConfiguration.get(getContext()).getScaledTouchSlop() 方法來獲取街望。
  4. VelocityTracker、GestureDetector 和 Scroller

    1. VelocityTracker

      1. 速度追蹤弟跑,用于追蹤手指在滑動(dòng)過程中的速度
      2. 使用
        VelocityTracker velocityTracker = VelociityTracker.obtain();
        velocityTracker.addMovement(event);
        
        //獲取速度之前按必須先計(jì)算速度灾前,速度指一段時(shí)間內(nèi)手指滑過的像素?cái)?shù)
        //速度 = (終點(diǎn)位置 - 起點(diǎn)位置)/ 時(shí)間段
        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity = (int)velocityTracker.getXVelocity();
        int yVelocity = (int)velocityTracker.getYVelocity();
        
        
        //不再需要使用的時(shí)候,重置并回收內(nèi)存
        velocityTracker.clear();
        velocityTracker.recycler();
        
    2. GestureDetector

      1. 手勢(shì)檢測(cè)孟辑,用于輔助檢測(cè)用戶的單擊哎甲、滑動(dòng)、長(zhǎng)按饲嗽、雙擊等行為炭玫。

      2. 使用

        //創(chuàng)建 GestureDetector 對(duì)象并實(shí)現(xiàn) 指定接口如 OnGestureListener 、 OnDoubleTapListener
        GestureDetector mGestureDetector = new GestureDetector(this);
        mGestureDetector.setIsLongpressEnabled(flase);
        
        //接著接管目標(biāo) View 的 onTouchEvent 方法
        boolean consume = mGestureDetector.onTouchEvent(event);
        return consume;
        
      3. 建議:如果只是監(jiān)聽滑動(dòng)相關(guān)貌虾,建議自己在 onTouchEvent 中實(shí)現(xiàn)吞加,如果要監(jiān)聽雙擊這種行為的話,那么就使用 GestureDetector

    3. Scroller

      1. 彈性滑動(dòng)對(duì)象尽狠,用于實(shí)現(xiàn) View 的彈性滑動(dòng)

      2. 使用

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

View 的滑動(dòng)

  1. 使用 ScrollTo / ScrollBy

    1. 只能改變 View 內(nèi)容的位置而不能改變 View 在布局中的位置
    2. mScrolllX 的值總是等于 View 左邊緣和 View 內(nèi)容左邊緣在水平方向的距離;mScrollY 的值總是等于 View 上邊緣和 View 內(nèi)容上邊緣在豎直方向的距離袄膏,二者單位均為像素践图。
    3. 當(dāng) View 左邊緣在 View 內(nèi)容左邊緣的右邊時(shí),mScrollX 為正值
    4. 當(dāng) View 上邊緣在 View 內(nèi)容上邊緣的下邊時(shí)沉馆,mScrollY 為正值
  2. 使用動(dòng)畫

    1. translationX / translationY 屬性
    2. View 動(dòng)畫
      1. 以上兩種码党,只是移動(dòng) View 的影像,不能改變真正的位置
    3. 屬性動(dòng)畫
      1. 可以改變 View 的參數(shù)
  3. 改變布局參數(shù)

    1. 改變 LayoutParams

    2. 舉例:

      //將一個(gè) Button 右平移100px
      ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)mTbn.getLayoutParams();
      params.leftMargin += 100;
      mBtn.requestLayout();
      //或者  mBtn.setLayoutParams(params);
      
  4. 小結(jié)

    1. ScrollTo / ScrollBy:操作簡(jiǎn)單斥黑,適合對(duì) View 內(nèi)容的滑動(dòng)
    2. 動(dòng)畫(View 動(dòng)畫):操作簡(jiǎn)單揖盘,主要適用于沒有交互的 View 和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果
    3. 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的 View

3. 彈性滑動(dòng)

  1. 具體思想:將一次大的滑動(dòng)分成若干次小的滑動(dòng)心赶,并在一個(gè)時(shí)間段內(nèi)完成扣讼。

  2. 實(shí)現(xiàn)方式

    1. 使用 Scrolller

      當(dāng) View 重繪后會(huì)在 draw 方法中調(diào)用 computeScroll,而 compiteScroll 又會(huì)去向 Scroller 獲取當(dāng)前的 scrollX 和 scrollY缨叫;然后通過 scrollTo 方法實(shí)現(xiàn)滑動(dòng)椭符;接著又調(diào)用 postInvalidate 方法來進(jìn)行第二次重繪荔燎,這一次重繪的過程和第一次重繪一樣,還是會(huì)導(dǎo)致 computeScroll 方法被調(diào)用销钝;后續(xù)同上有咨,如此反復(fù),直到整個(gè)滑動(dòng)過程結(jié)束蒸健。

    2. 通過動(dòng)畫

    3. 使用延時(shí)策略

      1. 核心思想:通過發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果

      2. 方法:

        使用 Handler或 View 的 postDelayed 方法座享,也可以使用線程的 sleep 方法,對(duì)于 postDelayed 方法來說似忧,通過其延時(shí)發(fā)送消息渣叛,然后在消息中進(jìn)行 View 的滑動(dòng)。接連不斷地發(fā)送這種延時(shí)消息盯捌,以此大導(dǎo)彈性滑動(dòng)的效果

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

  1. 核心的三個(gè)方法

    1. dispatchTouchEvent(MotionEvent ev)

      用來進(jìn)行事件的分發(fā)淳衙。返回結(jié)果受當(dāng)前 View 的 onTouchEvent 和 下級(jí) View 的 dispatchTouchEvent 方法的影響,表示是否消耗該事件饺著。

    2. onInterceptTouchEvent(MotionEvent ev)

      在上述方法中調(diào)用箫攀,用來判斷是否攔截某個(gè)事件,如果當(dāng)前View 攔截了某個(gè)事件幼衰,那么在同一個(gè)時(shí)間序列中靴跛,此方法不會(huì)再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件

    3. onTouchEvent(MotionEvent ev)

      在 dispatchTouchEvent 方法中調(diào)用渡嚣,用來處理點(diǎn)擊事件梢睛,返回結(jié)果表示是否消耗事件,如果不消耗严拒,則同一個(gè)事件序列中扬绪,當(dāng)前 View 無(wú)法再次接收到事件

    4. 三者關(guān)系偽代碼表示

      public boolean dispatchTouchEvent(MotionEvent ev){
          boolean consume = false;
          if(onInterceptTouchEvent(ev){
              consume = onTouchEvent(ev);
          }else {
              consume = child.dispatchTouchEvent(ev);
          }
          return consume;
      }
      
      
  2. 事件的傳遞規(guī)則

    1. 對(duì)于一個(gè)根 ViewGroup 來說,點(diǎn)擊事件產(chǎn)生后裤唠,首先傳遞給它挤牛,此時(shí)它的 dispatchTouchEvent 會(huì)被調(diào)用,如果這個(gè) ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要攔截當(dāng)前事件种蘸,接著事件就會(huì)交給這個(gè) ViewGroup 處理墓赴,即調(diào)用它的 onTouchEvent 方法。如果 onInterceptTouchEvent 方法 返回 false航瞭,表示不攔截當(dāng)前事件诫硕,這時(shí)該事件傳遞給它的子元素,接著子元素的 dispatchTouchEvent 方法被調(diào)用刊侯,如此反復(fù)章办。

    2. 傳遞過程遵循如下順序:

      Activity -> Window ( PhoneWindow )-> View (DecorView)

      當(dāng)一個(gè) View 的 onTouchEvent 返回 false,那么會(huì)調(diào)用其父容器的 onTouchEvent ,依此類推藕届。如果所有的元素都不處理這個(gè)事件挪蹭,那么這個(gè)事件將會(huì)最終傳遞給 Activity 處理。

  3. 一些結(jié)論

    1. 同一個(gè)時(shí)間序列指 從手指接觸屏幕的那一刻起休偶,到手指離開屏幕的那一刻結(jié)束梁厉。(down -> [move]* -> up )
    2. 正常情況下,一個(gè)事件序列只能被一個(gè) View 攔截并消耗
    3. 某個(gè) View 一旦決定攔截踏兜,那么這一個(gè)事件序列都只能由它來處理词顾,并且 onInterceptTouchEvent 不會(huì)再被調(diào)用
    4. 某個(gè) View 一旦開始處理事件,如果它不消耗 ACTION_DOWN( onTouchEvent 返回了 false)碱妆,那么同一事件序列中其他事件都不會(huì)再交給它來處理肉盹,事件將重新交給他的父元素處理,即父元素的 onTouchEvent 會(huì)被調(diào)用山橄。
    5. 如果某個(gè) View 不消耗除 ACTION_DOWN 以外的其他事件垮媒,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的 onTouchEvent 并不會(huì)被調(diào)用航棱,并且當(dāng)前 View 可以收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給 Activity 處理
    6. ViewGroup 默認(rèn)不攔截任何事件萌衬,ViewGroup 的 onInterceptTouchEvent 方法默認(rèn)返回 false
    7. View 沒有 onInterceptTouchEvent 方法饮醇,一旦有事件傳遞給它,那么它的 onTouchEvent 方法會(huì)被調(diào)用
    8. 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。
    9. View 的 enable 屬性不影響 o'nTouchEvent 的默認(rèn)返回值
    10. onClick 會(huì)發(fā)生的前提是當(dāng)前 View 是可點(diǎn)擊的歌径,并且它收到了 down 和 up 的事件
    11. 時(shí)間傳遞過程是由外向內(nèi)的毁嗦,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子 View回铛,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預(yù)父元素的事件分發(fā)過程狗准,但是 ACTION_DOWN 事件除外。

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

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

    1. 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致
    2. 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致
    3. 上面兩種情況的嵌套
  2. 滑動(dòng)沖突處理規(guī)則

    1. 滑動(dòng)方向有明顯差異時(shí):根據(jù)特征(水平滑動(dòng)還是豎直滑動(dòng))來決定讓誰(shuí)來攔截事件
    2. 滑動(dòng)方向無(wú)法辨別:根據(jù)業(yè)務(wù)需求來決定讓誰(shuí)來攔截事件
  3. 滑動(dòng)沖突的解決方式

  4. 外部攔截法(推薦)

    1. 指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理茵肃,如果父容器需要此事件就攔截腔长,否則不攔截。

    2. 需要重寫父容器的 onInterceptTouchEvent 方法验残,在內(nèi)部做相應(yīng)的攔截

    3. 典型偽代碼

      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          boolean intercepted = false;
          int x = (int) ev.getX();
          int y = (int) ev.getY();
          switch (ev.getAction()){
              //對(duì)于 ACTION_DOWN 事件捞附,父容器必須返回false,即不攔截,一旦攔截鸟召,那么后續(xù)的 MOVE胆绊、UP 事件都會(huì)直接交由父容器處理。沒法傳遞給子元素
              case MotionEvent.ACTION_DOWN:
                  intercepted = false;
                  break;
              //MOVE 事件根據(jù)需求來決定是否攔截药版,父容器需要?jiǎng)t返回true辑舷,否則返回false
              case MotionEvent.ACTION_MOVE:
                  if (/*父容器需要當(dāng)前點(diǎn)擊事件*/){
                      intercepted = true;
                  }else {
                      intercepted = false;
                  }
                  break;
              //必須返回 false,因?yàn)?UP 事件沒太多意義        
              case MotionEvent.ACTION_UP:
                  intercepted = false;
                  break;
              default:
                  break;
          }
          mLastXIntercept = x;
          mLastYIntercept = y;
          return intercepted;
      }
      
  5. 內(nèi)部攔截法

    1. 指 父容器不攔截任何事件槽片,所有的事件都傳遞給子元素何缓,如果子元素需要此事件就直接消耗掉,否則交由父容器進(jìn)行處理还栓。

    2. 需要 requestDisallowInterceptTouchEvent 方法配合工作碌廓。

    3. 典型偽代碼

      //子元素
      @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {
          int x = (int) ev.getX();
          int y = (int) ev.getY();
      
          switch (ev.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  getParent().requestDisallowInterceptTouchEvent(true);
                  break;
              case MotionEvent.ACTION_MOVE:
                  int deltaX = x - mLastX;
                  int deltaY = y - mLastY;
                  if (/*父容器需要此類點(diǎn)擊事件*/){
                      getParent().requestDisallowInterceptTouchEvent(false);
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  break;
              default:
                  break;
          }
          mLastX = x;
          mLastY = y;
      
          return super.dispatchTouchEvent(ev);
      }
      
      
      //父元素
      //父元素默認(rèn)攔截除了 ACTION_DOWN 外的事件,原因是 ACTION_DOWN 不受 requestDisallowInterceptTouchEvent() 方法的控制
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
          int action = ev.getAction();
          if (action == MotionEvent.ACTION_DOWN){
              return false;
          }else {
              return true;
          }
      }
      
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末剩盒,一起剝皮案震驚了整個(gè)濱河市谷婆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辽聊,老刑警劉巖纪挎,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異跟匆,居然都是意外死亡异袄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門玛臂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烤蜕,“玉大人,你說我怎么就攤上這事迹冤》碛” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵泡徙,是天一觀的道長(zhǎng)橱鹏。 經(jīng)常有香客問我,道長(zhǎng)锋勺,這世上最難降的妖魔是什么蚀瘸? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮庶橱,結(jié)果婚禮上贮勃,老公的妹妹穿的比我還像新娘。我一直安慰自己苏章,他們只是感情好寂嘉,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布奏瞬。 她就那樣靜靜地躺著,像睡著了一般泉孩。 火紅的嫁衣襯著肌膚如雪硼端。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天寓搬,我揣著相機(jī)與錄音珍昨,去河邊找鬼。 笑死句喷,一個(gè)胖子當(dāng)著我的面吹牛镣典,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播唾琼,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼兄春,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了锡溯?” 一聲冷哼從身側(cè)響起赶舆,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祭饭,沒想到半個(gè)月后芜茵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡倡蝙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年夕晓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悠咱。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖征炼,靈堂內(nèi)的尸體忽然破棺而出析既,到底是詐尸還是另有隱情,我是刑警寧澤谆奥,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布眼坏,位于F島的核電站,受9級(jí)特大地震影響酸些,放射性物質(zhì)發(fā)生泄漏宰译。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一魄懂、第九天 我趴在偏房一處隱蔽的房頂上張望沿侈。 院中可真熱鬧,春花似錦市栗、人聲如沸缀拭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蛛淋。三九已至咙好,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間褐荷,已是汗流浹背勾效。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留叛甫,地道東北人层宫。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像合溺,于是被迫代替她去往敵國(guó)和親卒密。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容