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

第三章 View的事件體系

學(xué)習(xí)清單:

  • View的事件體系

    • View的位置參數(shù)

    • View的觸控參數(shù)

    • View的滑動

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

    • 點(diǎn)擊事件傳遞規(guī)則
  • View的滑動沖突

    • 產(chǎn)生原因

    • 常見的滑動沖突場景

    • 處理規(guī)則

    • 解決方案


簡介

在Android的世界中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個View控件

從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個界面的控件形成了一個樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測量與繪制下層的控件,并傳遞交互事件

在每棵控件樹的頂部都存在著一個ViewParent對象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對整個視圖進(jìn)行整體控制

image-20200605111552941.png

一. View 的事件體系

1. View的位置參數(shù)

a. Q:如何確定一個View的位置?

A: View的位置主要通過它的四個頂點(diǎn)來決定, 分別是:

  • top: 左上角縱坐標(biāo)

  • left: 左上角橫坐標(biāo)

  • right: 右下角橫坐標(biāo)

  • bottom: 右下角縱坐標(biāo)

    image-20200605112730079.png

b. View的寬高和坐標(biāo)的關(guān)系:

width = right - left;
height = bottom - top;

 // 獲取這四個參數(shù)的方法
 Left = getLeft();
 Right = getRight();
 Top = getTop()
 Bottom = getBottom();

c. 從Android3.0開始, View增加了額外的四個參數(shù): x, y, translationX 和 translationY, 其中x 和 y 是View左上角坐標(biāo), 而translationX 和 translationY 是View左上角相對于父容器的偏移量, 這幾個參數(shù)也是相對于父容器的坐標(biāo), 關(guān)系如下圖:

image-20200605113942812.png
  • 換算關(guān)系: x = left + translationX, y = top + translationY
  • X由此可見, x和left不同體現(xiàn)在:left是View的初始坐標(biāo), 在繪制完畢后就不會再改變;而x是View偏移后的實(shí)時坐標(biāo), 是實(shí)際坐標(biāo). y和top的區(qū)別同理

2. View的觸控參數(shù)

a. MotionEven 和 TouchSlop:
  • MotionEven的觸摸事件:

    • ACTION_DOWN : 手指放接觸屏幕

    • ACTION_MOVE : 手指在屏幕上移動

    • ACTION_UP : 手指從屏幕上松開的一瞬間

    正常情況下, 一次手指觸碰屏幕的行為可能觸發(fā)一系列點(diǎn)擊事件, 如:

    • 點(diǎn)擊屏幕后立刻松開: DOWN -> UP;
    • 點(diǎn)擊屏幕后一會再松開: DOWN -> MOVE -> ... -> MOVE -> UP;
    • 通過MotionEven對象我們可以得到事件發(fā)生的 xy 坐標(biāo):

      • getX() / getY(): 返回相對于當(dāng)前View左上角的 x, y 坐標(biāo)

      • getRawX() / getRawY(): 返回相對于手機(jī)屏幕左上角的 x , y 坐標(biāo)


  • TouchSlop的使用:

    • TouchSlop: 是系統(tǒng)所能識別出的滑動最小距離, 是一個常量, 不同的設(shè)備上這個值可能是不同的

    • 通過 ViewConfiguration.get(getContext()).getScaledTouchSlop()可以獲得這個常量

      使用建議:

      • 通過TouchSlop, 可以對用戶的一些操作進(jìn)行過濾, 提高用戶使用體驗(yàn)

b. VelocityTracker 和 GestureDetector:
  • VelocityTracker速度追蹤:

    • 功能: 用于追蹤手指在滑動過程中的速度, 包括水平和豎直方向的速度

    • 使用:

      • 在View的onTouchEvent()方法中追蹤當(dāng)前單擊事件的速度
       VelocityTracker velocityTracker = VelocityTracker.obtain();
       velocityTracker.addMovement(event);
      
      • 獲取滑動速度
       int xVelocity = (int) velocityTracker.getXVelocity();
       int yVelocity = (int) velocityTracker.getYVelocity();
      

      注意:

      • 獲取速度之前必須調(diào)用computerCurrentVelocity方法
      • 獲取到的速度指的是在規(guī)定時間內(nèi)劃過的像素?cái)?shù)
      • 獲取到的速度可以為負(fù)數(shù), 從右向左 / 從下向上 獲取到的就是負(fù)數(shù)
      • 速度的公式 : 速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時間段
      • 不使用VelocityTracker的時候需要調(diào)用clear方法來重置并回收內(nèi)存, recycle方法重新調(diào)用

  • GestureDetector手勢檢測

    • 功能: 用于檢測用戶的單擊, 滑動, 長按, 雙擊等行為

    • 使用:

      • 創(chuàng)建一個GestureDetector對象

      • 根據(jù)不同的需求實(shí)現(xiàn)OnGestureListener接口或OnDoubleTapListener接口

        方法名 描述 所屬接口
        onDown 手指輕觸屏幕的一瞬間, 由1個ACTION_DOWN觸發(fā) OnGestureListener
        onShowPress 手指輕觸屏幕尚未松開或移動 OnGestureListener
        onSingleTapUp 手指輕觸屏幕后松開, 伴隨1個ACTION_UP觸發(fā) OnGestureListener
        onScroll 手指輕觸屏幕并拖動 OnGestureListener
        onLongPress 長按屏幕不放 OnGestureListener
        onFling 觸摸屏幕快速滑動后松開 OnGestureListener
        onDoubleTap 雙擊, 不能和onSingleTapConfirmed共存 OnDoubleTapLinstener
        onSingleTapConfirmed 嚴(yán)格單擊行為, 指不能是雙擊中的一次單擊 OnDoubleTapLinstener
        onDoubleTapEvent 發(fā)生了雙擊行為, 在雙擊期間移動也會觸發(fā) OnDoubleTapLinstener
    • 注意: 在實(shí)際開發(fā)中, 如果需要監(jiān)聽雙擊事件, 則使用GestureDetector, 否則可以在View的onTouchEvent方法中實(shí)現(xiàn)


3. View的滑動

a. 通過View本身的scrollTo / scrollBy實(shí)現(xiàn):
  • 方法:

    • scrollTo: 基于所傳遞參數(shù)的絕對滑動

    • scrollBy: 實(shí)際上是通過調(diào)用scroolTo方法實(shí)現(xiàn), 傳遞的是偏移量

  • 注意: 通過scrollTo / scrollBy只能改變View的內(nèi)容, 不能改變View在當(dāng)前布局中的位置


b. 使用動畫
  • 使用xml文件的方式:

    • xml代碼:
     <?xml version="1.0" encoding="utf-8"?>
     <set xmlns:android="http://schemas.android.com/apk/res/android"
      android:fillAfter="true"
      >
      <translate
      android:fromXDelta="0"
      android:fromYDelta="0"
      android:toXDelta="100"
      android:toYDelta="100"
      android:duration="2000"
      />
     </set>
    
    • java代碼:
     Animation animation = AnimationUtils.loadAnimation(this, R.anim.translate);
     view.startAnimation(animation);
    

    推薦閱讀: Animation補(bǔ)間動畫

  • 注意: 這種動畫只能改變View的內(nèi)容所在的位置, 真身仍在原來的位置

    關(guān)于動畫的內(nèi)容, 會在第7章詳細(xì)說明

c. 改變布局參數(shù)
  • 說明: 通過改變LayoutParams來實(shí)現(xiàn), 或例如在Button旁邊放置一個View, 通過改變這個View的大小來實(shí)現(xiàn)

  • 注意: 在修改了LayoutParams后記得使用requestLayout()方法更新

d. 三種方式的優(yōu)缺點(diǎn):
  • scrollTo / scrollBy : 操作簡單, 適合對View內(nèi)容的滑動;

  • 動畫: 操作簡單, 主要適用于不與用戶交互, 復(fù)雜的動畫效果

  • 改變布局參數(shù): 操作稍微復(fù)雜, 但適用與有交互的View


4.View的彈性滑動

View的滑動效果顯得太過生硬, Android中還提供了許多彈性滑動的方法, 下面記錄一下Android中的彈性滑動

a. 使用Scroller
  • 使用:

    • 創(chuàng)建Scroller的實(shí)例

    • 調(diào)用startScroll()方法來初始化滾動數(shù)據(jù)并刷新界面

    • 重寫computeScroll()方法,并在其內(nèi)部完成平滑滾動的邏輯

  • 慣用代碼:

     ?
     private void smoothScrollTo(int dstX, int dstY) {
      int scrollX = getScrollX();//View的左邊緣到其內(nèi)容左邊緣的距離
      int scrollY = getScrollY();//View的上邊緣到其內(nèi)容上邊緣的距離
      int deltaX = dstX - scrollX;//x方向滑動的位移量
      int deltaY = dstY - scrollY;//y方向滑動的位移量
      scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動
      invalidate(); //刷新界面
     }
     ?
     @Override//計(jì)算一段時間間隔內(nèi)偏移的距離,并返回是否滾動結(jié)束的標(biāo)記
     public void computeScroll() {
      if (scroller.computeScrollOffset()) { 
      scrollTo(scroller.getCurrX(), scroller.getCurY());
      postInvalidate();//通過不斷的重繪不斷的調(diào)用computeScroll方法
      }
     }
    

    其中startScroll源碼如下,可見它并沒有進(jìn)行實(shí)際的滑動操作,而是通過后續(xù)invalidate()方法去做滑動動作

      public void startScroll(int startX,int startY,int dx,int dy,int duration){
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;//滑動時間
        mStartTime = AnimationUtils.currentAminationTimeMills();//開始時間
        mStartX = startX;//滑動起點(diǎn)
        mStartY = startY;//滑動起點(diǎn)
        mFinalX = startX + dx;//滑動終點(diǎn)
        mFinalY = startY + dy;//滑動終點(diǎn)
        mDeltaX = dx;//滑動距離
        mDeltaY = dy;//滑動距離
        mDurationReciprocal = 1.0f / (float)mDuration;
      }
    

    具體過程:在MotionEvent.ACTION_UP事件觸發(fā)時調(diào)用startScroll方法->馬上調(diào)用invalidate/postInvalidate方法->會請求View重繪,導(dǎo)致View.draw方法被執(zhí)行->會調(diào)用View.computeScroll方法,此方法是空實(shí)現(xiàn),需要自己處理邏輯。具體邏輯是:先判斷computeScrollOffset,若為true(表示滾動未結(jié)束),則執(zhí)行scrollTo方法,它會再次調(diào)用postInvalidate,如此反復(fù)執(zhí)行,直到返回值為false。如圖所示:

image-20200609142617908.png
  • 原理: 原理:Scroll的computeScrollOffset()根據(jù)時間的流逝動態(tài)計(jì)算一小段時間里View滑動的距離,并得到當(dāng)前View位置,再通過scrollTo繼續(xù)滑動。即把一次滑動拆分成無數(shù)次小距離滑動從而實(shí)現(xiàn)彈性滑動。

b. 使用動畫:

動畫本身就是一種漸近的過程,故可通過動畫來實(shí)現(xiàn)彈性滑動

  • 代碼:

     ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
    

c. 使用延時策略:
  • 描述: 通過Handler / View的postDelayed方法發(fā)送一系列延時消息從而打到一種漸進(jìn)式的效果, 也可以用線程的sleep方法

  • 注意: 對彈性滑動完成總時間有精確要求的使用場景下, 使用延時策略是一個不太合適的選擇


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

事件分發(fā)機(jī)制是View的核心知識點(diǎn), 通過學(xué)習(xí)事件分發(fā)機(jī)制可以解決滑動沖突難題, 鞏固我們對View的掌握

1. 點(diǎn)擊事件傳遞規(guī)則

a. 事件分發(fā)本質(zhì):

  • 就是對MotionEvent事件分發(fā)的過程。即當(dāng)一個MotionEvent產(chǎn)生了以后,系統(tǒng)需要將這個點(diǎn)擊事件傳遞到一個具體的View上

  • 傳遞順序:

    • Activity(Window) -> ViewGroup -> View

    補(bǔ)充: 如果所有元素都不處理這個事件, 那么這個事件最終會由Activity處理, 即Activity的onTouchEvent方法會被調(diào)用

image-20200609165829185.png

b. 核心方法:

  • public boolean dispatchTouchEvent(MotionEvent ev):

    用于進(jìn)行事件的分發(fā), 如果事件能傳遞給當(dāng)前View, 則此方法一定調(diào)用. 返回結(jié)果受到當(dāng)前View的onTouchEvent和下級dispatchTouchEvent影響, 表示是否消耗當(dāng)前事件

  • public boolean onInterceptTouchEvent(MotionEvent event):

    dispatchTouchEvent方法中調(diào)用, 用于判斷是否攔截當(dāng)前事件, 如果當(dāng)前View攔截了某個事件, 則同一個任務(wù)序列中此方法不會再被調(diào)用(只有ViewGroup有這個方法)

  • public boolean onTouchEvent(MotionEvent event):

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

  • 補(bǔ)充閱讀: Android事件分發(fā)機(jī)制(源碼)


三. View的滑動沖突

a. 產(chǎn)生原因:
  • 一般情況下,在一個界面里存在內(nèi)外兩層可同時滑動的情況時,會出現(xiàn)滑動沖突現(xiàn)象
b. 常見的滑動沖突場景:
  1. 外部滑動方向和內(nèi)部滑動方向不一致;

  2. 外部滑動方向和內(nèi)部滑動方向一致;

  3. 上述兩種情況的嵌套;

c. 處理規(guī)則:
  • 對于場景一: 左右滑動時, 讓外部View攔截事件. 上下滑動時, 讓內(nèi)部View攔截事件

  • 對于場景二: 根據(jù)相應(yīng)的業(yè)務(wù)情景做出相應(yīng)的操作

  • 對于場景三: 將組合問題根據(jù)場景拆分成若干個小問題, 逐一解決

Q: 如何判斷是左右滑動還是上下滑動:

  • 根據(jù)滑動路徑與水平方向上的夾角
  • 根據(jù)水平和豎直方向上的速度差
  • 根據(jù)水平和豎直方向上的距離差
d. 解決方案:
  • 外部攔截法:

    • 含義: 先經(jīng)過父容器, 如果需要就攔截, 不需要再分發(fā)到子View

    • 方法: 重寫父容器的onInterceptTouchEvent方法, 在內(nèi)部做相應(yīng)的攔截

    public boolean onInterceptTouchEvent(MotionEvent event) {
            boolean intercepted = false;
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                //對于ACTION_DOWN事件必須返回false,一旦攔截后續(xù)事件將不能傳遞給子View
                case MotionEvent.ACTION_DOWN:
                    intercepted = false;
                    break;
                //對于ACTION_MOVE事件根據(jù)需要決定是否攔截
                case MotionEvent.ACTION_MOVE:
                    if (父容器需要當(dāng)前事件){
                    intercepted = true;
                } else{
                    intercepted = flase;
                    break;
                }
                //對于ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會觸發(fā)
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
                default:
                    break;
            }
    
            mLastXIntercept = x;
            mLastYIntercept = y;
            return intercepted;
    }
    
  • 內(nèi)部攔截法

    • 含義: 父容器不攔截任何事件, 如果子元素不需要就交由父容器處理

    • 方法: 重寫子元素的dispatchTouchEvent方法, 再配合requestDisallowInterceptTouchEvent方法,

   public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                // parent.requestDisallowInterceptTouchEvent()可以理解為:
                // 告訴(request)父容器(parent)
                // 不再(disallow)攔截(intercept)觸摸事件(touchEvent)嗎(boolean)
                // 當(dāng)requestDisallowInterceptTouchEvent(ture)時
                // 父容器不再攔截接下來的一系列事件
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此類點(diǎn)擊事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父View需要重寫onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.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)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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