You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复Kotlin Android Compose虚拟摇杆的方块显示问题?

虚拟摇杆显示为方块的修复方案及开发建议

我用Kotlin Android Compose开发虚拟摇杆,编写了Circle.kt和AnalogStick.kt组件,但运行后摇杆显示为方块套方块,拖动操作时该问题表现得十分明显,需要获取修复方案及相关开发建议。

Circle.kt 原代码

package io.github.kitswas.virtualgamepadmobile.ui.composables

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun Circle(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    colour: Color = Color.White,
    content: @Composable() (BoxScope.() -> Unit)
) {
    Box(
        modifier = modifier.clip(shape = CircleShape).background(Color.Transparent),
        contentAlignment = contentAlignment
    ) {
        Box(
            Modifier.background(colour).matchParentSize(),
            contentAlignment = contentAlignment
        ) {
            content()
        }
    }
}

@Preview(showBackground = false)
@Composable
fun CirclePreview() {
    Circle(
        modifier = Modifier
            .size(16.dp),
        contentAlignment = Alignment.Center
    ) {}
}

AnalogStick.kt 原代码

package io.github.kitswas.virtualgamepadmobile.ui.composables

import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import io.github.kitswas.VGP_Data_Exchange.GamepadReading
import io.github.kitswas.virtualgamepadmobile.ui.theme.darken
import io.github.kitswas.virtualgamepadmobile.ui.theme.lighten
import kotlin.math.roundToInt

enum class AnalogStickType {
    LEFT, RIGHT
}

@Composable
fun AnalogStick(
    modifier: Modifier = Modifier,
    ringColor: Color = MaterialTheme.colorScheme.outline,
    ringWidth: Dp = 4.dp,
    outerCircleColor: Color = darken(MaterialTheme.colorScheme.primary, 0.8f),
    outerCircleWidth: Dp = 4.dp,
    innerCircleColor: Color = lighten(MaterialTheme.colorScheme.primary, 0.2f),
    innerCircleRadius: Dp = 32.dp,
    gamepadState: GamepadReading,
    type: AnalogStickType,
) {
    Box(
        modifier = modifier,
        contentAlignment = Alignment.Center
    )
    {
        // 绘制外圈光环
        Circle(
            colour = ringColor,
            modifier = Modifier
                .size((innerCircleRadius + outerCircleWidth + ringWidth) * 2),
            contentAlignment = Alignment.Center
        ) {
            // 绘制外圆
            Circle(
                modifier = Modifier
                    .size((innerCircleRadius + outerCircleWidth) * 2),
                contentAlignment = Alignment.Center,
                colour = outerCircleColor,
            ) {
                var offsetX by remember { mutableStateOf(0f) }
                var offsetY by remember { mutableStateOf(0f) }
                var scaledOffsetX by remember { mutableStateOf(0f) }
                var scaledOffsetY by remember { mutableStateOf(0f) }

                // 绘制内圆(可拖动摇杆)
                Circle(
                    colour = innerCircleColor,
                    modifier = Modifier
                        .size(innerCircleRadius * 2)
                        .offset {
                            IntOffset(
                                (scaledOffsetX * outerCircleWidth.toPx()).roundToInt(),
                                (scaledOffsetY * outerCircleWidth.toPx()).roundToInt(),
                            )
                        }
                        .pointerInput(Unit) {
                            detectDragGestures(
                                onDragEnd = {
                                    offsetX = 0f
                                    offsetY = 0f
                                    scaledOffsetX = 0f
                                    scaledOffsetY = 0f
                                    when (type) {
                                        AnalogStickType.LEFT -> {
                                            gamepadState.LeftThumbstickX = scaledOffsetX
                                            gamepadState.LeftThumbstickY = scaledOffsetY
                                        }

                                        AnalogStickType.RIGHT -> {
                                            gamepadState.RightThumbstickX = scaledOffsetX
                                            gamepadState.RightThumbstickY = scaledOffsetY
                                        }
                                    }
                                },
                                onDrag = { change, dragAmount ->
                                    change.consume()
                                    offsetX += dragAmount.x
                                    offsetY += dragAmount.y
                                    // 缩放并限制在-1到1之间
                                    scaledOffsetX =
                                        (offsetX / outerCircleWidth.toPx()).coerceIn(-1f, 1f)
                                    scaledOffsetY =
                                        (offsetY / outerCircleWidth.toPx()).coerceIn(-1f, 1f)
                                    when (type) {
                                        AnalogStickType.LEFT -> {
                                            gamepadState.LeftThumbstickX = scaledOffsetX
                                            gamepadState.LeftThumbstickY = scaledOffsetY
                                        }

                                        AnalogStickType.RIGHT -> {
                                            gamepadState.RightThumbstickX = scaledOffsetX
                                            gamepadState.RightThumbstickY = scaledOffsetY
                                        }
                                    }
                                }
                            )
                        },
                ) {

                }
            }
        }
    }
}

@Preview(showBackground = false)
@Composable
fun AnalogStickPreview() {
    AnalogStick(
        gamepadState = GamepadReading(),
        type = AnalogStickType.LEFT,
    )
}

问题截图

虚拟摇杆显示为方块


修复方案

1. 修复Circle组件的显示问题

问题根源在于Circle组件的嵌套Box结构和Modifier顺序错误,导致背景无法被正确裁剪为圆形。将Circle组件简化,直接在同一个Box上组合clip和background:

修改后的Circle.kt代码:

package io.github.kitswas.virtualgamepadmobile.ui.composables

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun Circle(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.Center,
    colour: Color = Color.White,
    content: @Composable BoxScope.() -> Unit
) {
    Box(
        modifier = modifier
            .clip(CircleShape)
            .background(colour),
        contentAlignment = contentAlignment,
        content = content
    )
}

@Preview(showBackground = false)
@Composable
fun CirclePreview() {
    Circle(
        modifier = Modifier.size(16.dp),
        contentAlignment = Alignment.Center
    ) {}
}

2. 优化摇杆拖动逻辑(可选)

原代码中拖动偏移的计算逻辑存在范围限制不直观的问题,可优化为基于外圆半径的约束,确保内圆不会超出外圆边界:

修改AnalogStick.kt中的拖动部分(需添加import androidx.compose.ui.geometry.Offset):

// 替换原来的offsetX、offsetY相关逻辑
val maxOffset = outerCircleWidth.toPx()
var offset by remember { mutableStateOf(Offset.Zero) }

// 绘制内圆
Circle(
    colour = innerCircleColor,
    modifier = Modifier
        .size(innerCircleRadius * 2)
        .offset {
            IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
        }
        .pointerInput(Unit) {
            detectDragGestures(
                onDragEnd = {
                    offset = Offset.Zero
                    val (x, y) = Offset.Zero
                    when (type) {
                        AnalogStickType.LEFT -> {
                            gamepadState.LeftThumbstickX = x / maxOffset
                            gamepadState.LeftThumbstickY = y / maxOffset
                        }
                        AnalogStickType.RIGHT -> {
                            gamepadState.RightThumbstickX = x / maxOffset
                            gamepadState.RightThumbstickY = y / maxOffset
                        }
                    }
                },
                onDrag = { change, dragAmount ->
                    change.consume()
                    val newOffset = offset + dragAmount
                    // 限制内圆中心不超过外圆边界
                    val clampedOffset = newOffset.run {
                        if (getDistance() > maxOffset) {
                            normalize() * maxOffset
                        } else {
                            this
                        }
                    }
                    offset = clampedOffset
                    val scaledX = clampedOffset.x / maxOffset
                    val scaledY = clampedOffset.y / maxOffset
                    when (type) {
                        AnalogStickType.LEFT -> {
                            gamepadState.LeftThumbstickX = scaledX
                            gamepadState.LeftThumbstickY = scaledY
                        }
                        AnalogStickType.RIGHT -> {
                            gamepadState.RightThumbstickX = scaledX
                            gamepadState.RightThumbstickY = scaledY
                        }
                    }
                }
            )
        }
) { }

开发建议

  • 简化组件结构:避免不必要的嵌套Box,减少绘制层级,提升性能和可读性。
  • Modifier顺序规范:始终将clip放在background之前,确保背景被正确裁剪为目标形状。
  • 状态管理优化:如果GamepadReading是不可变数据类,建议使用MutableState包装其属性,或者将其设计为可变对象,避免直接修改属性导致的状态不一致。
  • 添加视觉反馈:给摇杆添加阴影(Modifier.shadow)、按压效果,提升交互体验。
  • 平滑动画:使用animateOffsetAsState实现摇杆回弹的平滑动画,替代直接重置offset。
  • 预览优化:在预览中添加背景色,方便直观查看组件效果。

内容的提问来源于stack exchange,提问作者Kitswas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 17:23:11