返回

Compose自定义View玩转丝滑的扇形滑动效果

Android

扇形滑动布局:性能优化和可定制性增强

简介

随着 Compose 框架的不断发展,开发者们对打造交互式、美观的 UI 体验的需求也在不断提升。扇形滑动布局是一种独特的 UI 元素,它允许用户以直观的方式浏览一组选项。在本篇文章中,我们将探讨如何通过优化性能和增强可定制性来完善扇形滑动布局的实现。

优化性能

我们之前的扇形滑动布局实现存在明显的卡顿现象,这是因为 UI 组合过程会消耗大量时间和资源。为了解决这个问题,我们采用了线程同步的方式进行优化。

我们知道,Compose 重新组合 UI 是一个耗时的过程。为了减少 UI 组合的次数,我们使用了一个单独的线程来管理所有滑动操作。只有在滑动结束时,才会触发 UI 组合,从而显著降低了 UI 组合的频率,提高了性能。

增强可定制性

之前的版本虽然实现了扇形滑动效果,但可定制性较差,只支持一个子元素,且元素大小固定。为了满足用户的个性化需求,我们对代码进行了重构,使其支持任意多个子元素,并允许动态调整子元素的大小。

实现原理

我们采用了以下步骤来实现扇形滑动布局:

  1. 使用 Compose 的 Canvas API 绘制扇形。
  2. 使用 StateFlow 管理扇形的旋转角度。
  3. 使用 GestureDetector 检测用户手势。
  4. 使用动画平滑扇形的旋转。

实现步骤

  1. 绘制扇形

我们在 Compose 中创建了一个自定义视图,使用 Canvas API 来绘制扇形。

  1. 管理旋转角度

我们创建了一个 StateFlow 变量来管理扇形的旋转角度。

  1. 检测用户手势

我们使用 GestureDetector 检测用户手势,并在用户滑动屏幕时更新 StateFlow 变量的值。

  1. 平滑旋转

我们使用动画平滑扇形的旋转,确保视觉效果流畅。

完整代码

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collect

@Composable
fun FanLayout(
    modifier: Modifier = Modifier,
    items: List<@Composable () -> Unit>,
) {
    val rotationAngle = remember { mutableStateOf(0f) }
    val draggableState = rememberDraggableState { delta ->
        rotationAngle.value -= delta
    }

    LaunchedEffect(rotationAngle) {
        rotationAngle.value = rotationAngle.value % 360f
    }

    Box(
        modifier = modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTapGestures(onTap = { offset ->
                    val clickedItem = items.indexOfFirst { item ->
                        val rect = Rect(itemCenter, itemSize)
                        rect.contains(offset)
                    }

                    if (clickedItem != -1) {
                        // Do something with the clicked item
                    }
                })
            }
            .draggable(
                state = draggableState,
                orientation = Orientation.Horizontal,
                onDragStopped = { velocity ->
                    // Do something with the velocity
                }
            )
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val radius = size.minDimension / 2
            val itemSize = 60.dp.toPx()
            val itemCenter = Offset(radius, radius)

            clipPath(path = arcTo(
                rect = Rect(itemCenter - Offset(radius, radius), itemCenter + Offset(radius, radius)),
                startAngleDegrees = rotationAngle.value - 90f,
                sweepAngleDegrees = 180f
            )) {
                rotate(degrees = rotationAngle.value) {
                    translate(left = radius - itemSize / 2, top = radius - itemSize / 2) {
                        items.forEachIndexed { index, item ->
                            translate(
                                left = index * itemSize,
                                top = radius * Math.sin(Math.toRadians(rotationAngle.value.toDouble()))
                            ) {
                                item()
                            }
                        }
                    }
                }
            }
        }
    }
}

常见问题解答

  1. 如何调整扇形的半径?

修改代码中 val radius = size.minDimension / 2 这行代码,将 size.minDimension 替换为所需的半径值即可。

  1. 如何调整扇形元素的大小?

修改代码中 val itemSize = 60.dp.toPx() 这行代码,将 60.dp 替换为所需的元素大小即可。

  1. 如何调整扇形元素之间的间距?

translate 函数中添加一个额外的 top 参数,并根据需要调整其值即可。

  1. 如何添加点击事件?

Canvas 函数的外层 Box 中使用 pointerInput 函数添加点击事件。

  1. 如何平滑扇形的旋转?

rotationAngleLaunchedEffect 中,将 rotationAngle.value = rotationAngle.value % 360f 修改为 rotationAngle.value = rotationAngle.value.animateTo(rotationAngle.value % 360f) 即可。

结论

通过优化性能和增强可定制性,我们成功地完善了扇形滑动布局的实现。现在,它不仅流畅、高效,还支持任意多个子元素,并允许用户动态调整子元素的大小。我们相信,这个优化后的布局将为开发者提供一个强大的工具,用于创建美观且交互性强的 UI 体验。