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

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

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

1. View 基礎(chǔ)知識

  1. View 定義

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

    1. Android 種,坐標系的 x 軸和 y 軸的正方向分別是右和下。
    2. View 的位置由其四個頂點決定,分別對應(yīng)四個屬性:top(左上角縱坐標)、left(左上角橫坐標)、right(右下角橫坐標)、bottom(右下角縱坐標),這些坐標相對于父容器來說的。
    3. Android 3.0 以后,加入 x、y、translationX、translationY,其中 x、y是 View 左上角的坐標,而 translationX、translationY 是 View 左上角相對于父容器的偏移量
  3. MotionEvent 和 TouchSlop

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

    1. VelocityTracker

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

      1. 手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。

      2. 使用

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

    3. Scroller

      1. 彈性滑動對象,用于實現(xiàn) View 的彈性滑動

      2. 使用

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

View 的滑動

  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)容左邊緣的右邊時,mScrollX 為正值
    4. 當(dāng) View 上邊緣在 View 內(nèi)容上邊緣的下邊時,mScrollY 為正值
  2. 使用動畫

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

    1. 改變 LayoutParams

    2. 舉例:

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

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

3. 彈性滑動

  1. 具體思想:將一次大的滑動分成若干次小的滑動,并在一個時間段內(nèi)完成。

  2. 實現(xiàn)方式

    1. 使用 Scrolller

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

    2. 通過動畫

    3. 使用延時策略

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

      2. 方法:

        使用 Handler或 View 的 postDelayed 方法,也可以使用線程的 sleep 方法,對于 postDelayed 方法來說,通過其延時發(fā)送消息,然后在消息中進行 View 的滑動。接連不斷地發(fā)送這種延時消息,以此大導(dǎo)彈性滑動的效果

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

  1. 核心的三個方法

    1. dispatchTouchEvent(MotionEvent ev)

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

    2. onInterceptTouchEvent(MotionEvent ev)

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

    3. onTouchEvent(MotionEvent ev)

      在 dispatchTouchEvent 方法中調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗事件,如果不消耗,則同一個事件序列中,當(dāng)前 View 無法再次接收到事件

    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. 對于一個根 ViewGroup 來說,點擊事件產(chǎn)生后,首先傳遞給它,此時它的 dispatchTouchEvent 會被調(diào)用,如果這個 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要攔截當(dāng)前事件,接著事件就會交給這個 ViewGroup 處理,即調(diào)用它的 onTouchEvent 方法。如果 onInterceptTouchEvent 方法 返回 false,表示不攔截當(dāng)前事件,這時該事件傳遞給它的子元素,接著子元素的 dispatchTouchEvent 方法被調(diào)用,如此反復(fù)。

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

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

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

  3. 一些結(jié)論

    1. 同一個時間序列指 從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束。(down -> [move]* -> up )
    2. 正常情況下,一個事件序列只能被一個 View 攔截并消耗
    3. 某個 View 一旦決定攔截,那么這一個事件序列都只能由它來處理,并且 onInterceptTouchEvent 不會再被調(diào)用
    4. 某個 View 一旦開始處理事件,如果它不消耗 ACTION_DOWN( onTouchEvent 返回了 false),那么同一事件序列中其他事件都不會再交給它來處理,事件將重新交給他的父元素處理,即父元素的 onTouchEvent 會被調(diào)用。
    5. 如果某個 View 不消耗除 ACTION_DOWN 以外的其他事件,那么這個點擊事件會消失,此時父元素的 onTouchEvent 并不會被調(diào)用,并且當(dāng)前 View 可以收到后續(xù)事件,最終這些消失的點擊事件會傳遞給 Activity 處理
    6. ViewGroup 默認不攔截任何事件,ViewGroup 的 onInterceptTouchEvent 方法默認返回 false
    7. View 沒有 onInterceptTouchEvent 方法,一旦有事件傳遞給它,那么它的 onTouchEvent 方法會被調(diào)用
    8. View 的 onTouchEvent 方法默認消耗事件(返回 true ),除非他是不可點擊的(clickable 和 longClickable 同時為 false)。View 的 longClickable 屬性默認都為 false,clickable 屬性分情況,Button 默認為 true,TextView 默認為false。
    9. View 的 enable 屬性不影響 o'nTouchEvent 的默認返回值
    10. onClick 會發(fā)生的前提是當(dāng)前 View 是可點擊的,并且它收到了 down 和 up 的事件
    11. 時間傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子 View,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是 ACTION_DOWN 事件除外。

5. View 的滑動沖突

  1. 常見的滑動沖突場景

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

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

  4. 外部攔截法(推薦)

    1. 指點擊事件都先經(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()){
              //對于 ACTION_DOWN 事件,父容器必須返回false,即不攔截,一旦攔截,那么后續(xù)的 MOVE、UP 事件都會直接交由父容器處理。沒法傳遞給子元素
              case MotionEvent.ACTION_DOWN:
                  intercepted = false;
                  break;
              //MOVE 事件根據(jù)需求來決定是否攔截,父容器需要則返回true,否則返回false
              case MotionEvent.ACTION_MOVE:
                  if (/*父容器需要當(dāng)前點擊事件*/){
                      intercepted = true;
                  }else {
                      intercepted = false;
                  }
                  break;
              //必須返回 false,因為 UP 事件沒太多意義        
              case MotionEvent.ACTION_UP:
                  intercepted = false;
                  break;
              default:
                  break;
          }
          mLastXIntercept = x;
          mLastYIntercept = y;
          return intercepted;
      }
      
  5. 內(nèi)部攔截法

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

    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 (/*父容器需要此類點擊事件*/){
                      getParent().requestDisallowInterceptTouchEvent(false);
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  break;
              default:
                  break;
          }
          mLastX = x;
          mLastY = y;
      
          return super.dispatchTouchEvent(ev);
      }
      
      
      //父元素
      //父元素默認攔截除了 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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容