Compose 实现圆弧滑动效果之FanLayout ( 二 )

之前那篇文章还有很多细节没有处理,最近有时间了想再完善下,效果如上,之前的实现文章点这

Android Compose 自定义ViewGroup实现圆弧滑动效果之FanLayout - 掘金

之前的还有惯性滚动fling , 以及选择之后的回调没有处理。

所以打算再开一篇文章详细说下实现过程。

自定义布局

像这种效果我们还是用自定义布局来实现比较好。

我们来看下measure 和 layout。

kotlin 复制代码
            require(measurables.size > 3)
            val placeables = measurables.mapIndexed { index, measurable ->
                measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
            }
            layout(constraints.maxWidth, constraints.maxHeight) {
                placeables.forEachIndexed { index, placeable ->
                    if (index == 0)
                        placeable.place(
                            0,
                            constraints.maxHeight / 2 - placeable.height / 4, zIndex = 1f
                        )
                    else
                        placeable.place(
                            0,
                            constraints.maxHeight / 2
                        )
                }
            }

首先必须保证有三个子待测量的,也就是轴承和其他两个item (1个item 还转什么)。

index =0 表示轴承,轴承也就是那个最大的滑稽,轴承zIndex 设置为1,其他的item zIndex为默认值0,表示他会覆盖在全部item之上。

item的坐标 x, y都设置成一样,因为待会还要绕着圆心旋转。和view相比简单吧,是的,Comopse 自定义布局 的measure 和 layout 就是如此简单。

手势的处理

首先定义几个变量

kotlin 复制代码
       var angleAnimator = remember {
            Animatable(0f)
        }
        var angle by remember {
            mutableFloatStateOf(0f)
        }
        val velocityTracker = remember {
            VelocityTracker()
        }
        val splineBasedDecay: DecayAnimationSpec<Float> = with(LocalDensity.current) {
            remember {
                splineBasedDecay(this)
            }
        }

angle表示旋转的角度,velocityTracker用来监测速率 ,splineBasedDecay 是衰退动画的曲线,有个指数的但是感觉没splineBasedDecay效果好。

compose的手势在pointerInput方法进行处理,我们使用awaitEachGesture来处理每一次事件流

kotlin 复制代码
                    awaitEachGesture {
                        while (true) {
                            awaitFirstDown(requireUnconsumed = true).let { change ->
                                velocityTracker.addPosition(
                                    change.uptimeMillis,
                                    change.position,
                                )
                                if (angleAnimator.isRunning) {
                                    scope.launch {
                                        angleAnimator.stop()
                                    }
                                }
                                drag(change.id) { change ->
                                    velocityTracker.addPosition(
                                        change.uptimeMillis,
                                        change.position,
                                    )
                                    scope.launch {
                                        angle += getAngle(change)
                                    }
                                }
                                scope.launch {
                                    val velocityY = velocityTracker.calculateVelocity().y / 10
                                    if (velocityY.absoluteValue > 0f) {
                                        var lastValue = 0f
                                        angleAnimator = Animatable(0f)
                                        angleAnimator
                                            .animateDecay(
                                                initialVelocity = velocityY,
                                                splineBasedDecay
                                            ) {
                                                angle += (value - lastValue)
                                                lastValue = value
                                            }
                                    }
                                    angleAnimator = Animatable(angle)
                                    if (angle.absoluteValue % 36 < 18) {
                                        //不足18度回滚
                                        angleAnimator.animateTo(angle - (angle % 36)) {
                                            angle = value
                                        }
                                    } else {
                                        //大于18度,补足至36度
                                        angleAnimator.animateTo(angle + (36f.withSign(angle.sign) - angle % 36)) {
                                            angle = value
                                        }
                                    }
                                }

                            }
                        }
                    }

firstdown 的时候如果动画在运行则停止,down 和move 的时候用velocityTracker.addPosition 监控速率,在up 的时候就能获取到fling的速率了,剩下的就是一些边界值的判断了。

getAngle 方法 ,根据上一个触摸点和当前触摸点和圆心三个点计算滚动角度,原理是勾股定理和反三角函数。

kotlin 复制代码
private fun getAngle(inputChange: PointerInputChange): Float {
    val l: Float
    val t: Float
    val r: Float
    val b: Float
    val preX = inputChange.previousPosition.x
    val preY = inputChange.previousPosition.y
    val curX = inputChange.position.x
    val curY = inputChange.position.y
    if (preX > curX) {
        r = preX; l = curX
    } else {
        r = curX; l = preX
    }
    if (preY > curY) {
        b = preY; t = curY
    } else {
        b = curY; t = preY
    }
    val pA1: Float = abs(preX - pivotX)
    val pA2: Float = abs(preY - pivotY)
    val pB1: Float = abs(curX - pivotX)
    val pB2: Float = abs(curY - pivotY)
    val hypotenuse =
        sqrt((r - l).toDouble().pow(2.0) + (b - t).toDouble().pow(2.0)).toFloat()
    val lineA = sqrt(pA1.toDouble().pow(2.0) + pA2.toDouble().pow(2.0)).toFloat()
    val lineB = sqrt(pB1.toDouble().pow(2.0) + pB2.toDouble().pow(2.0)).toFloat()
    if (hypotenuse > 0 && lineA > 0 && lineB > 0) {
        val angle = Math.toDegrees(
            acos(
                (lineA.toDouble().pow(2.0) + lineB.toDouble().pow(2.0) - hypotenuse.toDouble()
                    .pow(2.0)) / (2 * lineA * lineB)
            )
        ).toFloat()
        if (!java.lang.Float.isNaN(angle)) {
            return if (isClockwise(inputChange)) angle else -angle
        }
    }
    return 0f
}

还有一点需要优化,在轴承上触摸移动时需要把事件禁止掉,另外搞一个pointerInput来处理。

kotlin 复制代码
        .pointerInput(Unit) {
                    awaitEachGesture {
                        awaitFirstDown(pass = PointerEventPass.Initial).also {
                            val dis = sqrt(
                                (it.position.y - pivotY).absoluteValue.pow(2) + (it.position.x - pivotX).absoluteValue.pow(
                                    2
                                )
                            )
                            if (dis < 100.dp.toPx()) {
                                it.consume()
                            }
                        }
                    }
                }

轴承区域是一个直径为100dp的圆,只要触摸点在这个区域内要把事件消费掉,怎么判断触摸点在这个区域内?这时候就需要用到初中数学知识了,只要触摸点和圆心距离小于直径我们就可以认为触摸点在圆内了,在圆内需要把事件消费掉。 计算触摸点和圆心的距离可以使用距离公式

原理其实也是勾股定理,不清理勾股定理的请立即打电话给初中数学老师。

Item选择时候的回调

默认以在屏幕中间那个为选择的,选择之后会标红,可以看上图那个gif,我们可以使用onGloballyPositioned 来处理,每次在屏幕的坐标变了这个方法都会回调。

kotlin 复制代码
                        .onGloballyPositioned {
                            if (it.boundsInParent().topLeft == Offset(
                                    0f,
                                    (height / 2f)
                                        .roundToInt()
                                        .toFloat()
                                )
                            ) {
                                curSelectedIndex = index
                            }

                        }

完整代码

kotlin 复制代码
fun FanLayout() {
    Surface(modifier = Modifier.fillMaxSize()) {
        var angleAnimator = remember {
            Animatable(0f)
        }
        var angle by remember {
            mutableFloatStateOf(0f)
        }
        val velocityTracker = remember {
            VelocityTracker()
        }
        val splineBasedDecay: DecayAnimationSpec<Float> = with(LocalDensity.current) {
            remember {
                splineBasedDecay(this)
            }
        }
        var curSelectedIndex by remember {
            mutableIntStateOf(0)
        }
        val scope = rememberCoroutineScope()
        val height = with(LocalDensity.current) {
            pivotY =
                LocalConfiguration.current.screenHeightDp.dp.roundToPx() / 2 + 20.dp.toPx()
                    .roundToInt()
            LocalConfiguration.current.screenHeightDp.dp.roundToPx()
        }
        /*    val imageBrush =
                ShaderBrush(ImageShader(ImageBitmap.imageResource(id = R.drawable.img)))*/
        Layout(
            content = {
                Image(
                    modifier = Modifier
                        .size(100.dp)
                        .clip(CircleShape),
                    painter = painterResource(id = R.drawable.img),
                    contentDescription = ""
                )
                List(10) { index ->
                    Row(modifier = Modifier
                        .graphicsLayer {
                            transformOrigin = TransformOrigin(0f, 0f)
                            rotationZ = (360 / 10).toFloat() * index + (angle)
                        }
                        .onGloballyPositioned {
                            if (it.boundsInParent().topLeft == Offset(
                                    0f,
                                    (height / 2f)
                                        .roundToInt()
                                        .toFloat()
                                )
                            ) {
                                curSelectedIndex = index
                            }

                        }
                        .fillMaxWidth()
                        .requiredHeight(50.dp)
                        .clickable {
                            if (angleAnimator.isRunning) {
                                return@clickable
                            }
                            scope.launch {
                                angleAnimator = Animatable(angle)
                                angleAnimator.animateTo(angle - 36) {
                                    angle = value
                                }
                            }

                        }
                        .background(if (curSelectedIndex == index) Color.Red else Color.White)) {
                        repeat(10) {
                            Image(
                                painter = painterResource(id = R.drawable.img),
                                contentDescription = ""
                            )
                        }

                    }
                }
            },
            modifier = Modifier
                .fillMaxSize()
                .background(Color.White)
                .pointerInput(Unit) {
                    awaitEachGesture {
                        awaitFirstDown(pass = PointerEventPass.Initial).also {
                            val dis = sqrt(
                                (it.position.y - pivotY).absoluteValue.pow(2) + (it.position.x - pivotX).absoluteValue.pow(
                                    2
                                )
                            )
                            if (dis < 100.dp.toPx()) {
                                it.consume()
                            }
                        }
                    }
                }
                .pointerInput(Unit) {
                    awaitEachGesture {
                        while (true) {
                            awaitFirstDown(requireUnconsumed = true).let { change ->
                                velocityTracker.addPosition(
                                    change.uptimeMillis,
                                    change.position,
                                )
                                if (angleAnimator.isRunning) {
                                    scope.launch {
                                        angleAnimator.stop()
                                    }
                                }
                                drag(change.id) { change ->
                                    velocityTracker.addPosition(
                                        change.uptimeMillis,
                                        change.position,
                                    )
                                    scope.launch {
                                        angle += getAngle(change)
                                    }
                                }
                                scope.launch {
                                    val velocityY = velocityTracker.calculateVelocity().y / 10
                                    if (velocityY.absoluteValue > 0f) {
                                        var lastValue = 0f
                                        angleAnimator = Animatable(0f)
                                        angleAnimator
                                            .animateDecay(
                                                initialVelocity = velocityY,
                                                splineBasedDecay
                                            ) {
                                                angle += (value - lastValue)
                                                lastValue = value
                                            }
                                    }
                                    angleAnimator = Animatable(angle)
                                    if (angle.absoluteValue % 36 < 18) {
                                        angleAnimator.animateTo(angle - (angle % 36)) {
                                            angle = value
                                        }
                                    } else {
                                        angleAnimator.animateTo(angle + (36f.withSign(angle.sign) - angle % 36)) {
                                            angle = value
                                        }
                                    }
                                }

                            }
                        }
                    }
                }) { measurables, constraints ->
            require(measurables.size > 3)
            val placeables = measurables.mapIndexed { index, measurable ->
                measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
            }
            layout(constraints.maxWidth, constraints.maxHeight) {
                placeables.forEachIndexed { index, placeable ->
                    if (index == 0)
                        placeable.place(
                            0,
                            constraints.maxHeight / 2 - placeable.height / 4, zIndex = 1f
                        )
                    else
                        placeable.place(
                            0,
                            constraints.maxHeight / 2
                        )
                }
            }
        }
    }
}

private var pivotX = 0
private var pivotY = 0
private fun getAngle(inputChange: PointerInputChange): Float {
    val l: Float
    val t: Float
    val r: Float
    val b: Float
    val preX = inputChange.previousPosition.x
    val preY = inputChange.previousPosition.y
    val curX = inputChange.position.x
    val curY = inputChange.position.y
    if (preX > curX) {
        r = preX; l = curX
    } else {
        r = curX; l = preX
    }
    if (preY > curY) {
        b = preY; t = curY
    } else {
        b = curY; t = preY
    }
    val pA1: Float = abs(preX - pivotX)
    val pA2: Float = abs(preY - pivotY)
    val pB1: Float = abs(curX - pivotX)
    val pB2: Float = abs(curY - pivotY)
    val hypotenuse =
        sqrt((r - l).toDouble().pow(2.0) + (b - t).toDouble().pow(2.0)).toFloat()
    val lineA = sqrt(pA1.toDouble().pow(2.0) + pA2.toDouble().pow(2.0)).toFloat()
    val lineB = sqrt(pB1.toDouble().pow(2.0) + pB2.toDouble().pow(2.0)).toFloat()
    if (hypotenuse > 0 && lineA > 0 && lineB > 0) {
        val angle = Math.toDegrees(
            acos(
                (lineA.toDouble().pow(2.0) + lineB.toDouble().pow(2.0) - hypotenuse.toDouble()
                    .pow(2.0)) / (2 * lineA * lineB)
            )
        ).toFloat()
        if (!java.lang.Float.isNaN(angle)) {
            return if (isClockwise(inputChange)) angle else -angle
        }
    }
    return 0f
}


private fun isClockwise(inputChange: PointerInputChange): Boolean {
    val px = inputChange.previousPosition.x
    val py = inputChange.previousPosition.y
    val x = inputChange.position.x
    val y = inputChange.position.y
    return if (abs(y - py) > abs(x - px)) x < pivotX != y > py else y < pivotY == x > px
}
相关推荐
Devil枫1 小时前
Kotlin高级特性深度解析
android·开发语言·kotlin
ChinaDragonDreamer1 小时前
Kotlin:2.1.20 的新特性
android·开发语言·kotlin
雨白12 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹13 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空15 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭15 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日16 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安16 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑16 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟21 小时前
CTF Web的数组巧用
android