效果:
这里参考了官网的手势和动画来实现的
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之后,会发现第一次可以拖动,小球位置发生变化后第二次就拖不动了
这个问题也是修饰符的顺序引起的,把offset放到pointerInput之前就好了。