[go: nahoru, domu]

Merge "Handling the removal of PointerInputFilters synchronously." into androidx-master-dev
diff --git a/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/CoreDemos.kt b/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/CoreDemos.kt
index 02b4d9e1..abfdd6d 100644
--- a/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/CoreDemos.kt
+++ b/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/CoreDemos.kt
@@ -34,6 +34,7 @@
 import androidx.ui.core.demos.gestures.ScaleGestureFilterDemo
 import androidx.ui.core.demos.gestures.DragGestureFilterDemo
 import androidx.ui.core.demos.gestures.LongPressDragGestureFilterDemo
+import androidx.ui.core.demos.gestures.PointerInputDuringSubComp
 import androidx.ui.core.demos.keyinput.KeyInputDemo
 import androidx.ui.core.demos.viewinterop.ViewInComposeDemo
 import androidx.ui.demos.common.ComposableDemo
@@ -61,7 +62,8 @@
             ComposableDemo("Drag and Scale") { DragAndScaleGestureDetectorDemo() },
             ComposableDemo("Popup Drag") { PopupDragDemo() },
             ComposableDemo("Double Tap in Tap") { DoubleTapInTapDemo() },
-            ComposableDemo("Nested Long Press") { NestedLongPressDemo() }
+            ComposableDemo("Nested Long Press") { NestedLongPressDemo() },
+            ComposableDemo("Pointer Input During Sub Comp") { PointerInputDuringSubComp() }
         ))
 ))
 
diff --git a/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/gestures/SurviveThroughSubComp.kt b/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/gestures/SurviveThroughSubComp.kt
new file mode 100644
index 0000000..0af4c8d
--- /dev/null
+++ b/ui/ui-core/integration-tests/ui-core-demos/src/main/java/androidx/ui/core/demos/gestures/SurviveThroughSubComp.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.ui.core.demos.gestures
+
+import androidx.compose.Composable
+import androidx.compose.remember
+import androidx.compose.state
+import androidx.ui.core.Alignment
+import androidx.ui.core.Modifier
+import androidx.ui.core.PointerEventPass
+import androidx.ui.core.PointerInputChange
+import androidx.ui.core.changedToDownIgnoreConsumed
+import androidx.ui.core.changedToUpIgnoreConsumed
+import androidx.ui.core.composed
+import androidx.ui.core.pointerinput.PointerInputFilter
+import androidx.ui.core.pointerinput.PointerInputModifier
+import androidx.ui.foundation.AdapterList
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.drawBackground
+import androidx.ui.foundation.drawBorder
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Column
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.size
+import androidx.ui.layout.wrapContentSize
+import androidx.ui.unit.IntPxSize
+import androidx.ui.unit.TextUnit
+import androidx.ui.unit.dp
+
+/**
+ * Demonstration of how various press/tap gesture interact together in a nested fashion.
+ */
+@Composable
+fun PointerInputDuringSubComp() {
+    Column {
+        Text(
+            "Demonstrates that PointerInputFilters that are currently receiving pointer input " +
+                    "events can be removed from the hierarchy by sub composition with no difficulty"
+        )
+        Text(
+            "Below is an AdapterList with many touchable items.  Each item keeps track of the " +
+                    "number of pointers touching it.  If you touch an item and then scroll so " +
+                    "that it goes out of the viewport and then back into the viewport, you will" +
+                    " see that it no longer knows that a finger is touching it.  That is because " +
+                    "it is actually a new item that has not been hit tested yet.  If you keep " +
+                    "your finger there and then add more fingers, it will track those new fingers."
+        )
+        AdapterList(
+            List(100) { index -> index },
+            Modifier
+                .fillMaxSize()
+                .wrapContentSize(Alignment.Center)
+                .size(200.dp)
+                .drawBackground(Color.White)
+        ) {
+            val pointerCount = state { 0 }
+
+            Box(
+                Modifier.size(200.dp)
+                    .drawBorder(size = 1.dp, color = Color.Black)
+                    .pointerCounterGestureFilter { newCount -> pointerCount.value = newCount }
+            ) {
+                Text(
+                    "${pointerCount.value}",
+                    fontSize = TextUnit.Em(16),
+                    color = Color.Black,
+                    modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center)
+                )
+            }
+        }
+    }
+}
+
+fun Modifier.pointerCounterGestureFilter(
+    onPointerCountChanged: (Int) -> Unit
+): Modifier =
+    composed {
+        val filter = remember { PointerCounterGestureFilter() }
+        filter.>
+        PointerInputModifierImpl(filter)
+    }
+
+internal class PointerInputModifierImpl(override val pointerInputFilter: PointerInputFilter) :
+    PointerInputModifier
+
+internal class PointerCounterGestureFilter : PointerInputFilter() {
+
+    lateinit var onPointerCountChanged: (resultingPointerCount: Int) -> Unit
+
+    override fun onPointerInput(
+        changes: List<PointerInputChange>,
+        pass: PointerEventPass,
+        bounds: IntPxSize
+    ): List<PointerInputChange> {
+        if (pass == PointerEventPass.PostUp) {
+            if (changes.any {
+                    it.changedToDownIgnoreConsumed() || it.changedToUpIgnoreConsumed()
+                }) {
+                onPointerCountChanged.invoke(changes.count { it.current.down })
+            }
+        }
+        return changes
+    }
+
+    override fun onCancel() {}
+}
diff --git a/ui/ui-core/src/main/java/androidx/ui/core/pointerinput/HitPathTracker.kt b/ui/ui-core/src/main/java/androidx/ui/core/pointerinput/HitPathTracker.kt
index 07b2054..d645955 100644
--- a/ui/ui-core/src/main/java/androidx/ui/core/pointerinput/HitPathTracker.kt
+++ b/ui/ui-core/src/main/java/androidx/ui/core/pointerinput/HitPathTracker.kt
@@ -405,6 +405,7 @@
         downPass: PointerEventPass,
         upPass: PointerEventPass?
     ): Boolean {
+
         // Filter for changes that are associated with pointer ids that are relevant to this node.
         val relevantChanges =
             pointerInputChanges.filterTo(mutableMapOf()) { entry ->
@@ -416,28 +417,34 @@
             return false
         }
 
-        // For each relevant change:
-        //  1. subtract the offset
-        //  2. dispatch the change on the down pass,
-        //  3. update it in relevantChanges.
-        relevantChanges.let {
-            // TODO(shepshapard): would be nice if we didn't have to subtract and then add
-            //  offsets.  This is currently done because the calculated offsets are currently
-            //  global, not relative to eachother.
-            it.subtractOffset(pointerInputFilter.position)
-            it.dispatchToPointerInputFilter(pointerInputFilter, downPass, pointerInputFilter.size)
-            it.addOffset(pointerInputFilter.position)
+        // TODO(b/158243568): For this attached check, and all of the following checks like this, we
+        //  should ideally be dispatching cancel to the sub tree with this node as it's root, and
+        //  we should remove the same sub tree from the tracker.  This will currently happen on
+        //  the next dispatch of events, but we shouldn't have to wait for another event.
+        if (pointerInputFilter.isAttached) {
+            relevantChanges.let {
+                // TODO(shepshapard): would be nice if we didn't have to subtract and then add
+                //  offsets. This is currently done because the calculated offsets are currently
+                //  global, not relative to each other.
+                it.subtractOffset(pointerInputFilter.position)
+                it.dispatchToPointerInputFilter(
+                    pointerInputFilter,
+                    downPass,
+                    pointerInputFilter.size
+                )
+                it.addOffset(pointerInputFilter.position)
+            }
         }
 
-        // Call children recursively with the relevant changes.
-        children.forEach { it.dispatchChanges(relevantChanges, downPass, upPass) }
+        if (pointerInputFilter.isAttached) {
+            children.forEach { it.dispatchChanges(relevantChanges, downPass, upPass) }
+        }
 
-        // For each relevant change:
-        //  1. dispatch the change on the up pass,
-        //  2. add the offset,
-        //  3. update it in  relevant changes.
-        if (upPass != null) {
+        if (pointerInputFilter.isAttached && upPass != null) {
             relevantChanges.let {
+                // TODO(shepshapard): would be nice if we didn't have to subtract and then add
+                //  offsets.  This is currently done because the calculated offsets are currently
+                //  global, not relative to each other.
                 it.subtractOffset(pointerInputFilter.position)
                 it.dispatchToPointerInputFilter(pointerInputFilter, upPass, pointerInputFilter.size)
                 it.addOffset(pointerInputFilter.position)
@@ -446,7 +453,6 @@
 
         // Mutate the pointerInputChanges with the ones we modified.
         pointerInputChanges.putAll(relevantChanges)
-
         return true
     }
 
@@ -474,16 +480,22 @@
             return
         }
 
-        if (this != dispatchingNode) {
+        // TODO(b/158243568): For this attached check, and all of the following checks like this, we
+        //  should ideally be dispatching cancel to the sub tree with this node as it's root, and
+        //  we should remove the same sub tree from the tracker.  This will currently happen on
+        //  the next dispatch of events, but we shouldn't have to wait for another event.
+        if (pointerInputFilter.isAttached && this != dispatchingNode) {
             pointerInputFilter.onCustomEvent(event, downPass)
         }
 
-        // Call children recursively with the relevant changes.
-        children.forEach {
-            it.dispatchCustomEvent(event, relevantPointers, downPass, upPass, dispatchingNode)
+        if (pointerInputFilter.isAttached) {
+            // Call children recursively with the relevant changes.
+            children.forEach {
+                it.dispatchCustomEvent(event, relevantPointers, downPass, upPass, dispatchingNode)
+            }
         }
 
-        if (upPass != null && this != dispatchingNode) {
+        if (pointerInputFilter.isAttached && upPass != null && this != dispatchingNode) {
             pointerInputFilter.onCustomEvent(event, upPass)
         }
     }
diff --git a/ui/ui-core/src/test/java/androidx/ui/core/pointerinput/HitPathTrackerTest.kt b/ui/ui-core/src/test/java/androidx/ui/core/pointerinput/HitPathTrackerTest.kt
index d560d95..b4e1d14 100644
--- a/ui/ui-core/src/test/java/androidx/ui/core/pointerinput/HitPathTrackerTest.kt
+++ b/ui/ui-core/src/test/java/androidx/ui/core/pointerinput/HitPathTrackerTest.kt
@@ -964,15 +964,15 @@
 
         // Arrange.
 
-        val pif1 = PointerInputFilterMock(isAttached = true)
-        val pif2 = PointerInputFilterMock(isAttached = true)
-        val pif3 = PointerInputFilterMock(isAttached = true)
-        val pif4 = PointerInputFilterMock(isAttached = true)
-        val pif5 = PointerInputFilterMock(isAttached = true)
-        val pif6 = PointerInputFilterMock(isAttached = true)
-        val pif7 = PointerInputFilterMock(isAttached = true)
-        val pif8 = PointerInputFilterMock(isAttached = true)
-        val pif9 = PointerInputFilterMock(isAttached = true)
+        val pif1 = PointerInputFilterMock()
+        val pif2 = PointerInputFilterMock()
+        val pif3 = PointerInputFilterMock()
+        val pif4 = PointerInputFilterMock()
+        val pif5 = PointerInputFilterMock()
+        val pif6 = PointerInputFilterMock()
+        val pif7 = PointerInputFilterMock()
+        val pif8 = PointerInputFilterMock()
+        val pif9 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(1)
         val pointerId2 = PointerId(2)
@@ -1037,9 +1037,9 @@
     //  compositionRoot, root -> middle -> leaf
     @Test
     fun removeDetachedPointerInputFilters_1PathRootDetached_allRemovedAndCorrectCancels() {
-        val root = PointerInputFilterMock(isAttached = false)
-        val middle = PointerInputFilterMock(isAttached = false)
-        val leaf = PointerInputFilterMock(isAttached = false)
+        val root = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
 
@@ -1056,9 +1056,9 @@
     //  compositionRoot -> root, middle -> child
     @Test
     fun removeDetachedPointerInputFilters_1PathMiddleDetached_removesAndCancelsCorrect() {
-        val root = PointerInputFilterMock(isAttached = true)
-        val middle = PointerInputFilterMock(isAttached = false)
-        val child = PointerInputFilterMock(isAttached = false)
+        val root = PointerInputFilterMock()
+        val middle = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val child = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
@@ -1082,9 +1082,9 @@
     //  compositionRoot -> root -> middle, leaf
     @Test
     fun removeDetachedPointerInputFilters_1PathLeafDetached_removesAndCancelsCorrect() {
-        val root = PointerInputFilterMock(isAttached = true)
-        val middle = PointerInputFilterMock(isAttached = true)
-        val leaf = PointerInputFilterMock(isAttached = false)
+        val root = PointerInputFilterMock()
+        val middle = PointerInputFilterMock()
+        val leaf = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
@@ -1112,17 +1112,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots1Detached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = true)
-        val leaf1 = PointerInputFilterMock(isAttached = true)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock()
+        val leaf1 = PointerInputFilterMock()
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = true)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock()
 
-        val root3 = PointerInputFilterMock(isAttached = false)
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1176,17 +1176,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots1MiddleDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = true)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock()
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = true)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1242,17 +1242,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots1LeafDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = true)
-        val leaf1 = PointerInputFilterMock(isAttached = true)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock()
+        val leaf1 = PointerInputFilterMock()
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = true)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1309,17 +1309,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots2Detached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = false)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = true)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock()
 
-        val root3 = PointerInputFilterMock(isAttached = false)
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1366,17 +1366,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots2MiddlesDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = true)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1429,17 +1429,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots2LeafsDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = true)
-        val leaf1 = PointerInputFilterMock(isAttached = true)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock()
+        val leaf1 = PointerInputFilterMock()
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1492,17 +1492,17 @@
     //  compositionRoot, root3 -> middle3 -> leaf3
     @Test
     fun removeDetachedPointerInputFilters_3Roots3Detached_allRemovedAndCancelsCorrect() {
-        val root1 = PointerInputFilterMock(isAttached = false)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = false)
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = false)
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         hitPathTracker.addHitPath(PointerId(3), listOf(root1, middle1, leaf1))
         hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
@@ -1536,17 +1536,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots3MiddlesDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1594,17 +1594,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3Roots3LeafsDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = true)
-        val middle1 = PointerInputFilterMock(isAttached = true)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock()
+        val middle1 = PointerInputFilterMock()
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1655,17 +1655,17 @@
     @Test
     fun removeDetachedPointerInputFilters_3RootsStaggeredDetached_removesAndCancelsCorrect() {
 
-        val root1 = PointerInputFilterMock(isAttached = false)
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val root1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root2 = PointerInputFilterMock(isAttached = true)
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val root2 = PointerInputFilterMock()
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val root3 = PointerInputFilterMock(isAttached = true)
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val root3 = PointerInputFilterMock()
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1711,16 +1711,16 @@
     //   middle3 -> leaf3
     @Test
     fun removeDetachedPointerInputFilters_rootWith3MiddlesDetached_allRemovedAndCorrectCancels() {
-        val root = PointerInputFilterMock(isAttached = false)
+        val root = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         hitPathTracker.addHitPath(PointerId(3), listOf(root, middle1, leaf1))
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
@@ -1755,16 +1755,16 @@
     @Test
     fun removeDetachedPointerInputFilters_rootWith3Middles1Detached_removesAndCancelsCorrect() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle1 = PointerInputFilterMock(isAttached = true)
-        val leaf1 = PointerInputFilterMock(isAttached = true)
+        val middle1 = PointerInputFilterMock()
+        val leaf1 = PointerInputFilterMock()
 
-        val middle2 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = true)
+        val middle2 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock()
 
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1815,16 +1815,16 @@
     @Test
     fun removeDetachedPointerInputFilters_rootWith3Middles2Detached_removesAndCancelsCorrect() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle3 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = true)
+        val middle3 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1871,16 +1871,16 @@
     @Test
     fun removeDetachedPointerInputFilters_rootWith3MiddlesAllDetached_allMiddlesRemoved() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle1 = PointerInputFilterMock(isAttached = false)
-        val leaf1 = PointerInputFilterMock(isAttached = false)
+        val middle1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle2 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
+        val middle2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
-        val middle3 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val middle3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1923,13 +1923,13 @@
     @Test
     fun removeDetachedPointerInputFilters_middleWith3Leafs1Detached_correctLeafRemoved() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle = PointerInputFilterMock(isAttached = true)
+        val middle = PointerInputFilterMock()
 
-        val leaf1 = PointerInputFilterMock(isAttached = true)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = true)
+        val leaf1 = PointerInputFilterMock()
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock()
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -1975,13 +1975,13 @@
     @Test
     fun removeDetachedPointerInputFilters_middleWith3Leafs2Detached_correctLeafsRemoved() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle = PointerInputFilterMock(isAttached = true)
+        val middle = PointerInputFilterMock()
 
-        val leaf1 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = true)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock()
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -2024,13 +2024,13 @@
     @Test
     fun removeDetachedPointerInputFilters_middleWith3LeafsAllDetached_allLeafsRemoved() {
 
-        val root = PointerInputFilterMock(isAttached = true)
+        val root = PointerInputFilterMock()
 
-        val middle = PointerInputFilterMock(isAttached = true)
+        val middle = PointerInputFilterMock()
 
-        val leaf1 = PointerInputFilterMock(isAttached = false)
-        val leaf2 = PointerInputFilterMock(isAttached = false)
-        val leaf3 = PointerInputFilterMock(isAttached = false)
+        val leaf1 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf2 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
+        val leaf3 = PointerInputFilterMock(layoutCoordinates = LayoutCoordinatesStub(false))
 
         val pointerId1 = PointerId(3)
         val pointerId2 = PointerId(5)
@@ -2913,6 +2913,440 @@
         }
     }
 
+    @Test
+    fun dispatchChanges_pifRemovesSelfDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovesSelfDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovesSelfDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovesSelfDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovesSelfDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchChanges_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+        val layoutCoordinates = LayoutCoordinatesStub(true)
+        val pif: PointerInputFilter = PointerInputFilterMock(
+            pointerInputHandler =
+            spy(StubPointerInputHandler { changes, pass, _ ->
+                if (pass == removalPass) {
+                    layoutCoordinates.isAttached = false
+                }
+                changes
+            }),
+            layoutCoordinates = layoutCoordinates
+        )
+        hitPathTracker.addHitPath(PointerId(13), listOf(pif))
+
+        hitPathTracker.dispatchChanges(listOf(down(13)))
+
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            if (!passedRemovalPass) {
+                verify(pif).onPointerInput(any(), eq(it), any())
+                passedRemovalPass = it == removalPass
+            } else {
+                verify(pif, never()).onPointerInput(any(), eq(it), any())
+            }
+        }
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByParentDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByParentDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByParentDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByParentDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByParentDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchChanges_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+        val childLayoutCoordinates = LayoutCoordinatesStub(true)
+        val parentPif: PointerInputFilter = PointerInputFilterMock(
+            pointerInputHandler =
+            spy(StubPointerInputHandler { changes, pass, _ ->
+                if (pass == removalPass) {
+                    childLayoutCoordinates.isAttached = false
+                }
+                changes
+            })
+        )
+        val childPif: PointerInputFilter = PointerInputFilterMock(
+            layoutCoordinates = childLayoutCoordinates
+        )
+        hitPathTracker.addHitPath(PointerId(13), listOf(parentPif, childPif))
+
+        hitPathTracker.dispatchChanges(listOf(down(13)))
+
+        val removalPassIsDown =
+            when (removalPass) {
+                PointerEventPass.InitialDown -> true
+                PointerEventPass.PreDown -> true
+                PointerEventPass.PostDown -> true
+                else -> false
+            }
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            passedRemovalPass = passedRemovalPass || removalPassIsDown && it == removalPass
+            if (!passedRemovalPass) {
+                verify(childPif).onPointerInput(any(), eq(it), any())
+            } else {
+                verify(childPif, never()).onPointerInput(any(), eq(it), any())
+            }
+            passedRemovalPass = passedRemovalPass || !removalPassIsDown && it == removalPass
+        }
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByChildDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByChildDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByChildDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByChildDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchChanges_pifRemovedByChildDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchChanges_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+        val parentLayoutCoordinates = LayoutCoordinatesStub(true)
+        val parentPif: PointerInputFilter = PointerInputFilterMock(
+            layoutCoordinates = parentLayoutCoordinates
+        )
+        val childPif: PointerInputFilter = PointerInputFilterMock(
+            pointerInputHandler =
+            spy(StubPointerInputHandler { changes, pass, _ ->
+                if (pass == removalPass) {
+                    parentLayoutCoordinates.isAttached = false
+                }
+                changes
+            })
+        )
+        hitPathTracker.addHitPath(PointerId(13), listOf(parentPif, childPif))
+
+        hitPathTracker.dispatchChanges(listOf(down(13)))
+
+        val removalPassIsDown =
+            when (removalPass) {
+                PointerEventPass.InitialDown -> true
+                PointerEventPass.PreDown -> true
+                PointerEventPass.PostDown -> true
+                else -> false
+            }
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            passedRemovalPass = passedRemovalPass || !removalPassIsDown && it == removalPass
+            if (!passedRemovalPass) {
+                verify(parentPif).onPointerInput(any(), eq(it), any())
+            } else {
+                verify(parentPif, never()).onPointerInput(any(), eq(it), any())
+            }
+            passedRemovalPass = passedRemovalPass || removalPassIsDown && it == removalPass
+        }
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovesSelfDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovesSelfDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovesSelfDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovesSelfDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovesSelfDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchCustomMessage_pifRemovesSelfDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+
+        lateinit var dispatcher: CustomEventDispatcher
+
+        val layoutCoordinates = LayoutCoordinatesStub(true)
+
+        val dispatchingPif = PointerInputFilterMock(initHandler = { dispatcher = it })
+        val receivingPif = PointerInputFilterMock(
+             _, pointerEventPass ->
+                if (pointerEventPass == removalPass) {
+                    layoutCoordinates.isAttached = false
+                }
+            },
+            layoutCoordinates = layoutCoordinates
+        )
+
+        hitPathTracker.addHitPath(PointerId(13), listOf(dispatchingPif, receivingPif))
+
+        dispatcher.dispatchCustomEvent(object : CustomEvent {})
+
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            if (!passedRemovalPass) {
+                verify(receivingPif).onCustomEvent(any(), eq(it))
+                passedRemovalPass = it == removalPass
+            } else {
+                verify(receivingPif, never()).onCustomEvent(any(), eq(it))
+            }
+        }
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByParentDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByParentDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByParentDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByParentDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByParentDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchCustomMessage_pifRemovedByParentDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+        lateinit var dispatcher: CustomEventDispatcher
+
+        val layoutCoordinates = LayoutCoordinatesStub(true)
+
+        val dispatchingPif = PointerInputFilterMock(initHandler = { dispatcher = it })
+        val parentPif = PointerInputFilterMock(
+             _, pointerEventPass ->
+                if (pointerEventPass == removalPass) {
+                    layoutCoordinates.isAttached = false
+                }
+            }
+        )
+        val childPif = PointerInputFilterMock(
+            layoutCoordinates = layoutCoordinates
+        )
+
+        hitPathTracker.addHitPath(PointerId(13), listOf(dispatchingPif, parentPif, childPif))
+
+        dispatcher.dispatchCustomEvent(object : CustomEvent {})
+
+        val removalPassIsDown =
+            when (removalPass) {
+                PointerEventPass.InitialDown -> true
+                PointerEventPass.PreDown -> true
+                PointerEventPass.PostDown -> true
+                else -> false
+            }
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            passedRemovalPass = passedRemovalPass || removalPassIsDown && it == removalPass
+            if (!passedRemovalPass) {
+                verify(childPif).onCustomEvent(any(), eq(it))
+            } else {
+                verify(childPif, never()).onCustomEvent(any(), eq(it))
+            }
+            passedRemovalPass = passedRemovalPass || !removalPassIsDown && it == removalPass
+        }
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByChildDuringInitialDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.InitialDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByChildDuringPreUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByChildDuringPreDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PreDown
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByChildDuringPostUp_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostUp
+        )
+    }
+
+    @Test
+    fun dispatchCustomMessage_pifRemovedByChildDuringPostDown_noPassesReceivedAfterwards() {
+        dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+            PointerEventPass.PostDown
+        )
+    }
+
+    private fun dispatchCustomMessage_pifRemovedByChildDuringDispatch_noPassesReceivedAfterwards(
+        removalPass: PointerEventPass
+    ) {
+        lateinit var dispatcher: CustomEventDispatcher
+
+        val layoutCoordinates = LayoutCoordinatesStub(true)
+
+        val dispatchingPif = PointerInputFilterMock(initHandler = { dispatcher = it })
+        val parentPif = PointerInputFilterMock(
+            layoutCoordinates = layoutCoordinates
+        )
+        val childPif = PointerInputFilterMock(
+             _, pointerEventPass ->
+                if (pointerEventPass == removalPass) {
+                    layoutCoordinates.isAttached = false
+                }
+            }
+        )
+
+        hitPathTracker.addHitPath(PointerId(13), listOf(dispatchingPif, parentPif, childPif))
+
+        dispatcher.dispatchCustomEvent(object : CustomEvent {})
+
+        val removalPassIsDown =
+            when (removalPass) {
+                PointerEventPass.InitialDown -> true
+                PointerEventPass.PreDown -> true
+                PointerEventPass.PostDown -> true
+                else -> false
+            }
+        var passedRemovalPass = false
+        PointerEventPass.values().forEach {
+            passedRemovalPass = passedRemovalPass || !removalPassIsDown && it == removalPass
+            if (!passedRemovalPass) {
+                verify(parentPif).onCustomEvent(any(), eq(it))
+            } else {
+                verify(parentPif, never()).onCustomEvent(any(), eq(it))
+            }
+            passedRemovalPass = passedRemovalPass || removalPassIsDown && it == removalPass
+        }
+    }
+
     private fun areEqual(actualNode: NodeParent, expectedNode: NodeParent): Boolean {
         var check = true
 
@@ -2960,20 +3394,23 @@
 fun PointerInputFilterMock(
     initHandler: (CustomEventDispatcher) -> Unit = mock(),
     pointerInputHandler: PointerInputHandler = spy(StubPointerInputHandler()),
-    isAttached: Boolean = true
+    layoutCoordinates: LayoutCoordinates = LayoutCoordinatesStub(true),
+    onCustomEvent: (CustomEvent, PointerEventPass) -> Unit = mock()
 ): PointerInputFilter =
     spy(
         PointerInputFilterStub(
             pointerInputHandler,
-            initHandler
+            initHandler,
+            onCustomEvent
         ).apply {
-            layoutCoordinates = LayoutCoordinatesStub(isAttached)
+            this.layoutCoordinates = layoutCoordinates
         }
     )
 
 open class PointerInputFilterStub(
     val pointerInputHandler: PointerInputHandler,
-    val initHandler: (CustomEventDispatcher) -> Unit
+    val initHandler: (CustomEventDispatcher) -> Unit,
+    val customEventHandler: (CustomEvent, PointerEventPass) -> Unit
 ) : PointerInputFilter() {
 
     override fun onPointerInput(
@@ -2990,13 +3427,15 @@
         initHandler(customEventDispatcher)
     }
 
-    override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {}
+    override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {
+        customEventHandler(customEvent, pass)
+    }
 }
 
 internal data class TestCustomEvent(val value: String) : CustomEvent
 
 class LayoutCoordinatesStub(
-    override val isAttached: Boolean = true
+    override var isAttached: Boolean = true
 ) : LayoutCoordinates {
 
     override val size: IntSize