" 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 = Modifierset(value) {... ...val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->if (mod is RemeasurementModifier) {mod.onRemeasurementAvailable(this)}toWrap.entities.addBeforeLayoutModifier(toWrap, mod) // hereif (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.ktinternal 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.ktfun <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 hithead.hit(hitTestSource,pointerPosition,hitTestResult,isTouchEvent,isInLayer)} else {... ...}
}
现在我们再来看 head.hit()
:
// LayoutNodeWrapper.ktprivate 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.ktfun hit(node: T, isInLayer: Boolean, childHitTest: () -> Unit) {hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
}
又调用了 hitInMinimumTouchTarget():
// HitTestResult.ktfun hitInMinimumTouchTarget(node: T, // 1. 这里的 node 就是传进来的 PointInputModifierdistanceFromEdge: Float,isInLayer: Boolean,childHitTest: () -> Unit
) {val startDepth = hitDepthhitDepth++ensureContainerSize()values[hitDepth] = node // 2. 将 PointInputModifier 放进一个数组里,记录每个节点distanceFromEdgeAndInLayer[hitDepth] =DistanceAndInLayer(distanceFromEdge, isInLayer).packedValueresizeToHitDepth()childHitTest() // 3. 又调用了 childHitTest()hitDepth = startDepth
}
childHitTest
是传进来的,往回找就会发现其实 childHitTest
就是:
看到了 next
?进行下一个节点的 hit 函数处理,典型的递归调用了。
所以看到这里,我们再看回刚才的两个猜想:
1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?
2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?
这两条猜想都是正确的!