【Jetpack Compose】可拖动小球 自动吸边

693 阅读1分钟

效果

0m1bm-y7323.gif

这里参考了官网的手势和动画来实现的

1.获取可拖动的宽高

通过BoxWithConstraints可以得到它整个的宽度和高度,这里maxWidth和maxHeight的单位是dp,通过 with(LocalDensity.current) { maxWidth.toPx() }这个方法可以把dp转换为像素

BoxWithConstraints(modifier = modifier.fillMaxSize()) {
    val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
    val height = with(LocalDensity.current) { maxHeight.toPx() }.toInt()
}

2. 边界检测

/**
 * 边界检测,防止小球拖动到屏幕外面
 * @param parentSize  父容器的宽度或者高度
 * @param size        小球的的宽度或者高度
 * @param offset      小球x或者y的偏移量
 */
fun sideDetect(parentSize: Int, size: Int, offset: Float): Float {
    return if (offset <= 0) {
        0f
    } else if (offset >= parentSize - size) {
        (parentSize - size).toFloat()
    } else {
        offset
    }
}

3. 拖动和吸边动画

fun Modifier.dragToSide(width: Int, height: Int): Modifier = composed {
    val animOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
    offset {
        //Log.i("hj", "animOffset = ${animOffset.value.toString()}")
        IntOffset(animOffset.value.x.roundToInt(), animOffset.value.y.roundToInt())
    }.pointerInput(Unit) {
        // 用于计算抛掷衰减。
        val decay = splineBasedDecay<Offset>(this)
        //对触摸事件和 Animatable 使用挂起函数。
        coroutineScope {
            while (true) {
                // 检测touch down 事件.
                val firstDownPointerId = awaitPointerEventScope { awaitFirstDown().id }
                val velocityTracker = VelocityTracker()
                // 停止正在进行的动画
                animOffset.stop()
                //touch down 之后的一系列touch 事件
                awaitPointerEventScope {
                    drag(firstDownPointerId) { change ->
                        //边界检测防止小球移除到屏幕外面
                        val offsetX = sideDetect(
                            width,
                            size.width,
                            animOffset.value.x + change.positionChange().x
                        )
                        val offsetY = sideDetect(
                            height,
                            size.height,
                            animOffset.value.y + change.positionChange().y
                        )
                        //Log.i("hj", "offsetX = $offsetX , offsetY = $offsetY ")
                        //更新动画的坐标
                        launch {
                            animOffset.snapTo(Offset(offsetX, offsetY))
                        }
                        velocityTracker.addPosition(
                            change.uptimeMillis,
                            change.position
                        )
                    }
                }
                //touch事件结束,准备动画
                val velocity = velocityTracker.calculateVelocity()
                val targetOffset = decay.calculateTargetValue(
                    typeConverter = Offset.VectorConverter,
                    initialValue = animOffset.value,
                    initialVelocity = velocity.toOffset()
                )
                // 动画在到达边界时停止
                animOffset.updateBounds(
                    lowerBound = Offset(0f, 0f),
                    upperBound = Offset(
                        (width - size.width).toFloat(),
                        (height - size.height).toFloat()
                    )
                )
                launch {
                    if (targetOffset.x.absoluteValue <= width / 2f) {
                        animOffset.animateTo(targetValue = Offset(0f, targetOffset.y))
                    } else {
                        animOffset.animateTo(
                            targetValue = Offset(
                                (width - size.width).toFloat(),
                                targetOffset.y
                            )
                        )
                    }
                }
            }
        }
    }
}

fun Velocity.toOffset() = Offset(x, y)

4.最后是使用这个Modifier的扩展方法

@Composable
fun SwipeToSideExample(modifier: Modifier = Modifier) {
    BoxWithConstraints(modifier = modifier.fillMaxSize()) {
        val width: Int = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
        val height = with(LocalDensity.current) { maxHeight.toPx() }.toInt()
        Box(
            modifier = Modifier
                .size(45.dp)
                .dragToSide(width, height)
                .background(color = MaterialTheme.colors.primary, shape = CircleShape)
        )
    }
}

注意:重点要注意modifier修饰符的顺序,我开始把dragToSide放在了background的后面,发现小球无法拖动。

然后是参考官网的也把offset写在pointerInput之后,会发现第一次可以拖动,小球位置发生变化后第二次就拖不动了

image.png 这个问题也是修饰符的顺序引起的,把offset放到pointerInput之前就好了。