kyle shanks
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- title: 'View 事件分發 / 滑動衝突' disqus: kyleAlien --- View 事件分發 / 滑動衝突 === ## Overview of Content :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Android View 事件分發:處理滑動衝突 | 內部、外部事件攔截**](https://devtechascendancy.com/android-view-event_conflict-interception/) ::: * 以下的 Android 源碼會採用 [**Android-10**](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/view/View.java) 的源碼 [TOC] ## 點擊事件傳入 點擊事件是從 **Activity(起點) 透過一層層傳遞至 View(終點) 中**,下圖是一個示意圖,至於若對建構 View 有興趣可以參考 [**LayoutInflater 分析**](https://hackmd.io/-2Uta0yBSbyhNayaev5eyg#LayoutInflater-%E5%8A%A0%E8%BC%89) 最終傳入 View 中讓其處理點擊事件 (從最外部 Acitivty -> Window -> View) > ![](https://i.imgur.com/FbmUH6H.png) ### 事件分發概念 * 由於 View 的分發實作細節比較多,但我們要關注的 "**分發**",主要是由 3 個方法來完成 | 分發重點方法 | 功能 | 返回意義 | | -------- | -------- | -------- | | dispatchTouchEvent : boolean | 由上層 View 被觸發,傳遞至目標 View | 返回結果會由 `onTouchEvent`、子 View 的 `dispatchTouchEvent` 影響 (true: 被處理) | | onInterceptTouchEvent : boolean | 在當前 View 中,用來判斷是否攔截某個事件 | 返回結果表示該事件是否被攔截 (true: 被攔截) | | onTouchEvent : boolean | 當前 View 已經攔截,開始處理是建 | 返回結果代表該事件是否被消耗 (true: 被處理) | * 以下是 View 事件分發的偽程式,可以很好的描述出 View 事件分發概念 ```java= // View 偽程式 public boolean dispatchTouchEvent(MotionEvent e) { bool isEventConsume = false; if(onInterceptTouchEvent(e)) { // 是否攔截分發 isEventConsume = onTouchEvent(e); } else { isEventConsume = child.dispatchTouchEvent(e); } return isEventConsume; } ``` 1. 可以看出分發順序 `dispatchTouchEvent` -> `onInterceptTouchEvent`,在依照是否消耗來決定之後的走向 2. `dispatchTouchEvent` 的遞迴調用(若事件沒有被消耗,就會往子成員 `child` 繼續呼叫),直到找到消耗事件的 View :::info Android View 是一個 [**組件模式**](https://devtechascendancy.com/object-oriented_programming_composite/)、[**裝飾模式**](https://devtechascendancy.com/object-oriented_design_decorate/) 的混合設計模式,它擁有加強或是弱化抽象,還有遞歸抽象… 等等程式設計特性 ::: > ![](https://i.imgur.com/XU0hiYG.png) * 若該 View 已經攔截點擊事件,則會觸發 onTouch 1. `onTouch` 若沒消費該事件,才會傳遞給 `onTouchEvent` 方法 2. `onTouchEvent` 內才會有 onClick 事件 > ![](https://i.imgur.com/IiFi8Ta.png) ### Activity 接收點擊事件 * Activity (AppCompatActivity)、Windows (PhoneWindow) 之間的關係可以參考另一篇 [**Activity 布局**](https://hackmd.io/-2Uta0yBSbyhNayaev5eyg#AppCompatActivity),下圖表示了 `AppCompatActivity` 如何與 `PhoneWindow` 產生關係 > ![](https://i.imgur.com/i89DAQs.png) * **點擊事件從 ==Activity#dispatchTouchEvent== 開始分析**,一路會分析進入 `PhoneWindow`、`DecorWindow`,最後到達 DecorView#ViewGroup (最頂層 ViewGroup) 中的 **dispatchTouchEvent 方法** ```java= /** * Activity.java */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } // 這裡的 getWindow 就是 PhoneWindow 類 // @ 分析 superDispatchTouchEvent if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } // ------------------------------------------------------------- /** * PhoneWindow.java */ private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } // ------------------------------------------------------------- /** * DecorView.java * * DecorView 繼承於 FrameLayout,而 FrameLayout 並沒有 Overidde * dispatchTouchEvent 方法,所以必須往它的父類尋找 */ public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } // ------------------------------------------------------------- /** * ViewGroup.java * * ViewGroup 繼承於 View * ViewGroup 有重寫 dispatchTouchEvent,所以不用繼續往 View 去 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略 } ``` :::info * 從這裡可以看出 **事件分發會由 PhoneWindow 中的 DecorView 開始** ::: > ![](https://i.imgur.com/vBtHm8o.png) * **Activity & View 關係圖** > ![](https://i.imgur.com/59KfaTv.png) ## 事件處理 透過上面分析,我們就知道點擊 **事件是如何傳遞至 DecorView#ViewGroup 中**,這裡我們會再分 ViewGroup 點擊事件來分析 - **先了解幾個 MotionEvent** | MotionEvent 事件 | 動作 | 其他 | | -------- | -------- | -------- | | ACTION_DOWN | 手指下壓 | 又份為攔截、不攔截 | | ACTION_UP | 手指抬起 | 事件結束 | | ACTION_MOVE | 在螢幕滑動 | 會被 **多次觸發** | | ACTION_CANCEL | 事件取消 | **事件被上層攔截時候觸發** | ### [ViewGroup](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewGroup.java) 處理 `ACTION_DOWN`:ChildView 攔截 * **==Down 事件只會觸發一次== (單點觸控,多點觸控就不只一次)**,攔截就是該 ViewGroup **自己處理事件**,不會對 ChildView 分發 * 每個點擊事件都是以 Action Down 開始,**細節請注意以下的++註解++**,而它主要做的事情有 (這裡會先列出主要的處理項目) 1. 清除先前的事件:注意 **resetTouchState 方法,它會在 ViewGroup#`dispatchTouchEvent` 事件是 ACTION_DOWN 時執行** > ViewGroup 中有一個 member 是 **TouchTarget**:它用來串接該點擊事件 ```java= // ViewGroup.java // 從該 ViewGroup 開始,往下串接點擊的目標 private TouchTarget mFirstTouchTarget; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 取得目前行為 final int action = ev.getAction(); // 與 Mask 進行 and 操作,取得真正的 Action final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { // 在 Action Down 時才執行 reset // @ 追蹤 cancelAndClearTouchTargets 方法 cancelAndClearTouchTargets(ev); // @ 追蹤 resetTouchState 方法 resetTouchState(); } } ... 省略部分 return handled; } private void resetTouchState() { // @ 查看 clearTouchTargets 方法 clearTouchTargets(); resetCancelNextUpFlag(this); // 清除 FLAG_DISALLOW_INTERCEPT、允許 ViewGroup 攔截 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; } // 清除第一個點擊 View 串列的所有事件 private void clearTouchTargets() { // TouchTarget 是單向鏈表 TouchTarget target = mFirstTouchTarget; if (target != null) { do { TouchTarget next = target.next; // 回收 TouchTarget 方便覆用 target.recycle(); target = next; } while (target != null); // @ 將成員 mFirstTouchTarget 至為 null mFirstTouchTarget = null; } } ``` :::info * 我們來試運算,使用上面的 `~&` 公式,是否能清除 `FLAG_DISALLOW_INTERCEPT` 這個 Flag 值 ```shell= ## 假設 mGroupFlags 為 0x8A (0b1000 1010) ## FLAG_DISALLOW_INTERCEPT 為 0x80 (這是假設值 0b1000 0000) ## 首先反向 ~FLAG_DISALLOW_INTERCEPT ## Ans: 0b0111 1111 ## 做 And 運算 mGroupFlags & (0b0111 1111) ## (0b1000 1010) & (0b0111 1111) Ans: 0b0000 1010 ``` ::: 2. 是否會呼叫 ViewGroup#**onInterceptTouchEvent** 方法:會有兩個條件,再加上一個 FLAG 判斷: 1. 目前是 ACTION_DOWN 事件 2. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 mFirstTouchTarget 賦值) 3. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 `ACTION_DOWN` 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法) :::success * **FLAG_DISALLOW_INTERCEPT 判斷**: 可以透過 ViewGroup#requestDisallowInterceptTouchEvent 方法控制,**通常是 ChildView 在要求 Parent 不要攔截使用** 同時如果 ChildView 如果使用 requestDisallowInterceptTouchEvent 時也要同時禁止 ViewGroup 對 `ACTION_DOWN` 的攔截 (**請看第一點**) ::: ```java= // ViewGroup.java // 從該 ViewGroup 開始,往下串接點擊的目標 private TouchTarget mFirstTouchTarget; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 取得目前行為 final int action = ev.getAction(); // 與 Mask 進行 and 操作,取得真正的 Action final int actionMasked = action & MotionEvent.ACTION_MASK; // Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT final boolean intercepted; // 這裡有兩個判斷 決定是否呼叫 ViewGroup 自己的 onInterceptTouchEvent 方法 // 1. 目前是 ACTION_DOWN // 2. 已經有子 View 處理這個點擊事件 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 判斷是否禁止 攔截 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 { intercepted = true; } } ... 省略部分 return handled; } ``` > ![](https://i.imgur.com/UOLS8bz.png) 3. 若 ViewGroup 自身沒有攔截,就會 ++**遞迴 ChildView**++:並一一執行 **ViewGroup#dispatchTransformedTouchEvent** 分發給每個 ChildView 處理 - 若 ChildView 消耗事件則返回 true,則跳出迴圈 - 否則往下一個 ChildView 詢問 ```java= // ViewGroup // 從該 ViewGroup 開始,往下串接點擊的目標 private TouchTarget mFirstTouchTarget; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 取得目前行為 final int action = ev.getAction(); // 與 Mask 進行 and 操作,取得真正的 Action final int actionMasked = action & MotionEvent.ACTION_MASK; // Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT final boolean intercepted; // 判斷自身 ViewGroup 是否可以攔截事件 // 目前假設 ViewGroup 不攔截 if (!canceled && !intercepted) { ... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ... if (newTouchTarget == null && childrenCount != 0) { ... // 重新排列在該 ViewGroup 中的 ChildView 們的順序 final ArrayList<View> preorderedList = buildTouchDispatchChildList(); ... for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); ... // 是否動畫中 canReceivePointerEvents // 點擊是否在 child 元素內 isTransformedTouchPointInView if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 獲取點擊中的 View // 新的 View 事件才會返回 非 null newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Give it the new pointer in addition to the ones it is handling. // Child View 已經接收到點擊 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } // @ 重點在 dispatchTransformedTouchEvent 方法 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // ChildView 處理事件成功 ... 省略部分 mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // @ addTouchTarget 方法 // 內會賦予 mFirstTouchTarget 值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; // 跳出迴圈 break; } } } } } } ... 省略部分 return handled; } ``` :::success * 從這裡可以看出來 **View 是一個多元樹**,並且在這裡使用 [**責任鏈 OOP 設計**](https://devtechascendancy.com/object-oriented_design_chain_framework/) > ![](https://i.imgur.com/z60XP2V.png) ::: :::info * **按照螢幕上的 Z 軸**,重新排列 ViewGroup 中 ChildView 的順序 ```java= // ViewGroup public ArrayList<View> buildTouchDispatchChildList() { return buildOrderedChildList(); } ArrayList<View> buildOrderedChildList() { // 全部 ChildView 的數量 final int childrenCount = mChildrenCount; ... final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = 0; i < childrenCount; i++) { // add next child (in child order) to end of list final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View nextChild = mChildren[childIndex]; final float currentZ = nextChild.getZ(); // insert ahead of any Views with greater Z int insertIndex = i; while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) { insertIndex--; } // Z 軸越大則擺放至越前面 mPreSortedChildren.add(insertIndex, nextChild); } return mPreSortedChildren; } ``` ::: * [**ViewGroup**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)#**dispatchTransformedTouchEvent** 方法:**ViewGroup 會透過該方法把事件傳遞給 ChildView** > 內部其實就會針對 ChildView 執行 **dispatchTouchEvent 方法** 1. 若是傳入的 child 是 null 就呼叫 ViewGroup 父類的 dispatchTouchEvent 2. 不是 null 則呼叫指定 View 的 dispatchTouchEvent (**當前情況就是有傳入 View 物件,所以會轉跳到指定 View#dispatchTouchEvent 方法**) ```java= // ViewGroup.java private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... 省略部分 // 重點在個判斷 if (child == null) { // 如果 Child view 為 null 則回傳給 ViewGroup 的 Parent handled = super.dispatchTouchEvent(transformedEvent); } else { ... 目前走這裡 handled = child.dispatchTouchEvent(transformedEvent); } return handled; } ``` * ViewGroup#**addTouchTarget**:賦予 `mFirstTouchTarget` 成員 ```java= // ViewGroup.java private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { // 取覆用的 TouchTarget 物件 final TouchTarget target = TouchTarget .obtain(child, pointerIdBits); // 串接上一個事件的 View target.next = mFirstTouchTarget; // 第一個 處理點擊事件 的 View mFirstTouchTarget = target; return target; } ``` > ![](https://i.imgur.com/XAHPQnv.png) :::success 一開始 target.next 為 null,通過 TouchTarget#obtain 獲得對象 (相當於 new 的功能),並將 next 只給前一個 View **最後再將剛剛獲得的對象賦予 `mFirstTouchTarget`** 並返回 ::: 4. 在 ChildView 接收並攔截事件後 (透過 `dispatchTouchEvent` 方法),**會賦予該 ViewGroup#mFirstTouchTarget 值**、並跳出迴圈 * ChildView 處理完事件後,返回到 ViewGroup 繼續處理 ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 if (onFilterTouchEventForSecurity(ev)) { ... 省略上面分析 (ChildView 分發,跳出 for 迴圈) // 目前情況, ChildView 已經處理事件,所以 mFirstTouchTarget 不為 null if (mFirstTouchTarget == null) { // 下一小節在分析 } else { ... 目前判斷走這 TouchTarget predecessor = null; // 尋找目前的點擊事件相對的 ChildView 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; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } } ... return handled; } ``` :::info 到這裡就分析完 ViewGroup 不攔截事件,ChildView 透過 **View#dispatchTouchEvent** 收到事件 ::: ### [ViewGroup](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java) 處理 `ACTION_DOWN`:ChildView 不攔截 :::success ChildView 不攔截代表 **ViewGroup 要自己決定要不要處理該事件** ::: * ViewGroup ChildView 攔截事件:處理方式相同 (上一小節),這裡主要看看不同的部分,該事件由 ViewGroup 自己處理 (dispatchTransformedTouchEvent 方法) ```java= // ViewGroup.java // 從該 ViewGroup 開始,往下串接點擊的目標 private TouchTarget mFirstTouchTarget; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 取得目前行為 final int action = ev.getAction(); // 與 Mask 進行 and 操作,取得真正的 Action final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { // 在 Action Down 時才執行 reset cancelAndClearTouchTargets(ev); resetTouchState(); } } ... 省略部分 return handled; } ``` 1. 清除先前的事件:注意 **resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 `ACTION_DOWN` 時執行** > 同上,請參考上一小節 2. 是否會呼叫 ViewGroup#**onInterceptTouchEvent** 方法:會有兩個條件,再加上一個 FLAG 判斷 1. 目前是 `ACTION_DOWN` 事件 2. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 `mFirstTouchTarget` 賦值) 3. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 `ACTION_DOWN` 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法) > 同上,請參考上一小節 3. 若 ViewGroup 自身沒有攔截,就會遞迴 ChildView:並一一執行 **ViewGroup#dispatchTransformedTouchEvent** 分發給每個 ChildView 處理 ```java= // ViewGroup // 從該 ViewGroup 開始,往下串接點擊的目標 private TouchTarget mFirstTouchTarget; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { ... if (!canceled && !intercepted) { // 這邊會循環該 ViewGroup 中的所有的 ChildView,看有沒有 View 處理事件 // // 目前狀況是都沒有 ChildView 要處理這個事件 } } ... } ``` 4. ChildView 接收但 **全部都不攔截事件**,**ViewGroup#mFirstTouchTarget 為 null** ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 if (onFilterTouchEventForSecurity(ev)) { ... 省略上面分析 (ChildView 分發) // 目前情況,沒有 ChildView 處理事件 if (mFirstTouchTarget == null) { // 注意傳入的第三個參數 null handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { ... } } ... return handled; } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... 省略部分 if (child == null) { // 回到 ViewGroup 的 Parent dispatchTouchEvent handled = super.dispatchTouchEvent(transformedEvent); } else { ... } ... return handled; } ``` :::info 這裡可以看出 **ViewGroup 預設不處理事件**,直接將事件返回到上一層 View ::: ### [View](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/view/View.java) 收到事件:處理 `ACTION_UP` :::success 假設 ViewGroup 不攔截事件,最終會呼叫目標 View#`dispatchTouchEvent` 方法,**讓 View 處理事件** ::: * **View 首先分發給 onTouch 若是沒有處理則分發到 onTouchEvent**,順序如下 1. 判斷 `onTouch` 接口:**最先執行 onTouch 接口,若是已經處理,就 onTouchEvent 就不會接收到事件** 2. 判斷 `onTouchEvent` 接口:若 onTouch 沒有處理這個事件,就會輪到該 View 的 onTouchEvent 處理事件 ```java= // View.java public boolean dispatchTouchEvent(MotionEvent event) { ... 省略部分 boolean result = false; ... 省略部分 if (onFilterTouchEventForSecurity(event)) { ... // 最一開始事件分發到 onTouch ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null // 判斷該 View 是 enable && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // @ 分析 onTouchEvent if (!result && onTouchEvent(event)) { result = true; } } ... 省略 return result; } ``` * 在 View#**onTouchEvent** 方法中,可以看到 onClick、onLongClick 接口的呼叫 ```java= // View.java // PerformClick 代表該 View 的點擊事件 private PerformClick mPerformClick; public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); // 只要設定 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 其中一個, // 那該 View 就是可點擊的 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 點擊事件的代理 TouchDelegate if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ... 省略部分 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ... // 長按任務 mHasPerformedLongPress if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除長按 Callback removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } // @ 分析 performClickInternal if (!post(mPerformClick)) { performClickInternal(); } } } ... } mIgnoreNextUpEvent = false; break; } } } ``` * 這邊可以看到 View 的點擊事件是 **透過 Handler 傳送 Click 點擊任務 (Runnable),這樣就不會造成點擊事件堵塞** ```java= // View.java public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { // 將 Click 任務放入 Handler return attachInfo.mHandler.post(action); } getRunQueue().post(action); return true; } private boolean performClickInternal() { notifyAutofillManagerOnClick(); // @ 分析 performClick return performClick(); } public boolean performClick() { notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); // 執行使用者的 OnClick 接口 li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } ``` > ![](https://i.imgur.com/sswPfLY.png) ### ViewGroup:ACTION_DOWN 總結 1. 判斷事件是否被 ViewGroup 攔截: * ViewGroup 攔截事件 -> 直接走 `3` * ViewGroup 不攔截事件 -> 先走 `2` 再走 `3` 2. ViewGroup 不攔截,並有其中一個 ChildView 攔截事件並處理 * ViewGroup#newTouchTarget 被賦予值 * 分發到自身的 View (`super.dispatchTouchEvent` 呼叫自己的父類) ```java= // ViewGroup.java if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ... 省略部分 mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // @ addTouchTarget 方法內會賦予 mFirstTouchTarget 值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; // 跳出迴圈 break; } ``` 3. 事件分發、處理:有兩種情況 * 所有 ChildView 不攔截這個事件,**所以 mFirstTouchTarget 為 null**,相當於是最後一個 View > 因為沒有任何 ChildView 處理,所以 **ViewGroup#View 自己處理 Down 事件** ```java= // 分發程式 if (mFirstTouchTarget == null) { // 第三個參數為 null handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } ``` * 有 ChildView 攔截,**所以 mFirstTouchTarget 不為 null**,並且 while 只循環一次,因為 **ChildView 已處理 (分發時處理)** ```java= // 分發程式 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } ``` ### ACTION_MOVE:View 收到 MOVE * **Move 事件仍會先走 DecorView 這個 ViewGroup**,在執行 `ACTION_MOVE` 事件時先需要知道幾件事情 1. **ViewGroup 不會清理點擊的 Flag** 2. TouchTarget 類型的 **`mFirstTouchTarget` 元素 ++不為空++** (因為已經有 ChildView 元素處理) * ACTION_MOVE 事件的判斷流程圖如下,它主要做的事情是 1. ViewGroup 是否攔截事件 (以下預設不攔截事件) 2. 不攔截、**==Move 不分發事件==** 3. 分發 or 處理 > ![](https://i.imgur.com/NzZTMfj.png) * 同樣先來觀察 ViewGroup#dispatchTouchEvent 方法,並從這裡開始分析 1. 不會進入 reset 環節 ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { // 不清理 Flag,不會進入 reset 環節 if (actionMasked == MotionEvent.ACTION_DOWN) { ... 省略 } } ``` 2. 判斷當前 ViewGroup 是否有禁止攔截 `FLAG_DISALLOW_INTERCEPT`,如果沒有的話就調用自身的 `onInterceptTouchEvent` 方法 檢查是否攔截 ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 // Check for interception. // 由於 mFirstTouchTarget 不為空,所以會進入 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 判斷 ViewGroup 是否禁止攔截 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 預設 ViewGroup 不攔截 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { intercepted = true; } } ``` 3. 不是 `ACTION_DOWN` 事件,所以不會進入最初的 `ACTION_DOWN` 事件分發環節 ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 // 注意這兩個區域變數,下面會用到 TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // 進入 if (!canceled && !intercepted) { // 2. 不是 Down 事件,所以不進入 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 省略... 由於不進入,所以不會對 ChildView 分發 } } ... 省略部分 } ``` 4. `ACTION_MOVE` 的重點在事件分發 (請看註解) ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 // mFirstTouchTarget 不為空 (因為已經是 View 處理事件) if (mFirstTouchTarget == null) { ... } else { // ++重點在這++TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // alreadyDispatchedToNewTouchTarget 是 false if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // @ 分析 dispatchTransformedTouchEvent 分發給 ChildView if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } ... } predecessor = target; target = next; } } } ``` * 接下來透過 dispatchTransformedTouchEvent 方法:分發 `ACTION_MOVE` 事件給 ChildView 的 dispatchTouchEvent ```java= // ViewGroup.java private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) ...省略部分 if (child == null) { ... } else { // 加設有 ChildView 則會進入這裡 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); } } ``` ### ACTION_MOVE:ViewGroup 攔截 MOVE * 先了解目前情況:已經有 ChildView 處理 ACTION_DOWN 事件,而我們自訂一個 ViewGroup 並複寫 ViewGroup#**onInterceptTouchEvent** 方法,**==在 `ACTION_VIEW` 時返回 true 攔截==** ```java= // 自定義 ViewGroup.java public class MyViewGroup extends View { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_MOVE) { // ViewGroup 自己消耗事件 return true; } return super.onInterceptTouchEvent(ev); } } ``` * **ACTION_MOVE 事件是多次發生** 1. 第一個 ACTION_MOVE (++從 ParentView 進來++)的目的是:^1.^ 取消 ChildView 事件(**事件改為 ACTION_CANCEL**)、^2.^ 將 **ViewGroup#mFirstTouchTarget 置為空**,這時 ParentView 是不處理事件的 結果:這時 **ChildView 會收到 ACTION_CANCEL 事件** (事件被取消,之後都會由 ViewGroup 處理事件) ```java= /** * ViewGroup.java * * 由於複寫 onInterceptTouchEvent 方法會讓 Move 事件為 ture,所以 cancelChild 也為 ture, * 傳入 dispatchTransformedTouchEvent 方法的 cancel 參數為 ture, * * 這時事件就會被改為 ACTION_CANCEL,下面的 ChildView 就會接收到 ACTION_CANCEL 事件 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { //... 省略 if (mFirstTouchTarget == null) { ... } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { // ACTION_MOVE 第一次進入 // ^1^ 重點 intercepted 是 true,所以 cancelChild = true final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // ^2^ 重點,mFirstTouchTarget = next,next 為 null // 所以 mFirstTouchTarget = null if (cancelChild) { if (predecessor == null) { // next 為空,所以 mFirstTouchTarget 置為空 (next 為空) mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } ... } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); // cancel 為 true 會進入 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { // 改變事件為 ACTION_CANCEL event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { ... } else { // 往下分發 ChildView 的事件就為 cancel // 事件被上層攔截時觸發 handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ... 省略部分 return handled; } ``` 2. 第二個 ACTION_MOVE(從 ParentView 進來),由於 mFirstTouchTarget 為空,所以 ParentView 不會分發 結果:**ParentView 自己處理事件** ```java= /** * ViewGroup.java */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { // 在第一個 ACTION_MOVE 時 mFirstTouchTarget 被置為空,所以不會進入 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // ...省略,因為 mFirstTouchTarget = null } else { intercepted = true; } // 由於 intercepted = true 也就是攔截,就不會進入 if (!canceled && !intercepted) { //... 分發部份 } // 進入 if (mFirstTouchTarget == null) { // ChildView 為空 (第三個參數) handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { ... } ... } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... 省略部分 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); } // Done. transformedEvent.recycle(); return handled; } ``` ## 事件衝突 為何會發生衝突 ? 因為**事件只會有一個**,若是響應的元件不是自己要的 View 原件,這時就可以稱之為事件衝突 > Ex: ViewPager 包裹 RecyclerView * 分為兩種方法處理 1. **內部攔截法**:需要 ChildView#dispatchTouchEvent 處理 & 需要配合改動父容器 (ParentView) 的 onInterceptTouchEvent 攔截 2. **外部攔截法** (較常使用):只需要 ParentView 處理 ### 內部攔截:ChildView 處理 * **在 ChildView 中使用 `requestDisallowInterceptTouchEvent` 方法,控制 `FLAG_DISALLOW_INTERCEPT` 元素,讓 ParentView 不會往下分發事件** (先介紹這裡的坑~) * 內部攔截作法 - 2 個重點步驟 (都必須) > 以下假設,ChildView 只處理垂直滑動、水平滑動給 ParentView 處理 1. ChildView 在適當時機透過 `requestDisallowInterceptTouchEvent` 方法,要求 ParentView 不要攔截事件 ```java= // 自訂 View public class MyView extends View { int dispatchX, dispatchY; @Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch(event.getAction()) { case MotionEvent.ACTION_DOWN: // 要求 ParentView 不攔截事件 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_MOVE: int deltaX = x - dispatchX; int deltaY = y - dispatchY; // 水平時,事件交由 ViewGroup 處理 if(Math.abs(deltaX) > Math.abs(deltaY)) { getParent().requestDisallowInterceptTouchEvent(false); } break; } dispatchX = x; dispatchY = y; return super.dispatchTouchEvent(event); } } ``` 2. 重寫 ParentView 的 `onInterceptTouchEvent` 方法,設定在 ACTION_DOWN 時不攔截事件,這樣事件才能傳入 ChildView ```java= // 自訂 ViewGroup public class MyViewGroup extends ViewGroup { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(ev.getAction() == MotionEvent.ACTION_DOWN) { // ACTION_DOWN 時強制 ViewGroup 不攔截事件 return false; } return super.onInterceptTouchEvent(ev); } } ``` * `requestDisallowInterceptTouchEvent` 細節說明: 為啥 requestDisallowInterceptTouchEvent 無法控制 ACTION_DOWN 事件 :::warning * 發生原因 ViewGroup#`ACTION_DOWN` 事件,會清除 ViewGroup 的禁止攔截事件,所以就算 ChildView 有要求 ViewGroup 不攔截事件,但事件都無法傳入 ChildView 就沒有用了 ::: ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); // @ 查看 resetTouchState resetTouchState(); } // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // mFirstTouchTarget != null 滿足 // 控制 FLAG_DISALLOW_INTERCEPT final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 如果為 ture 則 ParentView 就不會往下分發 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { intercepted = true; } // 省略其他... } } // 這個方法可以控制 Flag @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } } ``` 1. 從上面可看出 ViewGroup 時 ACTION_DOWN 使用 **resetTouchState() 方法**,這裡會清理 `FLAG_DISALLOW_INTERCEPT` 這個 Flag,導致事件 ACTION_DOWN 一定會被分發 ```java= // ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... 省略部分 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 清理後 FLAG_DISALLOW_INTERCEPT 為空,結果就是為 false final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 判斷 ViewGroup#onInterceptTouchEvent 是否擷取 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { // ++不擷取++ 注意 intercepted 變數 intercepted = false; } } ... } ``` 2. 解坑:**ViewGroup 中複寫 `onInterceptTouchEvent`,讓 ViewGroup 在 ACTION_DOWN 事件時返回 false(不擷取),這樣 ACTION_DOWN 就會分發,==這樣才能往下分發到 ChildView==**,而其他的動作則會攔截 :::success 原因是因為 ViewGroup#`resetTouchState` 會清除 `FLAG_DISALLOW_INTERCEPT`,導致上面的設定失效 ::: ```java= /** * 自定義 ViewGroup 內重新 Override */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // ACTION_DOWN 事件時,就不擷取,分發到 ChildView if(ev.getAction() == ACTION_DOWN) { return false; } return true; } ``` ### 外部攔截:ParentView 處理 * 由 ParentView 處理事件攔截比較簡單,只需要覆寫 ParentView#**onInterceptTouchEvent** 決定哪個時間(條件)攔截事件 > 以下假設 水平滑動,就攔截事件給 ParentView#onTouchEvent 處理 ! ```java= // 自定義的 ViewGroup.java public class MyExternalViewGroup extends ViewGroup { int interceptX, interceptY; @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercept = false; boolean defaultIntercept = super.onInterceptTouchEvent(event); int x = (int) event.getX(); int y = (int) event.getY(); switch(event.getAction()) { case MotionEvent.ACTION_MOVE: int deltaX = x - interceptX; int deltaY = y - interceptY; // 水平滑動,就攔截 ! if(Math.abs(deltaX) > Math.abs(deltaY)) { intercept = true; } else { intercept = false; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_DOWN: // 不攔截事件 intercept = false; break; default: intercept = defaultIntercept; break; } interceptX = x; interceptY = y; return intercept; } } ``` ## Appendix & FAQ :::info ::: ###### tags: `Android Framework`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully