随着新设备规格超越触控屏,Compose 的输入系统也已不断演进,以应对这一挑战。其分层架构为无缝支持从高级触控板手势到新颖硬件输入的所有内容奠定了基础。
通过清晰的架构细分和实操代码示例,您将了解输入事件的整个生命周期,从操作系统平台层一直到 Compose 手势检测器和界面组件。您将了解如何在整个堆栈中跟踪事件、针对新兴输入类型应用关键架构模式,并自信地在自己的应用中调试复杂的手势互动。
performTrackpadInput 等方法。这些 API 提供了熟悉的测试界面,可用于验证应用是否能正确处理非触控设备。转写文稿
大家好!
希望您在 Android 开发者峰会上度过一段美好时光,感谢您参加我们的讲座。
我是 Alex Vanyo,Compose 团队的软件工程师。跟我一起演讲的还有 Jeremy,他也是 Compose 团队的软件工程师。
我们一直在努力改进 Jetpack Compose 的输入系统,今天,我们将讨论如何将这些系统扩展到触控屏之外,以便更轻松地开箱即用地支持非触控输入。
广义上讲,输入是指用户与应用互动的任何方式。
对于移动应用,您首先想到的可能是触摸屏,但触摸屏绝对不是唯一的输入类型。
其他类型的输入包括实体键盘、鼠标、触控板、触控笔、控制器和旋转设备。
此外,我们还看到了新的输入类型,例如智能眼镜上的触控板、XR 中的眼动和手部追踪,以及 Wear 上的手指和手腕手势。
如果您想了解更广泛的输入类型,还有其他类型的输入,例如用于文本编辑的软件 IME、麦克风、摄像头和其他设备传感器。
您需要预料到用户会通过各种输入类型来使用您的应用。
原因之一是,运行应用的设备种类越来越多,而且这些设备的适应性也越来越强。
设备类型可能有最常用的输入法,但请务必不要假设特定设备类型意味着使用特定输入类型。
虽然 Chromebook 具有内置触控板和键盘,但这并不意味着这些输入类型是该外形规格所独有的。
您应该预料到实体键盘、触控板和鼠标也会在手机、平板电脑和电视上使用,有些电视甚至还配有触摸屏。
需要预期并支持多种输入类型的另一个重要原因是无障碍。
允许用户使用他们选择的输入设备,并允许他们根据需要切换输入设备,这是提高应用无障碍性和易用性的正确做法。
此外,根据管辖区,法律可能要求您的应用针对这些输入类型(尤其是键盘和鼠标等设备)提供特定的兼容行为。
简而言之,无论设备类型如何,您都应构建对不同输入类型的支持。
接下来,我们来看看 Jetpack Compose 如何帮助您的应用满足这些用户需求。
Compose 的理念通常是,这些不同的输入类型应该能够与标准组件搭配使用,无需您执行任何特殊操作。
然后,您应该拥有所需的工具,以便根据需要自定义和调整行为。
因此,除了这些初始输入类型之外,随着底层平台支持更多输入类型,我们计划在 Compose 组件中构建对这些输入类型的支持,只需更新 Compose 版本即可改进应用的行为。
在本演讲中,我们将介绍两个这样的示例,它们都是 Compose 的最新功能。
巧合的是,这两者都是类似触控板的设备,但正如您所见,它们的使用方式截然不同。
首先,我们来看看最近在笔记本电脑式触控板方面的一些工作。
触控板是一种基于指针的输入设备。
更具体地说,当您使用触控板时,您会获得二维坐标。
但与触摸屏不同的是,触控板上手指的这些坐标并不直接位于界面的坐标空间中。
相反,对于类似笔记本电脑的触控板,则需要转换为控制光标。
在这一转换过程中,您会遇到一系列复杂的平台转换:例如光标加速和手掌误触。
因此,在 Compose 端,您会得到一个与光标位置对应的指针,其行为通常类似于鼠标输入。
从 Android 平台,我们可以获取悬停信息、点击(包括次级或三级点击),所有这些信息都会封装到 MotionEvent 中。
为了解我们在最新版本 Compose 中如何改进了行为,我们先来大致了解一下 Compose 如何更紧密地处理这些指针事件。
我将把这个过程分为六个不同的阶段,我们将依次介绍所有这些阶段。
第一阶段是 MotionEvent 进入 Compose。
因此,Compose 在底层实际上只是一个视图:一个功能强大的复杂视图,但它只是一个视图,这意味着它需要在整个 Android 系统中处理指针输入。
因此,在 Compose 中,这意味着 Compose 位于常规 activity 的常规窗口中,它会接收 MotionEvent,即 Android 平台的 MotionEvent。
然后,Compose 需要处理这些事件,并执行您希望在 Compose 中围绕指针输入执行的所有操作。
第一阶段是将这些 MotionEvent 转换为 Compose 中与平台无关的内部表示法,我们称之为 PointerInputEvent。
这是一种内部类型,因此您不会直接看到它。
如果您想查看此处发生的所有细节,可以查看 MotionEventAdapter,但我们在这里将跳过这些细节。
现在,我们在 Compose 中有了与平台无关的 MotionEvent 表示法。
接下来,输入通常会随着时间的推移而发生。
您会接连收到一系列输入事件,因此,了解一段时间内发生的情况非常有用。
手势、速度跟踪、滑动检测等都需要了解每个时间点发生的情况。
因此,此处理流水线的下一个阶段是添加此时间信息,并根据连续的指针事件进行一些预处理。
在代码中,您可以将其视为获取当前 PointerInputEvent,将其与上一个 PointerInputEvent 发生的情况相结合,然后创建一个时间感知型 InternalPointerEvent。
顾名思义,这是另一种内部类型,但您可以将其视为不同指针 ID 到指针输入更改的映射,而 PointerInputChanges 是一种公共类型。
这是我们的第一个。
您可能会注意到,在考虑不同的指针 ID 时,由于我们现在可以跟踪时间,因此也可以跟踪指针 ID 的出现和消失时间。
不只一个。
如果您在触摸屏上使用多根手指,可能会同时有多个指针出现和消失,我们需要处理这种情况。
同样,如果您想深入了解细节,可以查看 PointerInputChangeEventProducer,它会执行所有这些操作,但我们不打算深入探讨。
我们还有很多内容要讲。
现在,我们有了输入的 Compose 表示法,并且它包含时间信息。
下一步是确定如何处理该事件。
我们应该将它发送到哪里?
这就需要进行命中路径测试。
假设我们有一个 MotionEvent 正在发送,而这个 ComposeView 占据了整个窗口,甚至整个屏幕。
我们需要确定发送目的地。
由于 Compose 只是一个视图,但 Compose 内部有大量内容,包括许多不同的节点、按钮以及您与之交互的内容,因此我们需要在 Compose 中完成所有工作,以确定将发生在特定位置的事件发送到何处,并确定哪个节点实际上对该事件感兴趣。
这些布局节点有坐标、边界和形状,因此我们需要确定哪个节点(如果有)被命中。
但这也并非最普遍的观点。
事实证明,我们喜欢将可互动的内容放入其他可互动的内容中,例如将按钮放入列表中。
结果发现,我们经常这样做。
因此,这里的一般情况是,特定输入事件会创建一个完整的命中路径,该路径会经过输入事件坐标命中的每个可交互节点。
因此,在本例中,嵌套层次结构中有三个。
因此,我们有了命中路径,但这还不够。
还记得我之前说过,我们有多个指针输入事件,可能有多个指针按下吗?
我们实际上需要一个完整的命中路径树。
当您有多个指针时,命中路径不只有一个。
因此,下一个阶段是创建和更新此命中路径树。
下面,我们来详细了解一下具体情况。
左侧是直观布局的节点,左下角是层次结构表示法,显示哪些节点位于其他节点内部,哪些节点是彼此的祖先节点或后代节点。
当指针按下时,我们会跟踪该命中路径(即通过树的完整路径),然后获取该特定指针的命中路径。
当我们获得第二个指针时,此命中路径就会变成一棵树。
它可能与另一个网络中的一些相同节点相交,但也可能与其他节点交互。
右侧是我们现在维护的命中路径树。
指针可能会随时出现和消失,因此我们需要自行维护此列表,以确保获得当前对每个正在进行的事件感兴趣的完整节点列表。
好了
现在,我们已经为这些输入事件维护了一个命中路径树,这意味着我们进入了下一个阶段。
现在,我们实际上已经知道要将这些指针输入事件发送到哪里,以及哪些节点对每个特定事件感兴趣。
现在我们知道了这一点,就可以进行坐标转换了。
事实证明,如果节点在其自己的局部坐标空间内获取输入事件,那就太好了,这样您就不必自己进行大量坐标数学运算。
因此,一旦我们知道了这一点,就可以获取指针输入事件,将其与感兴趣的节点的坐标相结合,然后最终创建该公共 PointerEvent,该事件将被分派给所有人和指针输入修饰符。
这就是第 6 阶段(即最后一个阶段)的内容,最后实际发送事件。
这是一个指针输入事件,该事件已经发生,并发送到节点以执行其想要执行的操作。
我们通过三个不同的阶段来实现这一目标。
我们有初始阶段、主要阶段和最终阶段。
之所以有三个不同的阶段,是为了让节点能够相互通信,并传达对特定指针输入事件的特定部分的兴趣,然后在分发过程中使用这些事件的不同部分。
因此,第一轮(即初始轮)是从祖先到后代。
例如,如果某个外部布局节点可能是滚动容器,并且它知道自己已开始滚动,并希望阻止所有指针事件到达其任何子级,那么这种方法就很有用。
这样,在初始传递中,它就可以使用该事件,然后子级就不会再收到任何事件。
第二次传递是主传递。
这是唯一一个从后代到祖先的关系。
顾名思义,这是执行操作的主要通道。
例如,后代会在此阶段使用按下事件,开始处理点击,如果您有嵌套的可点击项,后代就有机会在任何父级之前处理点击。
最后一次传递(即最终传递)与初始传递相同,都是从祖先节点到后代节点。
这样,子项就可以告知父项,父项使用了事件的一部分。
因此,如果子项开始处理点击,但父项开始滚动,子项可以检测到父项的操作,然后取消已开始处理的点击。
以上就是三个阶段和六个步骤。
这些是整个指针输入系统的基础。
所有手势处理、可滚动、可拖动、可点击以及任何自定义指针输入都基于此基础构建。
了解了这些之后,我们现在可以稍微谈谈我们在 Compose 中改进触控板行为支持的具体细节。
因此,在 Compose 1.12 之前,触控板输入事件会被报告为触摸指针类型,因为它是手指与触控板进行物理交互来控制光标。
不过,用户期望这种输入类型的行为更像实体鼠标。
因此,我们将指针类型从 PointerType.Touch 更改为 PointerType.Mouse。
做出此项更改后,用户在使用任何查看此信息的组件时,都会获得更加一致的鼠标和触控板体验。
例如,提示行为:您可能希望触控板和鼠标的提示行为相同。
我们对触控板做出的第二项重大更改与您在触控板上执行的多指手势有关。
以前,Compose 会通过 Android 平台巧妙但有点诡异的“假手指”概念来处理。
那么,让我们看看这意味着什么。
当您在触控板上使用两根手指进行平移时,Android 会生成一根虚拟的触摸屏手指,该手指会在光标所在的位置按下。
然后,当您在触控板上移动手指时,触摸屏上的虚拟手指会移动正确的距离和方向,以模拟所需行为,然后松开。
因此,在触控板上向上移动两根手指会转换为在触摸屏上向上移动一根虚拟手指,这反过来会导致向下滚动。
这里有一个有趣的地方:如果您像我一样使用反向触控板滚动功能,那么您会希望滚动方向实际上是相反的。
实现这一目标的方法是翻转假手指的移动方向。
现在,在触控板上双指向上移动会转换为在触摸屏上单指假手指向下移动,这反过来会导致向上滚动,与我们之前的情况相反。
扩缩的工作方式与此类似。
当您在触控板上使用两根手指进行缩放时,Android 会生成两根虚拟的触控屏手指,它们会按下,然后根据手势靠近或分开,以模拟所需的放大或缩小行为。
因此,从 Compose 1.12(针对平移)和 Compose 1.13(针对缩放)开始,我们现在会拦截这些虚假手指。
我们不会直接发送平台提供给我们的事件,而是将其重新解读为新的指针事件类型:PanStart、PanMove 和 PanEnd(用于平移),以及 ScaleStart、ScaleChange 和 ScaleEnd(用于缩放)。
我们会在前面提到的 Android 到 Compose 事件转换阶段执行此操作。
因此,我们会将 Fake Finger 事件转换为适当的类型,然后通过这些平台定义的手势发送到与光标对应的命中路径。
我们来看看为什么这很重要。
我们为什么要这样做?
假设这个方框是一张我们想要放大的精美图片。
当您在触控板上使用两根手指进行缩放时,两根假手指中的一根可能会错过该元素。
其点击路径不会与图片相交,并且会从边界之外开始。
因此,这会导致图片滚动,而不是缩放。
然后,如果图片外部还有另一个可滚动元素,那么该元素可能也会开始一起滚动。
这并非用户想要的结果。
他们希望扩大规模。
因此,我们现在不再发送这些虚假手指,而是仅向光标位置下方的元素发送缩放手势,从而实现正确的行为。
无论光标位于何处,也无论光标距离要缩放的元素边缘有多近,现在都可以正确缩放。
因此,对大家来说,好消息是,如果您使用这些基本构建块,这些重新解读的输入事件会默认在可滚动和可转换等组件中使用,以便为触控板输入提供较高保真度的行为。
如果您使用的是基于这些组件构建的任何内容,只需将 Compose 版本更新到最新版本,即可获得改进后的行为。
如果您正在编写自定义指针输入修饰符,我建议您了解一下这些新 API,并确保您的应用支持这些 API,以便为用户提供跨各种输入设备的最佳体验。
当然,我们还添加了测试 API,您也可以检查应用是否正确处理了所有这些不同的输入类型。
因此,performTrackpadInput 与现有的鼠标和触控测试 API 非常相似。
onNodeWithContentDescription("Photo")
.performTrackpadInput {
click(center)
pan(Offset(0f, 100f))
scale(0.9f)
}此外,无论用户在设备上使用何种输入设备,您都可以使用所有这些工具来验证应用是否正常运行。
希望这能让您更好地了解指针事件如何从触控板上的手指一直传递到应用中。
现在,我将把时间交给 Jeremy,让他来谈谈触控板的一种截然不同的使用方式。
谢谢。
好了
大家好,我是 Jeremy Walker。
我主要负责输入方面的工作,我想如果我向您介绍一些我们在 Compose 输入方面所做的新工作以及这些工作对您有何影响,应该会很有趣。
特别是这种间接输入类型,它是由于一种基于新设备规格的新输入方式而产生的:智能眼镜。
因此,当您从眼镜获取输入时,可以通过几种方式进行:您可以看着某物,可以使用语音,可以使用手势,但我将专门讨论触控板。
这些与 Alex 提到的那些略有不同,它们位于头部两侧,呈垂直方向,非常宽,而且非常短。
因此,在思考如何在 Compose 中实现这一功能时,我们设想了这样一个场景:如果用户面前有一个小按钮,他们需要移动光标来尝试触摸它,这显然不是一个很好的体验,对吧?
那么,我们该如何支持它呢?
提醒一下,这就是眼镜界面的样子。
它采用独特设计,可将内容无缝叠加在您的视野范围内。
但它非常简单,我们尽量保持简单。
我们来看一个示例,您可以看到这是一个列表。
它聚焦于中心位置的某个元素。
我们真正想做的是什么?
我们可能想要选择它,或者移到上一个或下一个。
我们不一定需要屏幕位置。
这个稍微复杂一点,但您可以看到基本上有三个选项:您可以挂断、取消静音或关闭视频。
但实际上,您需要选择该选项,或移至其他选项并选择该选项。
因此,我们认为,其实不需要直接映射到屏幕上的某个位置,或者在本例中,映射到您的眼睛。
所以,这就是它的实际效果。
举个例子,大家可以看到,我只是在元素之间移动,这是在眼镜界面上,这符合我们的需求。
您还可以点按它。
现在,我们来看看 Android,思考一下有哪些现有技术可以用来支持这一功能。
如果大家有,抱歉。
这里跳过了。
让我回去。
就是这里
好了
因此,如果您使用过其中一种设备,并将其与 Android 设备关联,您就会知道,您可以轻松地在这些设备之间切换界面元素。
例如,使用遥控器,您可以想象一下,或者如果您已将其与 Google TV 关联,并且您有一个完整的电影网格,您可以按箭头按钮转到您想要的电影,然后您可以按按钮选择它,即确定按钮。
如果您有游戏菜单,并且有此控制器,则可以使用方向键或摇杆移动到界面元素(例如菜单中的某个元素),然后选择该元素。
键盘也是如此。
这与我们在 Compose 中想要实现的效果不谋而合。
同样,这里列出了这些内容,以便您大致了解其外观。
再往前追溯,其实还有一种与电视配对的触控板,它使用的是一种名为“导航触控”的技术。
其理念是,x 和 y 坐标完全不与屏幕相关联。
它们仅用于手势检测。
因此,如果您向右滑动,可以想象这就像按下箭头键一样,可以切换到下一个。
如果您向左滑动,则会向左、向上和向下移动。
这样,我们基本上就得到了想要的结果。
您只需使用 x 和 y 来跟踪速度,以确定拖动和滑动等操作。因此,这基本上可以满足我们开始使用 Compose 的需求。
它并不完全准确,因为它有点像电视,所以我们需要确保不走电视路线,以后不弄乱电视,所以我们需要确保将它们分开,但我们稍后会讨论这个问题。
但它为我们提供了一个起点。
这样我们就可以使用这种触摸导航。
好了。
API 设计会是什么样子?
在 Compose 中,最常见或最普遍的输入是触摸,对吧?
因此,我们在这方面做了很多工作。
因此,我们必须弄清楚它与该主题的关系。
提醒一下,PointerEvent 如下所示。
您可以在这里看到,您有类型,即向上移动的向下移动,然后您有这个 PointerInputChange,它只是一个列表,您可以将其视为手指。
如果您放下三根手指,列表中就会有三项内容。
因此,我们希望它看起来像这样。
现在,这不是一个界面模块,而是一个比大多数开发者在处理输入时使用的模块低得多的模块。
它们将位于基础层之上。
所有手势检测器都在这里。
因为作为开发者,您不会关心所有这些细微的输入。
您只想知道是否发生了拖动、点击或滑动,然后根据这些信息执行操作。
因此,我们提供了一系列代码来为您完成这些操作,这些代码在后台由这些技术提供支持。
因此,我们开始思考,如何让 Foundation 中现有的代码尽可能轻松地支持这些新类型。
因此,我们创建了一个看起来几乎相同的新类型。
只是称为间接。
间接的意思是,它不是直接触摸屏幕。
因此,它会提醒您,这个功能即将推出,请不要从触控的角度来考虑。
想想看,我们在这里只是处理手势和在界面元素之间导航。
类型相同。
其中包含一个手指列表。
在本例中,它是 IndirectPointerInputChange。
我们做了这些,效果非常好。
但后来我们又发现了一个名为“主要方向运动轴”的项目。
这听起来有点拗口,但等我讲完,您就会明白,因为它的诞生过程相当有趣。
那么,它是如何诞生的呢?
同样,这个触控板位于头部一侧,宽而短。
所以,当时我们就在想,既然我们已经有了触控导航,
因此,我们可以向左滑动和向右滑动。
效果非常好。
但如果想想现在的触控板,对吧?
当您向左或向右移动时,您将在触控板上向左或向右移动界面内容。
如果您向上或向下移动,则会向上或向下滚动列表。
所以如果你尝试戴着眼镜做这个动作,就像这样,感觉还不错,对吧?
但如果这样做,体验就不太好。
我们发现,最自然的动作显然是轻触,但沿着眼镜侧边滑动也很自然。
因此,我们对用户进行了一系列研究,发现他们实际上期望在水平滑动时,内容不会水平移动,而是垂直移动。
您可以在脑海中想象一下,当您在这里滑动时,如果面前有一个列表,您可以向前浏览列表,也可以向后浏览列表。
因此,我们采用的是二维输入,并尝试以一维方式进行思考。
这就是“主要运动轴”这一概念的由来:您可以水平移动,而它会垂直移动。
因此,您可以在设备本身上指定。
你可以说,嘿,这可不是普通的触控板。
我想让主要方向为水平方向,这也是我想要沿其移动的主要轴,实际上是唯一轴。
这就是这个功能的由来。
好的。
因此,我们对这方面感到非常满意。
Compose 会处理所有这些。
您还可以采用这种不错的垂直实现。
您可以考虑一下。
因此,我们现在采用水平定义,只有前进和后退。
我们没有上、下、左、右之分。
我们只有前进和后退。
然后,如果您熟悉“下一个”和“上一个”焦点,就可以进行映射。
现在,我们只需移至下一个和上一个。
这种方法非常适合垂直视频。
而且也更美观。
它开箱即用,支持横向布局,对吧?
因为你只有一堆焦点项。
只需前往下一个或上一个,它就能同时适用于两者。
因此,在 Compose 中,我们定义并解决了所有这些问题,但如果设备本身能够说明这一点,那就太好了,因为它知道自己是否形状奇特,也知道自己是否只需要这个主轴,但它无法定义这一点。
因此,我们必须将这一概念带回平台,并说,好吧,我们需要在平台中定义这一点,以便我们可以将其标准化,并且这些设备本身可以指定它,这样 Compose 就可以完全支持它。
因此,在某些情况下,我们会在 Compose 中进行开发,在 Compose 中向前推进,并向后兼容,并将其与我们一起带入。
所以我们正是这样做的。
我们创建了这个新 API,以便您或设备可以指定该信息。
InputDevice.ViewBehavior
.getPrimaryDirectionalMotionAxis()还记得我之前提到触控输入(或触控导航)时,我说过它主要适用于电视。
我们不想破坏这些实现。
因此,我们添加了另一个字段来指定这种新的玻璃输入。
好的。
那么,最终这对您意味着什么?
这是手势检测器的一种更具体的版本。
// Glimmer Library
Box(
modifier =
Modifier.fillMaxSize()
.onIndirectPointerGesture(
enabled = true,
onSwipeForward = { /* onSwipeForward */ },
onSwipeBackward = { /* onSwipeBackward */ },
onClick = { /* onClick */ },
)
.focusTarget()
) {
// App()如果您使用的是 Foundation,则开箱即用,只需搭配手势检测器即可。
我们已经为您完成了这项工作,因此您无需执行任何操作。
但如果您想为眼镜开发应用,可以使用我们的 Glimmer 库,我们还提供了一个特殊的手势检测器,它只提供这些手势,您无需将它们组合在一起,只需使用这一个,就能获取点击、向前滑动和向后滑动。
好的。
我们已经介绍了 API 本身在平台和 Compose 中的工作原理。
测试呢?
因此,当您想要进行触摸测试时,触摸操作就是这样的。
@Test
clickTest {
rule.onNodeWithTag(defaultTag)
.performTouchInput
{ click(center) }
}您只需说出performTouchInput,然后做出手势。
手势有很多种,我们通过组合各种小事件来实现手势。
大致的工作原理是怎样的?
Compose UI 模块和 UI 测试模块会通过 MotionEvent 进行通信,至少在输入方面是这样。
此外,界面测试模块还使用了一种队列机制,该机制完全基于 MotionEvent。
我为什么要告诉你这些?
因为您无法以编程方式创建类似于间接输入事件的 MotionEvent,这对我来说太棒了,因为这意味着我必须重写模块和队列系统之间的所有通信。
但我做到了。
那么,这对您来说意味着什么呢?
如果您习惯了触控输入测试,那么这就是间接输入测试的结果。
只需在名称中添加“indirect”即可。
@Test
clickTest {
rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput (
IndirectPointerEventPrimaryDirectionalMotionAxis.X,
inputDeviceSize,
)
{ click(center) }
}现在,我确实传递了一些额外的参数。
您可能已经认出第一个,那是您指定水平位置的地方。
输入尺寸显然就是触摸板的尺寸。
现在,这并没有太大变化。
如果您查看源代码,就会发现我只是编写了一个扩展函数,它看起来就像这样。
@Test
clickTest {
rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput
{ click(center) }
}好的。
这应该能让您了解,我们为了在镜框或智能眼镜上支持触控板,做了多少工作。
但我们正在进行这些练习,我也在进行眼动追踪和手部追踪练习,所以未来还会有更多精彩内容。
那么,从宏观角度来看,我希望大家从我的分享中记住什么?
onNodeWithContentDescription("Photo")
.performTrackpadInput {
click(center)
pan(Offset(0f, 100f))
scale(0.9f)
}InputDevice.ViewBehavior
.getPrimaryDirectionalMotionAxis()// Glimmer Library
Box(
modifier =
Modifier.fillMaxSize()
.onIndirectPointerGesture(
enabled = true,
onSwipeForward = { /* onSwipeForward */ },
onSwipeBackward = { /* onSwipeBackward */ },
onClick = { /* onClick */ },
)
.focusTarget()
) {
// App()@Test
clickTest {
rule.onNodeWithTag(defaultTag)
.performTouchInput
{ click(center) }
}@Test
clickTest {
rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput (
IndirectPointerEventPrimaryDirectionalMotionAxis.X,
inputDeviceSize,
)
{ click(center) }
}@Test
clickTest {
rule.onNodeWithTag(defaultTag)
// .performTouchInput
.performIndirectPointerInput
{ click(center) }
}