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

109 阅读1分钟

陈小缘之前用view实现了一遍,自己打算延用其思路,用compose 复刻一下这个效果。

陈小缘之前用view实现的效果链接点这 Android实现圆弧滑动效果之FanLayout篇

要实现这个效果我们得掌握几个compoe的几个基础知识点:

  • 事件分发以及手势处理
  • 自定义layout 或者叫自定义viewgroup也可以

compose的事件分发优先级有PointerEventPass.Final , PointerEventPass.Main ,PointerEventPass.Initial,我们需要在down的时候消费事件并把事件优先级设为final , 不然触摸在子view上无法滑动,关键代码如下:

    @Composable
     fun FanLayout() {
        Surface(modifier = Modifier.fillMaxSize()) {
            val angle = remember {
                Animatable(0f)
            }
            Layout(
                content = {
                    List(10) {
                        Row(modifier = Modifier
                            .fillMaxWidth()
                            .requiredHeight(40.dp)
                            .graphicsLayer {
                                transformOrigin = TransformOrigin(0f, 0f)
                                rotationZ = (360 / 10).toFloat() * it + (angle.value % 360)
                            }
                            .padding(start = 100.dp)
                            .clickable {
                                Toast.makeText(this@MainActivity, "click $it", Toast.LENGTH_SHORT).show()
                            }
                            .background(Color((0xff000000..0xffffffff).random()))) {
                            repeat(10){
                                Image(
                                    painter = painterResource(id = R.mipmap.ic_launcher),
                                    contentDescription = "",
                                )
                            }

                        }
                    }
                },
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Gray)
                    .drawWithContent {
                        drawContent()
                        drawCircle(
                            Color.Green,
                            center = Offset(20.dp.toPx(), size.height / 2),
                            radius = 50.dp.toPx()
                        )
                    }
                    .pointerInput(Unit) {
                        val decay = splineBasedDecay<Offset>(this)
                        coroutineScope {
                            while (true) {
                                val firstDownPointerId =
                                    awaitPointerEventScope {
                                        awaitFirstDown(
                                            requireUnconsumed = false,
                                            pass = PointerEventPass.Final
                                        ).id
                                    }
                                val velocityTracker = VelocityTracker()
                                angle.stop()
                                awaitPointerEventScope {
                                    drag(firstDownPointerId) { change ->
                                        velocityTracker.addPosition(
                                            change.uptimeMillis,
                                            change.position,
                                        )
                                        launch {
                                            angle.snapTo(
                                                angle.value + getAngle(change),
                                            )
                                        }
                                    }
                                }
                                if (angle.value % 36 < 2) {
                                    angle.snapTo(
                                        angle.value - angle.value % 36,
                                    )
                                } else {
                                    angle.snapTo(
                                        angle.value + 36 - angle.value % 36,
                                    )
                                }
                            }
                        }
                    }) { measurables, constraints ->
                // 对 children 进行测量和放置
                val placeables = measurables.mapIndexed { index, measurable ->
                    measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
                }
                layout(constraints.maxWidth, constraints.maxHeight) {
                    // 摆放每个 child
                    placeables.forEachIndexed { index, placeable ->
                        placeable.placeRelative(
                            0,
                            constraints.maxHeight / 2
                        )
                    }
                }
            }
        }
    }

本来还想着处理下fling , 但无论怎么处理体验都不如不处理好,所以你懂得。