You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中如何实现悬停/拖拽经过行内项时的缩放效果

Jetpack Compose 实现Row内表情悬停/拖拽缩放方案

核心实现不要给每个子项单独加事件监听,直接在父容器统一处理指针位置,结合子项位置信息计算命中状态,流畅度比子项单独监听好很多,不会出现快速拖拽时事件丢失的问题。

核心实现步骤

  • 第一步:记录所有表情项的位置边界
    给每个表情项添加onGloballyPositioned修饰符,将每个项相对于父Row(或者Popup根容器)的边界矩形Rect存到状态列表里,注意坐标要统一转换到和指针事件相同的坐标系下,避免位置判断错位。

    val emojiRectList = remember { mutableStateListOf<Rect>() }
    // 单个表情项的修饰符
    Modifier.onGloballyPositioned { coordinates ->
        val rect = coordinates.boundsInParent()
        if (index < emojiRectList.size) emojiRectList[index] = rect
        else emojiRectList.add(rect)
    }
    
  • 第二步:统一监听父容器的指针位置
    给包裹所有表情的Row添加pointerInput修饰符,在指针事件协程里同时监听悬停、拖拽移动、指针移出三类事件,实时更新当前指针相对于父容器的坐标,指针移出父容器时将坐标置空,重置所有表情状态。

    val currentPointerOffset = remember { mutableStateOf<Offset?>(null) }
    Modifier.pointerInput(Unit) {
        awaitPointerEventScope {
            while (true) {
                val event = awaitPointerEvent()
                val change = event.changes.firstOrNull() ?: continue
                when (event.type) {
                    PointerEventType.Move, PointerEventType.Hover -> {
                        currentPointerOffset.value = change.position
                        change.consume()
                    }
                    PointerEventType.Exit, PointerEventType.Release -> {
                        currentPointerOffset.value = null
                    }
                }
            }
        }
    }
    
  • 第三步:按距离计算每个表情的缩放值,加动画过渡
    不要只给命中的表情加缩放,参考目标交互的逻辑,根据指针和每个表情中心的距离做递进缩放:离指针越近缩放比例越大,相邻表情也有轻微放大,过渡更自然。缩放用graphicsLayer实现,配合弹性动画避免生硬。

    // 单个表情的缩放计算
    val targetScale = if (currentPointerOffset.value == null) 1f else {
        val pointerPos = currentPointerOffset.value!!
        val itemCenter = emojiRectList[index].center
        val distance = sqrt(
            (itemCenter.x - pointerPos.x).pow(2) + (itemCenter.y - pointerPos.y).pow(2)
        )
        when {
            distance < 36.dp.toPx() -> 1.8f // 悬停命中项最大缩放
            distance < 72.dp.toPx() -> 1.3f // 相邻次近项缩放
            distance < 108.dp.toPx() -> 1.1f // 边缘轻微缩放
            else -> 1f // 其他项保持原大小
        }
    }
    val animatedScale by animateFloatAsState(
        targetValue = targetScale,
        animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
        label = "emojiScale"
    )
    // 应用缩放到表情
    Modifier.graphicsLayer {
        scaleX = animatedScale
        scaleY = animatedScale
    }
    

注意事项

  • 如果用Popup承载表情面板,记录子项位置时要注意坐标系转换,boundsInWindow()拿到的是全局窗口坐标,需要减去Popup自身的窗口偏移量,再和指针坐标做比对
  • 不要给单个表情项单独设置hoverable或draggable监听,快速滑动时事件会在子项间切换断层,父容器统一监听的方案流畅度高一个量级
  • 如果需要实现选中逻辑,加个超时判断即可:指针在某个表情的Rect范围内停留超过100ms,就标记该表情为当前选中项,触发对应回调

内容的提问来源于stack exchange,提问作者Jalson1982

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 11:24:15