" Jetpack Compose - - Modifier 系列文章 "
?? 《 深入解析 Compose 的 Modifier 原理 - - Modifier、CombinedModifier 》
?? 《 深度解析 Compose 的 Modifier 原理 - - Modifier.composed()、ComposedModifier 》
?? 《 深入解析 Compose 的 Modifier 原理 - - Modifier.layout()、LayoutModifier 》
?? 《 深度解析 Compose 的 Modifier 原理 - - DrawModifier 》
?? 《 深度解析 Compose 的 Modifier 原理 - - PointerInputModifier 》
?? 《 深度解析 Compose 的 Modifier 原理 - - ParentDataModifier 》
其实原理性分析的文章,真的很难讲的通俗易懂,讲的简单了就没必要写了,讲的繁琐难懂往往大家也不乐意看,所以只能尽量想办法,找个好的角度(比如从 Demo 代码示例出发)慢慢带着大家去钻源码,如果确实能帮助到大家完全理解了文章所讲述到的源码理论,那就值了。
在正式开始分析 DrawModifier 之前,建议你先看看 【LayoutModifier 和 Modifier.layout 用法及原理】这篇文章,毕竟它是作为 Modifier 原理解析的第一篇文章,对你了解整个 Modifier 架构还是很有帮助的,或者说它是最基础的一篇文章,如果不熟悉,后面的系列 Modifier 你可能会看的比较费劲… …
在 Compose 中处理点击事件,最简单的方式就是:Modifier.clickable。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .clickable { // 单击处理,添加逻辑 }) { } } } } }
但 Modifier.clickable() 只能处理单击事件,如果你需要处理长按、双击等事件,则需要用到另外一个函数:Modifier.combinedClickable()。
class MainActivity : ComponentActivity() { @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .combinedClickable { }) { } } } } }
combinedClickable() 是 Modifier 的一个扩展函数:
@ExperimentalFoundationApi fun Modifier.combinedClickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, // 长按 onDoubleClick: (() -> Unit)? = null, // 双击 onClick: () -> Unit // 单击 )
从函数的字面意思就可以知道它是一个组合类型的 clickable,可以通过参数指定单击类型,如果不填写任何参数,那它跟 clickable 没有任何区别。
Modifier.clickable { } // 无参数情况下,等同 Modifier.combinedClickable { }
现在我们来测试下 combinedClickable 的用法:
class MainActivity : ComponentActivity() { @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .combinedClickable( onLongClick = { println("@@@ 长按了 Box") }, onDoubleClick = { println("@@@ 双击了 Box") } ) { // onClick() println("@@@ 单击了 Box") }) { } } } } }
上面只是满足点击监听的需求,如果需要复杂的触摸反馈定制(类似于 View 的 onTouchEvent),我们可以使用另外一个扩展函数:Modifier.pointerInput()。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .pointerInput(Unit) { detectTapGestures() } ) } } } }
我们来看看 detectTapGestures() 函数:
suspend fun PointerInputScope.detectTapGestures( onDoubleTap: ((Offset) -> Unit)? = null, // 双击 onLongPress: ((Offset) -> Unit)? = null, // 长按 onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture, // 触摸到即触发 onTap: ((Offset) -> Unit)? = null // 单击 )
它一样可以监听双击、长按、单击事件,唯独多了一个 onPress,那跟 combinedClickable 有什么区别?
Modifier.combinedClickable() 和 detectTapGestures() 的区别在于它们的级别或者说定制深度上是不同的,detectTapGestures() 是更底层的一种实现,实际上 Modifier.combinedClickable() 底层也是使用 detectTapGestures() 实现的。
@ExperimentalFoundationApi fun Modifier.combinedClickable(...) = composed(...) { Modifier.combinedClickable(...) } @ExperimentalFoundationApi fun Modifier.combinedClickable(...) = composed( factory = { ... ... val gesture = Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) { centreOffset.value = size.center.toOffset() detectTapGestures( onDoubleTap = ..., onLongPress = ..., onPress = ..., // onPress 并没有暴露出来 onTap = ... ) } ... ... )
如果还要做更复杂的触摸反馈且完全由我们自己控制,Compose 还提供了 awaitPointerEventScope(),让我们可以监听每个触摸事件:
class MainActivity : ComponentActivity() { @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .combinedClickable { } .pointerInput(Unit) { awaitPointerEventScope { // 这里面就要完全自定义触摸事件处理逻辑了 val down = awaitFirstDown() // 获取一个按压事件 } } ) } } } }
这样就可以在 awaitPointerEventScope 内部进行触摸事件处理了,但往往我们还会给 awaitPointerEventScope 套一层 forEachGesture。
class MainActivity : ComponentActivity() { @OptIn(ExperimentalFoundationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeBlogTheme { Box(Modifier.size(40.dp) .background(Color.Green) .combinedClickable { } .pointerInput(Unit) { forEachGesture { awaitPointerEventScope { val down = awaitFirstDown() } } } ) { } } } } }
forEachGesture() :循环检测每个事件,否则 awaitPointerEventScope() 监听一次点击之后就会失效。
其实 detectTapGestures 内部也是用 awaitPointerEventScope() 实现的:
suspend fun PointerInputScope.detectTapGestures(...) = coroutineScope { val pressScope = PressGestureScopeImpl(this@detectTapGestures) forEachGesture { awaitPointerEventScope { val down = awaitFirstDown() down.consume() ... ... } } }
Modifier.pointerInput() 内部使用的 detectXxxGesture() 几乎无一例外都是使用的该方案监听触摸事件。
至此,我们已经简单了解了 Modifier.pointerIput() 怎么使用,接下来开始分析定制的触摸反馈是怎么影响到界面展示的。
如果你已经看过 【 DrawModifier 原理解析】 的文章,那么对 PointerInputModifier 的处理位置应该会不陌生了。
我们直接看源码:
override var modifier: Modifier = Modifier set(value) { ... ... val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap -> if (mod is RemeasurementModifier) { mod.onRemeasurementAvailable(this) } toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // here if (mod is OnGloballyPositionedModifier) { getOrCreateOnPositionedCallbacks() += toWrap to mod } val wrapper = if (mod is LayoutModifier) { // Re-use the layoutNodeWrapper if possible. (reuseLayoutNodeWrapper(toWrap, mod) ?: ModifiedLayoutNode(toWrap, mod)).apply { onInitialize() updateLookaheadScope(mLookaheadScope) } } else { toWrap } wrapper.entities.addAfterLayoutModifier(wrapper, mod) wrapper } ... ... }
对 PointerInputModifier 的处理和 DrawModifier 一样:
toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // here
我们跟踪进去:
fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) { if (modifier is DrawModifier) { add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index) } if (modifier is PointerInputModifier) { add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index) } ... ... }
现在看就很明显了,PointerInputModifier 跟 DrawModifier 的存储方式一摸一样。在存储时也会将 PointerInputModifier 包装到一个链表中,后续新加的 PointerInputModifier 会用头插法插入链表头部。
那么分析到这里就可以有两个猜测:
1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?
// PointerInputModifier 对右边的 LayoutModifier 生效 // 想要对哪个 LayoutModifier 生效,就把 PointerInputModifier 写在哪个的左边 Modifier.pointerInput().padding()
2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?
// 两个 PointerInputModifier 影响着 LayoutModifier // 两个 PointerInputModifier 是父子关系,最左边的 PointerInputModifier 管理右边的 PointerInputModifier Modifier.pointerInput().pointerInput().size()
现在我们从源码角度来看看这两个猜测是否正确。
// LayoutNode.kt internal fun hitTest( pointerPosition: Offset, hitTestResult: HitTestResult<PointerInputFilter>, isTouchEvent: Boolean = false, isInLayer: Boolean = true ) { val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition) outerLayoutNodeWrapper.hitTest( LayoutNodeWrapper.PointerInputSource, positionInWrapped, hitTestResult, isTouchEvent, isInLayer ) }
hitTest() 实际上是做的检测工作,主要的作用是检查触摸事件应该下发给哪个组件,检测后再把事件分发到对应组件。
// LayoutNodeWrapper.kt fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> hitTest( hitTestSource: HitTestSource<T, C, M>, pointerPosition: Offset, hitTestResult: HitTestResult<C>, isTouchEvent: Boolean, isInLayer: Boolean ) { val head = entities.head(hitTestSource.entityType()) // 获取 PointerInputModifier 链表的头部 if (!withinLayerBounds(pointerPosition)) { ... ... } else if (isPointerInBounds(pointerPosition)) { // A real hit head.hit( hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer ) } else { ... ... } }
现在我们再来看
// LayoutNodeWrapper.kt private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit( hitTestSource: HitTestSource<T, C, M>, pointerPosition: Offset, hitTestResult: HitTestResult<C>, isTouchEvent: Boolean, isInLayer: Boolean ) { if (this == null) { hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer) } else { // 核心代码 hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) { next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer) } } }
首先需要了解一下:hitTestSource.contentFrom(this) 做了什么?-- 返回了 PointerInputModifier 链表的头节点内部包含的 PointerInputModifier 自身。
现在我们再往下跟踪:
// HitTestResult.kt fun hit(node: T, isInLayer: Boolean, childHitTest: () -> Unit) { hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest) }
又调用了 hitInMinimumTouchTarget():
// HitTestResult.kt fun hitInMinimumTouchTarget( node: T, // 1. 这里的 node 就是传进来的 PointInputModifier distanceFromEdge: Float, isInLayer: Boolean, childHitTest: () -> Unit ) { val startDepth = hitDepth hitDepth++ ensureContainerSize() values[hitDepth] = node // 2. 将 PointInputModifier 放进一个数组里,记录每个节点 distanceFromEdgeAndInLayer[hitDepth] = DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue resizeToHitDepth() childHitTest() // 3. 又调用了 childHitTest() hitDepth = startDepth }
看到了
所以看到这里,我们再看回刚才的两个猜想:
1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?
2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?
这两条猜想都是正确的!