Try   HackMD

Chain - 責任鏈 模式

Overview of Content

如有引用參考請詳註出處,感謝

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

這是資料結構 Array 單向鏈結 的一種應用,串連多個物件,使其都有機會處理請求

Chain 使用場景

  • 多個物件 1. 有順序的處理、2. 同一請求

  • 具體要由哪個物件處理可由 運行時動態決定

Chain 責任鏈 - 分類

  • 責任鏈有兩種類型,如下表

    責任鏈類型 說明
    純 - 責任鏈 需求可被責任鏈消費
    不純 - 責任鏈 責任鏈不能處理需求

Chain 定義 & Chain UML

  • Chain 定義

    多個物件都有機會處理請求,避免請求者(request)、接收處理者(handle)耦合;將對象鏈成一條鏈,並沿著鏈傳遞直到有處理者

    • 處理的邏輯重點在 鏈上
  • Chain UML 有分為兩種:分別都有兩個行為 請求、處理

    1. 合併 請求、處理:這裡我們主要是關注 Linked 的處理

      類別 功能
      LinkedItem (鏈、抽象) 其內部有 0 ~ 1 個 LinkedItem,判斷是否讓下一個 LinkedItem 處理;這裡 有 2 個選擇,不是 處理,就是 往下傳遞
      ConcreateA、B (實作) 處理使用者的請求 (請求、處理一起)
      • LinkedItem 有三個責任

        1. 對外開放一個請求處理入口( handleRequest

        2. 設定鏈的方式 (setNext)

        3. 具體的請求者的抽象方法

          • 抽象定義能處理的級別

          • 抽象定義每個子類處理的方式

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

    2. 分離 請求、處理:我們在基礎的責任鏈上將 請求處理 分開

      • 這裡的重點是使用 依賴倒置 概念,將 請求處理 抽象化
      類別 功能
      LinkedItem (鏈、抽象) 其內部有 0~1 個 LinkedItem,判斷是否讓下一個 LinkedItem 處理;這裡 有 2 個選擇,不是 1.處理,就是 2.往下傳遞
      ConcreateLinkedA、B(實作) 處理使用者的請求
      Request (抽象) 抽象化共同處理的請求
      ConcreateRequestA、B(實作) 定義請求的詳細規則

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

Chain - 優缺點

  • Chain 設計優點

    • 自由的決定要由何處開始處理這個任務

    • 符合單一職責:一個類只負責它自身要處理的有範圍的邏輯

    • 符合迪米特原則:不用知道別的類如何處理,只須關注自己的部份,其他交給父類處理

    • 請求者(Request)與 處理者(Handler)解偶

      • 處理者不用知道請求的全貌,只須關注部份(關注自己能不能處理、處理方式)

      • 請求者也無須知道是哪個類處理的,只須關注結果即可

  • Chain 設計缺點

    • 搜尋速度較慢(如 Linked List 的特性)

      可以 setNext 時設置一個閥值,避免無意識的破壞調系統性能(不好查)

    • 採用遞歸方式運作,也就導致了複雜度增加、可讀性降低

Chain 實作

Chain 標準 - 不分離請求

  • 現在假設在申報出國出差的經費,經費需要經過不同主管批省

    1. 批省要有順序

    2. 每個主管的額度也不相同

  1. LinkedItem (抽象)1. 定義相同行為,並且內部有 2. 0 ~ 1 個相同的抽象成員 (指向 next 處理對象)

    • 這裡要特別注意,結束條件:如果沒有設定結束條件,會導致 Function 堆疊超出 (StackOverflow 錯誤)
    ​​​​// LinkedItem (抽象) ​​​​public abstract class LinkedItem { ​​​​ private final String handlePersonName; ​​​​ public LinkedItem() { ​​​​ // 簡單定義處理者 ​​​​ this.handlePersonName = getClass().getSimpleName(); ​​​​ } ​​​​ public LinkedItem next; ​​​​ public final boolean start(int money) { ​​​​ boolean canHandle = handle(money); ​​​​ if(!canHandle) { ​​​​ System.out.println(handlePersonName + " cannot pay."); ​​​​ // 將處理傳遞給下一個對象 ​​​​ // 結束條件 next == null ​​​​ return next != null && next.start(money); ​​​​ } else { ​​​​ System.out.println(handlePersonName + " handle pay: " + money); ​​​​ } ​​​​ return true; ​​​​ } ​​​​ // 1. 為相同行為定義抽象 ​​​​ protected abstract boolean handle(int money); ​​​​}
    • 從這裡可以看出 模板設計的影子 (固定調用抽象 handle 方法)
  2. ConstractA、B:定義實體處理的子類,並在這裡 由各個子類自行判斷是否處理,在這裡返回的 結果將會決定 Linked 是否繼續往下執行

    ​​​​// ConstractA、B ​​​​public class Supervisor extends LinkedItem { ​​​​ protected static final int MONEY_LIMIT = 1_000; ​​​​ @Override ​​​​ protected boolean handle(int money) { ​​​​ return money <= MONEY_LIMIT; ​​​​ } ​​​​} ​​​​// ----------------------------------------------------- ​​​​public class Manager extends LinkedItem { ​​​​ protected static final int MONEY_LIMIT = 10_000; ​​​​ @Override ​​​​ protected boolean handle(int money) { ​​​​ return money <= MONEY_LIMIT; ​​​​ } ​​​​} ​​​​// ----------------------------------------------------- ​​​​public class CEO extends LinkedItem { ​​​​ protected static final int MONEY_LIMIT = 100_000; ​​​​ @Override ​​​​ protected boolean handle(int money) { ​​​​ return money <= MONEY_LIMIT; ​​​​ } ​​​​}
  • User 使用:使用者可以自行串接處理的順序,並決定啟動時機

    ​​​​// User ​​​​public class Main { ​​​​ public static void main(String[] args) { ​​​​ // 創建實際處理的子類 ​​​​ LinkedItem ceo = new CEO(); ​​​​ LinkedItem manager = new Manager(); ​​​​ LinkedItem supervisor = new Supervisor(); ​​​​ // User 自行串接處理順序 ​​​​ supervisor.next = manager; ​​​​ manager.next = ceo; ​​​​ System.out.println("Linked result: " + supervisor.start(1_000)); ​​​​ System.out.println("--------------------------------\n"); ​​​​ System.out.println("Linked result: " + supervisor.start(10_000)); ​​​​ System.out.println("--------------------------------\n"); ​​​​ System.out.println("Linked result: " + supervisor.start(100_000)); ​​​​ System.out.println("--------------------------------\n"); ​​​​ System.out.println("Linked result: " + supervisor.start(1_000_000)); ​​​​ } ​​​​}
    • final 關鍵字:

      使用 Java 類加載的特性,定義 final 變量 最慢 必須在建構函數中定義

    實作

Chain 標準 - 分離請求

  • 現在除了上面的需求外還有新增一個需求:需要填入申請者的資料 (申請者名稱、Project 名稱、花費)
  1. Request:定義新增的 Request 抽象類

    ​​​​public interface Request { ​​​​ String getRequestName(); ​​​​ String projectName(); ​​​​ int cost(); ​​​​}
  2. ConcreateRequestA、B:定義具體的 Request 處理細節

    ​​​​public class Alien implements Request { ​​​​ @Override ​​​​ public String getRequestName() { ​​​​ return "Alien"; ​​​​ } ​​​​ @Override ​​​​ public String projectName() { ​​​​ return "Sky"; ​​​​ } ​​​​ @Override ​​​​ public int cost() { ​​​​ return 1_000; ​​​​ } ​​​​} ​​​​// ----------------------------------------------- ​​​​public class Pan implements Request { ​​​​ @Override ​​​​ public String getRequestName() { ​​​​ return "Pan"; ​​​​ } ​​​​ @Override ​​​​ public String projectName() { ​​​​ return "Pandora"; ​​​​ } ​​​​ @Override ​​​​ public int cost() { ​​​​ return 10_000; ​​​​ } ​​​​} ​​​​// ----------------------------------------------- ​​​​public class Shanks implements Request { ​​​​ @Override ​​​​ public String getRequestName() { ​​​​ return "Shanks"; ​​​​ } ​​​​ @Override ​​​​ public String projectName() { ​​​​ return "Comics"; ​​​​ } ​​​​ @Override ​​​​ public int cost() { ​​​​ return 100_000; ​​​​ } ​​​​}
  3. LinkedItem 抽象:也就是處理類的抽象,主要修改接收參數,LinkedItem 抽象 參數依賴抽象 Request

    ​​​​public abstract class LinkedItem { ​​​​ private final String handlePersonName; ​​​​ public LinkedItem() { ​​​​ this.handlePersonName = getClass().getSimpleName(); ​​​​ } ​​​​ public LinkedItem next; ​​​​ // 主要修改接收參數,讓參數依賴抽象 Request ​​​​ public final boolean start(Request request) { ​​​​ // 獲取具體花費 ​​​​ int cost = request.cost(); ​​​​ boolean canHandle = handle(cost); ​​​​ if(!canHandle) { ​​​​ System.out.println(handlePersonName + " cannot pay."); ​​​​ return next != null && next.start(request); ​​​​ } else { ​​​​ // 獲取 Request name、projectName ​​​​ System.out.println("Person: " + request.getRequestName() + ", project" + request.projectName()); ​​​​ System.out.println(handlePersonName + " handle pay: " + cost); ​​​​ } ​​​​ return true; ​​​​ } ​​​​ protected abstract boolean handle(int money); ​​​​}
  4. ConcreateLinkedA、B:具體實現抽象 Linked 細節,同上個範例,沒有做修改

  • User 使用:這裡較不一樣的是使用者必須自訂 Request 需求

    ​​​​public class Main { ​​​​ public static void main(String[] args) { ​​​​ LinkedItem ceo = new CEO(); ​​​​ LinkedItem manager = new Manager(); ​​​​ LinkedItem supervisor = new Supervisor(); ​​​​ supervisor.next = manager; ​​​​ manager.next = ceo; ​​​​ System.out.println("Linked result: " + supervisor.start(new Alien())); ​​​​ System.out.println("--------------------------------\n"); ​​​​ System.out.println("Linked result: " + supervisor.start(new Pan())); ​​​​ System.out.println("--------------------------------\n"); ​​​​ System.out.println("Linked result: " + supervisor.start(new Shanks())); ​​​​ } ​​​​}
    • 這裡可以注意到一點,使用者(高層模塊)通常會使用類一個封裝過得類來進行請求,而不是自己串接任務鏈

      可以使用創建類型:像是 FactoryBuilder 設計都可以

    實作

Android Source

Android View 的傳遞事件就是使用 Linked 模式,可以參考另一篇 View 事件分發

透過 dispatchTouchEvent 方法來迭代

ViewGroup 事件分發 - dispatchTouchEvent

  • 這裡我們直接從 ViewGroup 接收的 dispatchTouchEvent 方法開始分析

    簡單來說,都是傳遞的事件會從 PhoneWindow 的 DecorView (ViewGroup) 往下傳遞事件,傳遞方式是使用 遞迴呼叫 (DFS)

    1. 清理當前 View 的事件:如果是 ACTION_DOWN 事件,代表它是一個新的事件,需要清理當前 ViewGroup 的一些設定

      關注變數 當前數值
      actionMasked MotionEvent.ACTION_DOWN
      ​​​​​​​​// ViewGroup.java ​​​​​​​​@Override ​​​​​​​​public boolean dispatchTouchEvent(MotionEvent ev) { ​​​​​​​​ ... ​​​​​​​​ boolean handled = false; ​​​​​​​​ if (onFilterTouchEventForSecurity(ev)) { ​​​​​​​​ final int action = ev.getAction(); ​​​​​​​​ final int actionMasked = action & MotionEvent.ACTION_MASK; ​​​​​​​​ // Handle an initial down. ​​​​​​​​ if (actionMasked == MotionEvent.ACTION_DOWN) { ​​​​​​​​ // 在新事件開始時,先處理上一個事件 (透過 Cancel、Clear) ​​​​​​​​ cancelAndClearTouchTargets(ev); ​​​​​​​​ resetTouchState(); ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​}
      • cancelAndClearTouchTargets 方法:透過 TouchTarget Linked 將 cancel 事件傳遞給所有 view

        ​​​​​​​​​​​​// ViewGroup.java ​​​​​​​​​​​​private void cancelAndClearTouchTargets(MotionEvent event) { ​​​​​​​​​​​​ if (mFirstTouchTarget != null) { ​​​​​​​​​​​​ boolean syntheticEvent = false; ​​​​​​​​​​​​ if (event == null) { ​​​​​​​​​​​​ final long now = SystemClock.uptimeMillis(); ​​​​​​​​​​​​ event = MotionEvent.obtain(now, now, ​​​​​​​​​​​​ MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); ​​​​​​​​​​​​ event.setSource(InputDevice.SOURCE_TOUCHSCREEN); ​​​​​​​​​​​​ syntheticEvent = true; ​​​​​​​​​​​​ } ​​​​​​​​​​​​ for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { ​​​​​​​​​​​​ resetCancelNextUpFlag(target.child); ​​​​​​​​​​​​ // 第二個參數 true, 代表 cacel 事件 ​​​​​​​​​​​​ // 傳遞所有 view cancel 事件 ​​​​​​​​​​​​ dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ clearTouchTargets(); ​​​​​​​​​​​​ if (syntheticEvent) { ​​​​​​​​​​​​ event.recycle(); ​​​​​​​​​​​​ } ​​​​​​​​​​​​ } ​​​​​​​​​​​​}
      • resetTouchState 方法:清除 FLAG_DISALLOW_INTERCEPT FLAG:不同意 ViewGroup 中斷

        ​​​​​​​​​​​​// ViewGroup.java ​​​​​​​​​​​​private TouchTarget mFirstTouchTarget; ​​​​​​​​​​​​private void resetTouchState() { ​​​​​​​​​​​​ clearTouchTargets(); ​​​​​​​​​​​​ resetCancelNextUpFlag(this); ​​​​​​​​​​​​ mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; ​​​​​​​​​​​​ mNestedScrollAxes = SCROLL_AXIS_NONE; ​​​​​​​​​​​​} ​​​​​​​​​​​​// 透過該方法將 mFirstTouchTarget 設置為 null ​​​​​​​​​​​​private void clearTouchTargets() { ​​​​​​​​​​​​ TouchTarget target = mFirstTouchTarget; ​​​​​​​​​​​​ if (target != null) { ​​​​​​​​​​​​ do { ​​​​​​​​​​​​ TouchTarget next = target.next; ​​​​​​​​​​​​ target.recycle(); ​​​​​​​​​​​​ target = next; ​​​​​​​​​​​​ } while (target != null); ​​​​​​​​​​​​ mFirstTouchTarget = null; ​​​​​​​​​​​​ } ​​​​​​​​​​​​}
    2. 檢查該 ViewGroup 中斷事件

      關注變數 當前數值
      actionMasked MotionEvent.ACTION_DOWN
      mFirstTouchTarget null
      ​​​​​​​​// ViewGroup.java ​​​​​​​​@Override ​​​​​​​​public boolean dispatchTouchEvent(MotionEvent ev) { ​​​​​​​​ ... ​​​​​​​​ boolean handled = false; ​​​​​​​​ if (onFilterTouchEventForSecurity(ev)) { ​​​​​​​​ ... 清除上一個點擊事件 ​​​​​​​​ // 檢查中斷 ​​​​​​​​ final boolean intercepted; ​​​​​​​​ if (actionMasked == MotionEvent.ACTION_DOWN // 成立 ​​​​​​​​ || mFirstTouchTarget != null) { // 不成立 ​​​​​​​​ // 判斷 viewgroup flag 是否不允許中斷 ​​​​​​​​ final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; ​​​​​​​​ // 沒有禁止中斷 ​​​​​​​​ if (!disallowIntercept) { ​​​​​​​​ // 呼叫 onInterceptTouchEvent 方法 ​​​​​​​​ intercepted = onInterceptTouchEvent(ev); ​​​​​​​​ ev.setAction(action); // restore action in case it was changed ​​​​​​​​ } else { ​​​​​​​​ intercepted = false; ​​​​​​​​ } ​​​​​​​​ } else { ​​​​​​​​ // 並非下壓事件 & 有 childView 已經在處理事件 ​​​​​​​​ intercepted = true; ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ } ​​​​​​​​}
    3. 檢查事件是否被取消

      ​​​​​​​​// ViewGroup.java ​​​​​​​​@Override ​​​​​​​​public boolean dispatchTouchEvent(MotionEvent ev) { ​​​​​​​​ ... ​​​​​​​​ boolean handled = false; ​​​​​​​​ if (onFilterTouchEventForSecurity(ev)) { ​​​​​​​​ ... ​​​​​​​​ // 檢查事件是否被取消 ​​​​​​​​ final boolean canceled = resetCancelNextUpFlag(this) ​​​​​​​​ || actionMasked == MotionEvent.ACTION_CANCEL; ​​​​​​​​ // Update list of touch targets for pointer down, if needed. ​​​​​​​​ // 如果有需要的話需要對所有 down 事件檢查 ​​​​​​​​ final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE; ​​​​​​​​ final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 ​​​​​​​​ && !isMouseEvent;
    4. ViewGroup 處理、分發事件(事件沒有被取消或中斷):在這一步會透過遞迴呼叫,嘗試找到處理事件的 View

      這裡的遞迴操作,就是遍歷二元樹的操作

      關注變數 當前數值
      actionMasked MotionEvent.ACTION_DOWN
      mFirstTouchTarget null
      canceled false
      intercepted false
      ​​​​​​​​// ViewGroup.java ​​​​​​​​// ViewGroup 的 ChildView 數量 ​​​​​​​​private int mChildrenCount; ​​​​​​​​// ChildView 聚集 ​​​​​​​​private View[] mChildren; ​​​​​​​​@Override ​​​​​​​​public boolean dispatchTouchEvent(MotionEvent ev) { ​​​​​​​​ ... ​​​​​​​​ boolean handled = false; ​​​​​​​​ if (onFilterTouchEventForSecurity(ev)) { ​​​​​​​​ ... ​​​​​​​​ if (!canceled && !intercepted) { // 成立 ​​​​​​​​ // 檢查事件是否有需要分配到其他 View 上 ​​​​​​​​ View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ​​​​​​​​ ? findChildWithAccessibilityFocus() : null; ​​​​​​​​ if (actionMasked == MotionEvent.ACTION_DOWN // 成立 ​​​​​​​​ || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) ​​​​​​​​ || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 多指觸控 ​​​​​​​​ // 如果是 ACTION_DOWN 的話就是 0 ​​​​​​​​ final int actionIndex = ev.getActionIndex(); // always 0 for down ​​​​​​​​ final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) ​​​​​​​​ : TouchTarget.ALL_POINTER_IDS; ​​​​​​​​ // Clean up earlier touch targets for this pointer id in case they ​​​​​​​​ // have become out of sync. ​​​​​​​​ removePointersFromTouchTargets(idBitsToAssign); ​​​​​​​​ // 取得目前 ChildeView 的數量 ​​​​​​​​ final int childrenCount = mChildrenCount; ​​​​​​​​ if (newTouchTarget == null && childrenCount != 0) {// 成立 ​​​​​​​​ final float x = ​​​​​​​​ isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex); ​​​​​​​​ final float y = ​​​​​​​​ isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex); ​​​​​​​​ // 由上到下找尋可以接收該事件的 ChildView ​​​​​​​​ final ArrayList<View> preorderedList = buildTouchDispatchChildList(); ​​​​​​​​ final boolean customOrder = preorderedList == null ​​​​​​​​ && isChildrenDrawingOrderEnabled(); ​​​​​​​​ final View[] children = mChildren; ​​​​​​​​ // 跌代所有 ChildView ​​​​​​​​ for (int i = childrenCount - 1; i >= 0; i--) { ​​​​​​​​ final int childIndex = getAndVerifyPreorderedIndex( ​​​​​​​​ childrenCount, i, customOrder); ​​​​​​​​ final View child = getAndVerifyPreorderedView( ​​​​​​​​ preorderedList, children, childIndex); ​​​​​​​​ ... ​​​​​​​​ // canReceivePointerEvents,ChildView 無法收到該事件 ​​​​​​​​ // isTransformedTouchPointInView,ChildView 無法接都到該事件 ​​​​​​​​ if (!child.canReceivePointerEvents() ​​​​​​​​ || !isTransformedTouchPointInView(x, y, child, null)) { ​​​​​​​​ ev.setTargetAccessibilityFocus(false); ​​​​​​​​ continue; ​​​​​​​​ } ​​​​​​​​ // 取得 處理事件的 View ​​​​​​​​ newTouchTarget = getTouchTarget(child); ​​​​​​​​ if (newTouchTarget != null) { ​​​​​​​​ // Child is already receiving touch within its bounds. ​​​​​​​​ // Give it the new pointer in addition to the ones it is handling. ​​​​​​​​ newTouchTarget.pointerIdBits |= idBitsToAssign; ​​​​​​​​ break; ​​​​​​​​ } ​​​​​​​​ resetCancelNextUpFlag(child); ​​​​​​​​ // 往下分發事件,這邊如果 ChildView ​​​​​​​​ // 1. ViewGroup: 遞迴呼叫 dispatchTransformedTouchEvent 方法 ​​​​​​​​ // 2. View: 調用 View#dispatchTouchEvent,最終由 onTouchEvent 處理 ​​​​​​​​ if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ​​​​​​​​ // Child wants to receive touch within its bounds. ​​​​​​​​ mLastTouchDownTime = ev.getDownTime(); ​​​​​​​​ if (preorderedList != null) { ​​​​​​​​ // childIndex points into presorted list, find original index ​​​​​​​​ for (int j = 0; j < childrenCount; j++) { ​​​​​​​​ if (children[childIndex] == mChildren[j]) { ​​​​​​​​ mLastTouchDownIndex = j; ​​​​​​​​ break; ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ } else { ​​​​​​​​ mLastTouchDownIndex = childIndex; ​​​​​​​​ } ​​​​​​​​ mLastTouchDownX = ev.getX(); ​​​​​​​​ mLastTouchDownY = ev.getY(); ​​​​​​​​ ​​​​​​​​ // 處理完事件後,賦予 newTouchTarget ​​​​​​​​ // @ 查看 addTouchTarget 方法 ​​​​​​​​ newTouchTarget = addTouchTarget(child, idBitsToAssign); ​​​​​​​​ // 標註以分發事件完成 ​​​​​​​​ alreadyDispatchedToNewTouchTarget = true; ​​​​​​​​ break; ​​​​​​​​ } ​​​​​​​​ // The accessibility focus didn't handle the event, so clear ​​​​​​​​ // the flag and do a normal dispatch to all children. ​​​​​​​​ ev.setTargetAccessibilityFocus(false); ​​​​​​​​ } // end for ​​​​​​​​ if (preorderedList != null) preorderedList.clear(); ​​​​​​​​ } // end if ​​​​​​​​ // 跌代完所有的 ChildView,但沒有 View 處理該事件 ​​​​​​​​ if (newTouchTarget == null && mFirstTouchTarget != null) { ​​​​​​​​ // 指定 pointer 給最後添加的 View ​​​​​​​​ newTouchTarget = mFirstTouchTarget; ​​​​​​​​ while (newTouchTarget.next != null) { ​​​​​​​​ newTouchTarget = newTouchTarget.next; ​​​​​​​​ } ​​​​​​​​ newTouchTarget.pointerIdBits |= idBitsToAssign; ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ ​​​​​​​​ } ​​​​​​​​} ​​​​​​​​// 賦予 mFirstTouchTarget ​​​​​​​​private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { ​​​​​​​​ final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); ​​​​​​​​ target.next = mFirstTouchTarget; ​​​​​​​​ mFirstTouchTarget = target; ​​​​​​​​ return target; ​​​​​​​​}
    5. 判斷事件是否已經分發到目標 ChildView,1. 如果沒有 ChildView 處理,則拋回到 ViewGroup 的 View、2. 處理 cancel 事件

      關注變數 當前數值
      actionMasked MotionEvent.ACTION_DOWN
      mFirstTouchTarget 有可能為 null (如果是 ChildView 處理就不是 null)
      canceled false
      intercepted false
      ​​​​​​​​// ViewGroup.java ​​​​​​​​// ViewGroup 的 ChildView 數量 ​​​​​​​​private int mChildrenCount; ​​​​​​​​// ChildView 聚集 ​​​​​​​​private View[] mChildren; ​​​​​​​​@Override ​​​​​​​​public boolean dispatchTouchEvent(MotionEvent ev) { ​​​​​​​​ ... ​​​​​​​​ boolean handled = false; ​​​​​​​​ if (onFilterTouchEventForSecurity(ev)) { ​​​​​​​​ ... ​​​​​​​​ if (!canceled && !intercepted) { // 成立 ​​​​​​​​ ... 結束事件分發 ​​​​​​​​ } ​​​​​​​​ // 目前沒有 View 處理事件 ​​​​​​​​ if (mFirstTouchTarget == null) { ​​​​​​​​ // 視為普通視圖 (最終會傳遞到當前 ViewGroup 的 View) ​​​​​​​​ handled = dispatchTransformedTouchEvent(ev, canceled, null, ​​​​​​​​ TouchTarget.ALL_POINTER_IDS); ​​​​​​​​ } else { ​​​​​​​​ // 分發到點擊的 View,如果已經分發,有需要的排除其他點擊目標 ​​​​​​​​ TouchTarget predecessor = null; ​​​​​​​​ TouchTarget target = mFirstTouchTarget; ​​​​​​​​ while (target != null) { ​​​​​​​​ final TouchTarget next = target.next; ​​​​​​​​ ​​​​​​​​ // 判斷是否已經分發事件 ​​​​​​​​ if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { ​​​​​​​​ // 已分發 ​​​​​​​​ handled = true; ​​​​​​​​ } else { ​​​​​​​​ // 是否取消事件 ​​​​​​​​ final boolean cancelChild = resetCancelNextUpFlag(target.child) ​​​​​​​​ || intercepted; ​​​​​​​​ ​​​​​​​​ // 分發事件 ​​​​​​​​ if (dispatchTransformedTouchEvent(ev, cancelChild, ​​​​​​​​ target.child, target.pointerIdBits)) { ​​​​​​​​ handled = true; ​​​​​​​​ } ​​​​​​​​ ​​​​​​​​ // 如果是 Cacncel 事件則轉移 mFirstTouchTarget ​​​​​​​​ if (cancelChild) { ​​​​​​​​ if (predecessor == null) { ​​​​​​​​ mFirstTouchTarget = next; ​​​​​​​​ } else { ​​​​​​​​ predecessor.next = next; ​​​​​​​​ } ​​​​​​​​ target.recycle(); ​​​​​​​​ target = next; ​​​​​​​​ continue; ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ predecessor = target; ​​​​​​​​ target = next; ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ return handled; ​​​​​​​​}

ViewGroup 事件傳遞 - dispatchTransformedTouchEvent

  • 上面介紹了 ViewGroup 的事件分法,現在介紹 ViewGroup 的事件傳遞 (+Transformed)

    1. 處理 View 的取消事件:如果有就透過 dispatchTouchEvent 方法分發給目標 View

      ​​​​​​​​private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ​​​​​​​​ View child, int desiredPointerIdBits) { ​​​​​​​​ final boolean handled; ​​​​​​​​ final int oldAction = event.getAction(); ​​​​​​​​ // 判斷事件是否取消 ​​​​​​​​ if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { ​​​​​​​​ event.setAction(MotionEvent.ACTION_CANCEL); ​​​​​​​​ ​​​​​​​​ // 沒有目標 View ​​​​​​​​ if (child == null) { ​​​​​​​​ // 傳給自身的 ViewGroup 的 View ​​​​​​​​ handled = super.dispatchTouchEvent(event); ​​​​​​​​ } else { ​​​​​​​​ handled = child.dispatchTouchEvent(event); ​​​​​​​​ } ​​​​​​​​ event.setAction(oldAction); ​​​​​​​​ return handled; ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ return handle; ​​​​​​​​}
    2. 計算將被傳遞的 pointer 數量

      ​​​​​​​​private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ​​​​​​​​ View child, int desiredPointerIdBits) { ​​​​​​​​ final boolean handled; ​​​​​​​​ ... ​​​​​​​​ // 計算將被傳遞的 pointer 數量 ​​​​​​​​ final int oldPointerIdBits = event.getPointerIdBits(); ​​​​​​​​ final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; ​​​​​​​​ // 如果沒有點,則直接返回 ​​​​​​​​ if (newPointerIdBits == 0) { ​​​​​​​​ return false; ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​}
    3. 觸摸數量相同:計算偏移量,再透過 dispatchTouchEvent 方法分發事件給 ChildView

      • 偏移量

        如果有調用 scrollTo or scrollBy 對 ChildView 進行滾動,就會產生 xy 的偏移量

      ​​​​​​​​private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ​​​​​​​​ View child, int desiredPointerIdBits) { ​​​​​​​​ final boolean handled; ​​​​​​​​ ... ​​​​​​​​ // 保存座標轉換後的 MotionEvent ​​​​​​​​ final MotionEvent transformedEvent; ​​​​​​​​ // 點擊數量一致 ​​​​​​​​ if (newPointerIdBits == oldPointerIdBits) { ​​​​​​​​ if (child == null || child.hasIdentityMatrix()) { ​​​​​​​​ if (child == null) { ​​​​​​​​ // 呼叫 ViewGroup 的 View ​​​​​​​​ handled = super.dispatchTouchEvent(event); ​​​​​​​​ } else { ​​​​​​​​ // 計算 x,y 偏移量 ​​​​​​​​ final float offsetX = mScrollX - child.mLeft; ​​​​​​​​ final float offsetY = mScrollY - child.mTop; ​​​​​​​​ // 設定轉換後的 offset 目標 ​​​​​​​​ event.offsetLocation(offsetX, offsetY); ​​​​​​​​ // 分發事件 到 ChildView ​​​​​​​​ handled = child.dispatchTouchEvent(event); ​​​​​​​​ // 復位 ​​​​​​​​ event.offsetLocation(-offsetX, -offsetY); ​​​​​​​​ } ​​​​​​​​ return handled; ​​​​​​​​ } ​​​​​​​​ transformedEvent = MotionEvent.obtain(event); ​​​​​​​​ } else { ​​​​​​​​ transformedEvent = event.split(newPointerIdBits); ​​​​​​​​ } ​​​​​​​​ ​​​​​​​​ ... ​​​​​​​​}
    4. 觸摸數量不相同:同上,最終都是透過 dispatchTouchEvent 方法分發事件

      ​​​​​​​​private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ​​​​​​​​ View child, int desiredPointerIdBits) { ​​​​​​​​ final boolean handled; ​​​​​​​​ ... ​​​​​​​​ // Perform any necessary transformations and dispatch. ​​​​​​​​ if (child == null) { ​​​​​​​​ handled = super.dispatchTouchEvent(transformedEvent); ​​​​​​​​ } else { ​​​​​​​​ // 計算偏移量 ​​​​​​​​ final float offsetX = mScrollX - child.mLeft; ​​​​​​​​ final float offsetY = mScrollY - child.mTop; ​​​​​​​​ transformedEvent.offsetLocation(offsetX, offsetY); ​​​​​​​​ if (! child.hasIdentityMatrix()) { ​​​​​​​​ transformedEvent.transform(child.getInverseMatrix()); ​​​​​​​​ } ​​​​​​​​ // 分發事件 ​​​​​​​​ handled = child.dispatchTouchEvent(transformedEvent); ​​​​​​​​ } ​​​​​​​​ ... ​​​​​​​​ return handled; ​​​​​​​​}

更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

創建模式 - Creation Patterns

行為模式 - Behavioral Patterns

結構模式 - Structural Patterns

Appendix & FAQ

tags: Java 設計模式 基礎進階