" Jetpack Compose - - Modifier 系列文章 "
????📑 《 深入解析 Compose 的 Modifier 原理 - - Modifier、CombinedModifier 》
????📑 《 深度解析 Compose 的 Modifier 原理 - - Modifier.composed()、ComposedModifier 》
????📑 《 深入解析 Compose 的 Modifier 原理 - - Modifier.layout()、LayoutModifier 》
????📑 《 深度解析 Compose 的 Modifier 原理 - - DrawModifier 》
????📑 《 深度解析 Compose 的 Modifier 原理 - - PointerInputModifier 》
其实原理性分析的文章,真的很难讲的通俗易懂,讲的简单了就没必要写了,讲的繁琐难懂往往大家也不乐意看,所以只能尽量想办法,找个好的角度(比如从 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 {
... ...
}
}
现在我们再来看 head.hit()
:
// 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
}
childHitTest
是传进来的,往回找就会发现其实 childHitTest
就是:
看到了 next
?进行下一个节点的 hit 函数处理,典型的递归调用了。
所以看到这里,我们再看回刚才的两个猜想:
1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?
2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?
这两条猜想都是正确的!