许多应用程序提供了某种形式的多选行为,您可以通过拖动选择整个范围的元素。例如,Google相册允许您轻松选择一系列照片进行共享、添加到相册或删除。在本博文中,我们将实现类似的行为,目标是:
我们将采取以下步骤来实现这个最终结果:
-
实现一个基本网格
-
为网格元素添加选择状态
-
添加手势处理,以便我们可以通过拖动选择/取消选择元素
-
完善元素的外观,使其看起来像照片
只想看代码?这是完整代码!
我们将此网格实现为LazyVerticalGrid
,以便应用程序在所有屏幕尺寸上都能正常工作。较大的屏幕将显示更多列,较小的屏幕将显示较少列。
尽管我们目前只显示一个简单的彩色Surface
,但我们已经将元素称为photos
。只需这几行代码,我们就已经有了一个可以滚动的漂亮网格:
然而,一个简单的网格在我们的多选旅程中并没有带给我们太多。我们需要跟踪当前选择的项目以及我们当前是否处于选择模式,并使我们的元素反映出这种状态。
首先,让我们将网格项提取到它们自己的组合中,以反映它们的选择状态。这个组合将:
-
如果用户不处于选择模式,则为空
-
当用户处于选择模式且元素未被选择时,显示一个空的单选按钮
-
当用户处于选择模式且元素已被选择时,显示一个勾选标记
这个组合是_无状态的_,因为它不保存任何自己的状态。它只是反映您传递给它的状态。
为了使项目响应其选择状态,网格应该跟踪这些状态。此外,用户应该能够通过与网格中的项目交互来更改所选值。现在,当用户点击它时,我们将简单地切换项目的选择状态:
我们在一个集合中跟踪所选项目。当用户点击其中一个ImageItem
实例时,将添加或从集合中删除该项目的ID。
我们通过检查是否有任何当前选择的元素来定义是否处于选择模式。每当所选ID的集合发生变化时,此变量将自动重新计算。
通过这个添加,我们现在可以通过点击来添加和删除选择的元素:
现在我们正在跟踪状态,我们可以实现正确的手势,以添加和删除选择的元素。我们的要求如下:
-
通过长按元素进入选择模式
-
长按后拖动以添加或删除起始元素和目标元素之间的所有元素
-
在选择模式下,通过点击元素添加或删除元素
-
对已选择的元素进行长按不会产生任何效果
第二个要求是最棘手的。由于我们需要在拖动过程中调整所选ID的集合,我们需要将手势处理添加到网格中,而不是元素本身。我们需要进行自己的命中检测,以确定指针当前指向网格中的哪个元素。这可以通过LazyGridState
和拖动变化位置的组合来实现。
首先,让我们将LazyGridState
提取到懒网格之外,并将其传递给我们自定义的手势处理程序。这样我们就可以读取网格信息并在其他地方使用它。具体来说,我们可以使用它来确定用户当前指向网格中的哪个项目。
我们可以利用pointerInput
修饰符和detectDragGesturesAfterLongPress
方法来设置我们的拖动处理:
正如您在此代码片段中所看到的,我们在手势处理程序中内部跟踪initialKey
和currentKey
。我们需要在拖动开始时设置初始键,并在用户使用指针移动到不同元素时更新当前键。
让我们首先实现onDragStart
:
逐步解释这个方法,它:
-
查找指针下方的项目的键(如果有)。这表示用户长按并将从该元素开始进行拖动手势。
-
如果找到一个项目(用户指向网格中的一个元素),则检查该项目是否仍未选择(从而满足要求4)。
-
将初始键和当前键都设置为此键值,并主动将其添加到所选元素列表中。
我们必须自己实现帮助方法gridItemKeyAtPosition
:
对于网格中的每个可见项目,此方法检查hitPoint
是否落在其边界内。
现在我们只需要更新onDrag
lambda,该lambda将在用户在屏幕上移动指针时定期调用:
只有在设置了初始键时才处理拖动。根据初始键和当前键,此lambda将更新所选项目的集合。它确保选择初始键和当前键之间的所有元素。
通过这个设置,我们现在可以拖动选择多个元素:
最后,我们需要替换单个元素的可点击行为,以便在选择模式下添加/删除它们。这也是开始考虑此手势处理的可访问性的正确时间。我们使用onLongClick
语义属性来为辅助功能服务的用户提供替代选择机制,让他们通过长按元素进入选择模式。我们通过设置semantics
修饰符来实现这一点。
semantics
修饰符允许您覆盖或添加辅助功能服务用于在不依赖触摸的情况下与屏幕交互的属性和操作处理程序。大多数情况下,Compose系统会自动为您处理这一点,但在这种情况下,我们需要显式添加长按行为。
此外,通过为项目使用toggleable
修饰符(仅在用户处于选择模式时添加),我们确保Talkback可以向用户提供有关项目当前选择状态的信息。
如您在上面的屏幕录制中所见,我们目前无法拖动超过屏幕的顶部和底部边缘。这限制了选择机制的功能。我们希望在指针接近屏幕边缘时,网格可以滚动。此外,我们应该在用户将指针移动到屏幕边缘越近时滚动得更快。
期望的最终结果:
首先,我们将更改我们的拖动处理程序,以便根据与容器顶部或底部的距离设置滚动速度:
如您所见,我们根据阈值和距离更新滚动速度,并确保在拖动结束或取消时重置滚动速度。
现在,从手势处理程序更改此滚动速度值还没有任何效果。我们需要更新PhotoGrid
组合,以在值更改时开始滚动网格:
每当滚动速度变量的值更改时,LaunchedEffect
将重新触发,滚动将重新开始。
您可能想知道为什么我们没有直接从onDrag
处理程序中更改滚动级别。原因是onDrag
lambda 仅在用户实际移动指针时调用!因此,如果用户在屏幕上保持手指静止,滚动将停止。您可能在应用程序中注意到这个滚动错误,在那里您需要“刷”屏幕底部才能让它滚动。
通过这最后的添加,我们的网格行为非常稳定。然而,它看起来与我们在博客文章开头的示例不太相似。让我们确保网格项反映实际的照片:
<script src="https://gist.github.com/JolandaVerhoef/564843ab167b16f8adc1271c5e12c6f6.js"></script>
如你所见,我们扩展了照片列表,除了id外还有一个URL。使用该URL,我们可以在网格项中加载一张图片。在切换选择模式时,该图片的填充和角形状会发生变化,我们使用动画使这种变化平滑过渡。
在这个GitHub代码片段中查看完整代码。我们只用了不到200行代码,就创建了一个包含丰富交互的强大用户界面。
代码片段许可证:
版权所有 2023 Google LLC。
SPDX-License-Identifier: Apache-2.0