手势   您所在的位置:网站首页 安卓滚动条拖动 手势  

手势  

2023-08-26 15:50| 来源: 网络整理| 查看: 265

Compose 提供了多种 API,可帮助您检测用户互动生成的手势。API 涵盖各种用例:

其中一些级别较高,旨在覆盖最常用的手势。例如,clickable 修饰符可用于轻松检测点击,此外它还提供无障碍功能,并在点按时显示视觉指示(例如涟漪)。

还有一些不太常用的手势检测器,它们在较低级别提供更大的灵活性,例如 PointerInputScope.detectTapGestures 或 PointerInputScope.detectDragGestures,但不提供额外功能。

点按并按下

clickable 修饰符允许应用检测对已应用该修饰符的元素的点击。

@Composable fun ClickableSample() { val count = remember { mutableStateOf(0) } // content that you want to make clickable Text( text = count.value.toString(), modifier = Modifier.clickable { count.value += 1 } ) }

响应点按的界面元素示例

当需要更大灵活性时,您可以通过 pointerInput 修饰符提供点按手势检测器:

Modifier.pointerInput(Unit) { detectTapGestures( onPress = { /* Called when the gesture starts */ }, onDoubleTap = { /* Called on Double Tap */ }, onLongPress = { /* Called on Long Press */ }, onTap = { /* Called on Tap */ } ) } 滚动 注意:如果您想要显示项列表,请考虑使用 LazyColumn 和 LazyRow 而不是使用这些 API。LazyColumn 和 LazyRow 具有滚动功能,它们的效率远高于滚动修饰符,因为它们仅在需要时组合各个项。如需了解详情,请参阅列表和网格文档。 滚动修饰符

verticalScroll 和 horizontalScroll 修饰符提供一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScroll 和 horizontalScroll 修饰符,您无需转换或偏移内容。

@Composable fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }

响应滚动手势的简单垂直列表

借助 ScrollState,您可以更改滚动位置或获取当前状态。如需使用默认参数创建此列表,请使用 rememberScrollState()。

@Composable private fun ScrollBoxesSmooth() { // Smoothly scroll 100px on first composition val state = rememberScrollState() LaunchedEffect(Unit) { state.animateScrollTo(100) } Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .padding(horizontal = 8.dp) .verticalScroll(state) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } } 可滚动的修饰符

scrollable 修饰符与滚动修饰符不同,区别在于 scrollable 可检测滚动手势,但不会偏移其内容。必须有 ScrollableState,此修饰符才能正常工作。构造 ScrollableState 时,您必须提供一个 consumeScrollDelta 函数,该函数将在每个滚动步骤调用(通过手势输入、流畅滚动或快速滑动),并且增量以像素为单位。该函数必须返回所消耗的滚动距离,以确保在存在具有 scrollable 修饰符的嵌套元素时,可以正确传播相应事件。

注意:scrollable 修饰符不会影响它所应用到的元素的布局。这意味着,对元素布局或其子级进行的任何更改都必须通过由 ScrollableState 提供的增量进行处理。另外请务必注意,scrollable 不会考虑子级的布局,这意味着它无需测量子级,即可传播滚动增量。

以下代码段可检测手势并显示偏移量的数值,但不会偏移任何元素:

@Composable fun ScrollableSample() { // actual composable state var offset by remember { mutableStateOf(0f) } Box( Modifier .size(150.dp) .scrollable( orientation = Orientation.Vertical, // Scrollable state: describes how to consume // scrolling delta and update offset state = rememberScrollableState { delta -> offset += delta delta } ) .background(Color.LightGray), contentAlignment = Alignment.Center ) { Text(offset.toString()) } }

一种用于检测手指按下手势并显示手指位置数值的界面元素

嵌套滚动

Compose 支持嵌套滚动,可让多个元素对一个滚动手势做出回应。典型的嵌套滚动示例是在一个列表中嵌套另一个列表,而收起工具栏则是一种较为复杂的嵌套滚动情况。

自动嵌套滚动

简单的嵌套滚动无需您执行任何操作。启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理。

部分 Compose 组件和修饰符原生支持自动嵌套滚动,包括:verticalScroll、horizontalScroll、scrollable、Lazy API 和 TextField。这意味着,当用户滚动嵌套组件的内部子级时,之前的修饰符会将滚动增量传播到支持嵌套滚动的父级。

以下示例显示的元素应用了 verticalScroll 修饰符,而其所在的容器同样应用了 verticalScroll 修饰符。

val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White) Box( modifier = Modifier .background(Color.LightGray) .verticalScroll(rememberScrollState()) .padding(32.dp) ) { Column { repeat(6) { Box( modifier = Modifier .height(128.dp) .verticalScroll(rememberScrollState()) ) { Text( "Scroll here", modifier = Modifier .border(12.dp, Color.DarkGray) .background(brush = gradient) .padding(24.dp) .height(150.dp) ) } } } }

响应内部元素内外手势的两个嵌套垂直滚动界面元素

使用 nestedScroll 修饰符

如果您需要在多个元素之间创建高级协调滚动,可以使用 nestedScroll 修饰符定义嵌套滚动层次结构来提高灵活性。 如上一部分所述,某些组件具有内置的嵌套滚动支持。但是,对于不可自动滚动的可组合项(例如 Box 或 Column),此类组件上的滚动增量不会在嵌套滚动系统中传播,并且增量不会到达 NestedScrollConnection 或父组件。若要解决此问题,您可以使用 nestedScroll 向其他组件(包括自定义组件)提供此类支持。

嵌套滚动互操作性(从 Compose 1.2.0 开始)

当您尝试在可滚动的可组合项中嵌套可滚动的 View 元素时,可能会遇到问题,反之亦然。如果您滚动子项,到达其起始或结束边界并预期父项接续滚动,会发生明显的问题。不过,这种预期行为可能无法发生或无法以预期方式发生。

此问题是由可滚动的可组合项中内置的预期行为而导致。可滚动的可组合项有“默认嵌套滚动”规则,这意味着任何可滚动容器都必须通过 NestedScrollConnection 作为父项参与嵌套滚动链,并通过 NestedScrollDispatcher 作为子项参与嵌套滚动链。然后,当子项位于边界上时,子项将为父项推动嵌套滚动。例如,此规则允许 Compose Pager 和 Compose LazyRow 良好地配合工作。然而,当使用 ViewPager2 或 RecyclerView 完成互操作性滚动时,由于不会实现 NestedScrollingParent3,因此无法做到由子项到父项的连续滚动。

如要在可滚动的 View 元素与可滚动的可组合项之间实现双向嵌套的嵌套滚动互操作 API,您可以在下列场景中使用嵌套滚动互操作 API 来缓解这些问题。

包含子级 ComposeView 的协作式父级 View

协作式父级 View 已实现 NestedScrollingParent3,因此能够从参与协作的嵌套子级可组合项接收滚动增量。在这种情况下,ComposeView 将充当子项,并且需要(间接)实现 NestedScrollingChild3。比如,androidx.coordinatorlayout.widget.CoordinatorLayout 就是一个协作式父项的示例。

如果您需要在可滚动的 View 父级容器与嵌套的可滚动子级可组合项之间实现嵌套滚动互操作性,可以使用 rememberNestedScrollInteropConnection()。

rememberNestedScrollInteropConnection() 会允许并记住 NestedScrollConnection,后者支持在实现 NestedScrollingParent3 的 View 父项和 Compose 子项之间实现嵌套滚动互操作性。此方法应与 nestedScroll 修饰符结合使用。由于嵌套滚动在 Compose 端默认处于启用状态,因此您可以使用此连接在 View 端实现嵌套滚动,并在 Views 和可组合项之间添加必要的粘合逻辑。

一个常见的用例是使用 CoordinatorLayout、CollapsingToolbarLayout 和子级可组合项,具体如以下示例所示:

在您的 activity 或 fragment 中,您需要设置子级可组合项和必需的 NestedScrollConnection:

open class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() // Add the nested scroll connection to your top level @Composable element // using the nestedScroll modifier. LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) { items(20) { item -> Box( modifier = Modifier .padding(16.dp) .height(56.dp) .fillMaxWidth() .background(Color.Gray), contentAlignment = Alignment.Center ) { Text(item.toString()) } } } } } } } 包含子级 AndroidView 的父级可组合项

此场景涵盖了当您的父级可组合项包含子级 AndroidView 时,对 Compose 端嵌套滚动互操作 API 的实现。AndroidView 会实现 NestedScrollDispatcher,因为它充当 Compose 滚动父项的子项;还会实现 NestedScrollingParent3,因为它充当 View 滚动子项的父项。然后,Compose 父项将能够从嵌套的可滚动子级 View 接收嵌套滚动增量。

下面的示例展示了在此场景中如何使用 Compose 收起工具栏来实现嵌套滚动互操作性:

@Composable private fun NestedScrollInteropComposeParentWithAndroidChildExample() { val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() } val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } // Sets up the nested scroll connection between the Box composable parent // and the child AndroidView containing the RecyclerView val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Updates the toolbar offset based on the scroll to enable // collapsible behaviour val delta = available.y val newOffset = toolbarOffsetHeightPx.value + delta toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) return Offset.Zero } } } Box( Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) ) { TopAppBar( modifier = Modifier .height(ToolbarHeight) .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) } ) AndroidView( { context -> LayoutInflater.from(context) .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply { with(findViewById(R.id.main_list)) { layoutManager = LinearLayoutManager(context, VERTICAL, false) adapter = NestedScrollInteropAdapter() } }.also { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(it, true) } }, // ... ) } } private class NestedScrollInteropAdapter : Adapter() { val items = (1..10).map { it.toString() } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): NestedScrollInteropViewHolder { return NestedScrollInteropViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) ) } override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) { // ... } class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) { fun bind(item: String) { // ... } } // ... }

此示例展示了如何将该 API 与 scrollable 修饰符结合使用:

@Composable fun ViewInComposeNestedScrollInteropExample() { Box( Modifier .fillMaxSize() .scrollable(rememberScrollableState { // View component deltas should be reflected in Compose // components that participate in nested scrolling it }, Orientation.Vertical) ) { AndroidView( { context -> LayoutInflater.from(context) .inflate(android.R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }

最后,此示例展示了如何将嵌套滚动互操作 API 与 BottomSheetDialogFragment 结合使用,以实现成功的拖动和关闭操作:

class BottomSheetFragment : BottomSheetDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false) rootView.findViewById(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() LazyColumn( Modifier .nestedScroll(nestedScrollInterop) .fillMaxSize() ) { item { Text(text = "Bottom sheet title") } items(10) { Text( text = "List item number $it", modifier = Modifier.fillMaxWidth() ) } } } return rootView } } }

请注意,rememberNestedScrollInteropConnection() 会在附加到它的元素中安装 NestedScrollConnection。NestedScrollConnection 负责将增量从 Compose 级别传输到 View 级别。这使元素能够参与嵌套滚动,但不会自动启用元素滚动。对于不可自动滚动的可组合项(例如 Box 或 Column),此类组件上的滚动增量不会在嵌套滚动系统中传播,并且增量不会到达 rememberNestedScrollInteropConnection() 提供的 NestedScrollConnection,因此这些增量不会到达父级 View 组件。如要解决此问题,请确保将可滚动的修饰符也设置为这些类型的嵌套可组合项。如需了解详情,请参阅上文中关于嵌套滚动的部分。

包含子级 ComposeView 的非协作式父级 View

非协作式 View 无法在 View 端实现必要的 NestedScrolling 接口。请注意,这意味着这类 Views 不能直接支持嵌套滚动互操作性。非协作式 Views 是 RecyclerView 和 ViewPager2。

拖动

draggable 修饰符是向单一方向拖动手势的高级入口点,并且会报告拖动距离(以像素为单位)。

请务必注意,此修饰符与 scrollable 类似,仅检测手势。您需要保存状态并在屏幕上表示,例如通过 offset 修饰符移动元素:

var offsetX by remember { mutableStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" )

如果您需要控制整个拖动手势,请考虑改为通过 pointerInput 修饰符使用拖动手势检测器。

Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() offsetX += dragAmount.x offsetY += dragAmount.y } } ) }

通过手指按下操作拖动的界面元素

滑动

您可以使用 swipeable 修饰符拖动元素,释放后,这些元素通常朝一个方向定义的两个或多个锚点呈现动画效果。其常见用途是实现“滑动关闭”模式。

请务必注意,此修饰符不会移动元素,而只检测手势。您需要保存状态并在屏幕上表示,例如通过 offset 修饰符移动元素。

在 swipeable 修饰符中必须提供可滑动状态,且该状态可以通过 rememberSwipeableState() 创建和记住。此状态还提供了一组有用的方法,用于以程序化方式为锚点添加动画效果(请参阅 snapTo、animateTo、performFling 和 performDrag),同时为属性添加动画效果,以观察拖动进度。

可以将滑动手势配置为具有不同的阈值类型,例如 FixedThreshold(Dp) 和 FractionalThreshold(Float),并且对于每个锚点的起始与终止组合,它们可以是不同的。

为了获得更大的灵活性,您可以配置滑动越过边界时的 resistance,还可以配置 velocityThreshold,即使尚未达到位置 thresholds,velocityThreshold 仍将以动画方式向下一个状态滑动。

@Composable fun SwipeableSample() { val width = 96.dp val squareSize = 48.dp val swipeableState = rememberSwipeableState(0) val sizePx = with(LocalDensity.current) { squareSize.toPx() } val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states Box( modifier = Modifier .width(width) .swipeable( state = swipeableState, anchors = anchors, thresholds = { _, _ -> FractionalThreshold(0.3f) }, orientation = Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } .size(squareSize) .background(Color.DarkGray) ) } }

响应滑动手势的界面元素

多点触控:平移、缩放、旋转

如需检测用于平移、缩放和旋转的多点触控手势,您可以使用 transformable 修饰符。此修饰符本身不会转换元素,只会检测手势。

@Composable fun TransformableSample() { // set up all transformation states var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange rotation += rotationChange offset += offsetChange } Box( Modifier // apply other transformations like rotation and zoom // on the pizza slice emoji .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) // add transformable to listen to multitouch transformation events // after offset .transformable(state = state) .background(Color.Blue) .fillMaxSize() ) }

响应多点触控手势(平移、缩放和旋转)的界面元素

如果您需要将缩放、平移和旋转与其他手势结合使用,可以使用 PointerInputScope.detectTransformGestures 检测器。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有