Jeptpack Compose 官网教程学习笔记(五)番外-手势

手势

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

  • 其中一些级别较高,旨在覆盖最常用的手势。例如,clickable 修饰符可用于轻松检测点击,此外它还提供无障碍功能,并在点按时显示视觉指示(例如涟漪)
  • 还有一些不太常用的手势检测器,它们在较低级别提供更大的灵活性,例如 PointerInputScope.detectTapGesturesPointerInputScope.detectDragGestures,但不提供额外功能

点击

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

@Composable
fun ClickableSample() {
    val count = remember { mutableStateOf(0) }
    // 你想要实现点击效果的组件
    Text(
        text = count.value.toString(),
        modifier = Modifier.clickable { count.value += 1 }
    )
}
响应点按的界面元素示例

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

Modifier.pointerInput(Unit) {
    detectTapGestures(
        onPress = { /* 按下事件,所有手势事件的开始 */ },
        onDoubleTap = { /* 双击事件 */ },
        onLongPress = { /* 长按事件 */ },
        onTap = { /* 点击事件 */ }
    )
}

在使用pointerInput必须传递至少一个Key,当Key发生变化时会重新调用pointerInput,若不想其发生变化传递一个Unit即可

fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
) { ... }

可以看到block为挂起函数,因为等待手势必然是异步事件,需要使用协程等待手势事件的发生

PointerInputScope内我们可以在awaitPointerEventScope协程作用域内检测手势事件发生,详情请看官网介绍

当然级别高的手势检测器内部都是通过低级别的手势检测器完成,比如clickable内部是通过封装pointerInput完成点击事件的检测

fun Modifier.clickable(
   interactionSource: MutableInteractionSource,
   indication: Indication?,
   enabled: Boolean = true,
   onClickLabel: String? = null,
   role: Role? = null,
   onClick: () -> Unit
) = composed(
   factory = {
       val onClickState = rememberUpdatedState(onClick)
       ...
       val gesture = Modifier.pointerInput(interactionSource, enabled) {
           detectTapAndPress(
               onPress = { offset ->
                   if (enabled) {
                       handlePressInteraction(
                           offset,
                           interactionSource,
                           pressedInteraction,
                           delayPressInteraction
                       )
                   }
               },
               onTap = { if (enabled) onClickState.value.invoke() }
           )
       }
       Modifier
           .then(
               ...
           ).genericClickableWithoutGesture(
               gestureModifiers = gesture,
               interactionSource = interactionSource,
               indication = indication,
               enabled = enabled,
               onClickLabel = onClickLabel,
               role = role,
               onLongClickLabel = null,
               onLongClick = null,
               onClick = onClick
           )
   },
   inspectorInfo = ...
)

滚动

注意:如果您想要显示项列表,请考虑使用 LazyColumnLazyRow 而不是使用这些 API。LazyColumnLazyRow 具有滚动功能,它们的效率远高于滚动修饰符,因为它们仅在需要时组合各个项

滚动修饰符

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

@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,您可以更改滚动位置或获取当前状态

scrollable修饰符

scrollable 修饰符与滚动修饰符不同,区别在于 scrollable 可检测滚动手势,但不会偏移其内容。必须有 ScrollableState参数,此修饰符才能正常工作

构造 ScrollableState 时,必须提供一个 consumeScrollDelta 函数,该函数将在每个滚动步骤调用(通过手势输入、流畅滚动或快速滑动),并且增量以像素为单位。该函数会返回所消耗的滚动距离,以确保在存在具有 scrollable 修饰符的嵌套元素时,可以正确传播相应事件

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

@Composable
fun ScrollableSample() {
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                //不是 rememberScrollState,而且rememberScrollState也可以作为这里的参数 
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
//          可以通过offset实现滚动效果
//            .offset(0.dp, LocalDensity.current.run {
//                offset.toDp()
//            })
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}
检测手指按下手势并显示手指位置数值

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

嵌套滚动

Compose 支持嵌套滚动,可让多个元素对一个滚动手势做出回应。典型的嵌套滚动示例是在一个列表中嵌套另一个列表

自动嵌套滚动

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

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

以下示例显示的元素应用了 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 向其他组件(包括自定义组件)提供此类支持

拖动

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() 创建和记住。此状态还提供了一组有用的方法,用于以程序化方式为锚点添加动画效果,同时为属性添加动画效果,以观察拖动进度

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

阈值:当滑动元素超过阈值指定的值,此时松开手指,滑动元素会自动滑动至同方向上的下一个anchor(锚点)

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

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableSample() {
    val width = 96.dp
    val squareSize = 48.dp

    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    // Maps anchor points (in px) to states
    // 锚点元素不能空,而且value也必须唯一
    val anchors = mapOf(0f to 0, sizePx to 1)
    
    Box(
        modifier = Modifier
            .width(width)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(.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 检测器

suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)

centroid:质点,旋转中心
pan:位移量
zoom:放大系数
rotation:旋转角度

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容