深度解析 Compose 的 Modifier 原理 -- PointerInputModifier

在这里插入图片描述


" 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?

这两条猜想都是正确的!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/421760.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智慧文旅运营综合平台:重塑文化旅游产业的新引擎

目录 一、建设意义 二、包含内容 三、功能架构 四、典型案例 五、智慧文旅全套解决方案 - 210份下载 在数字化浪潮席卷全球的今天&#xff0c;智慧文旅运营综合平台作为文化旅游产业与信息技术深度融合的产物&#xff0c;正逐渐显现出其强大的生命力和广阔的发展前景。 该…

JAVA RPC Thrift基操实现与微服务间调用

一、Thrift 基操实现 1.1 thrift文件 namespace java com.zn.opit.thrift.helloworldservice HelloWorldService {string sayHello(1:string username) }1.2 执行命令生成Java文件 thrift -r --gen java helloworld.thrift生成代码HelloWorldService接口如下 /*** Autogene…

Android OpenGL EGL使用——自定义相机

如果要使用OpenGl来自定义相机&#xff0c;EGL还是需要了解下的。 可能大多数开发者使用过OpengGL但是不知道EGL是什么&#xff1f;EGL的作用是什么&#xff1f;这其实一点都不奇怪&#xff0c;因为Android中的GlSurfaceView已经将EGL环境都给配置好了&#xff0c;你一直在使用…

数据操作——Column 对象

Column 对象 1. 什么是Column对象 Column 表示了 Dataset 中的一个列, 并且可以持有一个表达式, 这个表达式作用于每一条数据, 对每条数据都生成一个值 2.Column对象如何创建 ’ 单引号 ’ 在 Scala 中是一个特殊的符号, 通过 ’ 会生成一个 Symbol 对象, Symbol 对象可以理…

《SPSS统计学基础与实证研究应用精解》视频讲解:SPSS数据查找

《SPSS统计学基础与实证研究应用精解》4.4 视频讲解 视频为《SPSS统计学基础与实证研究应用精解》张甜 杨维忠著 清华大学出版社 一书的随书赠送视频讲解4.4节内容。本书已正式出版上市&#xff0c;当当、京东、淘宝等平台热销中&#xff0c;搜索书名即可。本书旨在手把手教会使…

【QT+QGIS跨平台编译】之三:【OpenSSL+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文章目录 一、OpenSSL介绍二、OpenSSL配置三、Window环境下配置四、Linux环境下配置五、Mac环境下配置 一、OpenSSL介绍 OpenSSL是一个开放源代码的软件库包&#xff0c;应用程序可以使用这个包来进行安全通信&#xff0c;避免窃听&#xff0c;同时确认另一端连接者的身份。这…

Python文件操作和异常处理:高效处理数据的利器

文章目录 一、引言1.1 文件操作和异常处理对于编程的重要性1.2 Python作为实现文件操作和异常处理的强大工具 二、为什么学习文件操作和异常处理2.1 处理各种文件格式&#xff1a;从文本到图像到音频等2.2 确保代码的鲁棒性&#xff1a;有效处理异常情况 三、文件读取和写入3.1…

2023年12月 Scratch 图形化(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch图形化等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 在制作推箱子游戏时,地图是用数字形式储存在电脑里的,下图是一个推箱子地图,地图表示如下: 第一行(111111) 第二行(132231) 第三行(126621) 第四行( ) 第五行(152…

网络安全全栈培训笔记(56-服务攻防-数据库安全H2ElasticsearchCouchDBInfluxdb复现)

第56天 服务攻防-数据库安全&H2&Elasticsearch&CouchDB&Influxdb复现 知识点&#xff1a; 1、服务攻防数据库类型安全 2、influxdb,.未授权访问wt验证 3、H2 database-未授权访问-配置不当 4、CouchDB-权限绕过配合RCE-漏洞 5、ElasticSearch-文件写入&RC…

vue3前端开发,一篇文章看懂何谓pinia

vue3前端开发,pinia的基础练习第一节&#xff01; 前言&#xff0c;pinia是为了取代vuex而诞生的产品&#xff0c;它有一些改进。去掉了之前的mutations。只有一个action,既可以支持异步&#xff0c;又支持同步。还提供了解构函数&#xff0c;可以把返回的对象内部属性和方法直…

给创业者的一份忠告~创业就要选择低成本的轻资产创业项目

王健林继续出售资产&#xff0c;5天卖了4座万达广场&#xff0c;出售计划仍在继续..... 根据天眼查app最新消息&#xff0c;厦门殿前万达广场商业管理有限公司发生工商变更&#xff0c;2023至今已经转让10座万达广场。两年前万达旗下的核心子公司万达商管&#xff0c;向一群投资…

xxe漏洞之scms靶场漏洞

xxe-scms 代码审核 &#xff08;1&#xff09;全局搜索simplexml_load_string simplexml_load_string--将XML字符串解释为对象 &#xff08;2&#xff09;查看源代码 ID1 $GLOBALS[HTTP_RAW_POST_DATA]就相当于file_get_contents("php://input"); 因此这里就存…