From 1d5733392518730d60ec9ad7667d0814ed43d5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B1=E9=A3=8E?= Date: Thu, 18 Jun 2026 12:42:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=B0=86=E6=91=87=E6=9D=86?= =?UTF-8?q?=E9=9B=86=E6=88=90=E8=BF=9B=E5=B8=83=E5=B1=80=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../movtery/layer_controller/ControlEditor.kt | 100 ++- .../com/movtery/layer_controller/Layout.kt | 225 ++++--- .../layer_controller/data/JoystickData.kt | 79 +++ .../layer_controller/layout/ControlLayer.kt | 12 +- .../layer_controller/layout/ControlLayout.kt | 20 +- .../layer_controller/layout/Joystick.kt | 577 ++++++++++++++++++ .../observable/ObservableControlLayer.kt | 44 +- .../observable/ObservableControlLayout.kt | 59 +- .../observable/ObservableJoystickData.kt | 249 ++++++++ .../observable/ObservableSpecial.kt | 22 +- .../observable/ObservableWidget.kt | 5 + .../observable/TouchProcessor.kt | 48 +- .../observable/TouchSession.kt | 5 +- .../src/main/assets/default_layout.json | 1 - .../ui/control/gamepad/Joystick.kt | 2 +- .../ui/control/joystick/JoystickControl.kt | 89 +-- .../ui/control/joystick/JoystickDirection.kt | 37 -- .../ui/control/joystick/_Movement.kt | 1 + .../ui/screens/game/GameScreen.kt | 208 +------ .../game/elements/GameMenuSubscreen.kt | 19 +- .../elements/JoystickManageOperationUI.kt | 361 ----------- .../main/control_editor/ControlEditor.kt | 173 +++--- .../control_editor/ControlEditorElements.kt | 66 +- .../main/control_editor/PreviewLayout.kt | 82 +-- ...tStyleDialog.kt => JoystickStyleEditor.kt} | 256 ++------ .../edit_layer/EditControlLayerDialog.kt | 8 - .../edit_widget/BasicInfoEdit.kt | 155 ++++- .../edit_widget/EditJoystickSettings.kt | 108 ++++ .../edit_widget/EditWidgetDialog.kt | 32 +- .../edit_widget/EditWidgetStyle.kt | 13 +- .../edit_widget/SelectedWidgetData.kt | 2 +- .../edit_widget/_DialogNavigation.kt | 3 + .../viewmodel/EditorViewModel.kt | 20 +- .../viewmodel/GamepadViewModel.kt | 2 +- .../viewmodel/JoystickMovementViewModel.kt | 13 +- .../src/main/res/drawable/ic_joystick.xml | 24 + .../src/main/res/values-ar/strings.xml | 9 - .../src/main/res/values-es/strings.xml | 16 - .../src/main/res/values-in/strings.xml | 17 - .../src/main/res/values-ja/strings.xml | 17 - .../src/main/res/values-ko/strings.xml | 9 - .../src/main/res/values-pt-rBR/strings.xml | 17 - .../src/main/res/values-ru/strings.xml | 17 - .../src/main/res/values-tr/strings.xml | 17 - .../src/main/res/values-vi/strings.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 19 +- .../src/main/res/values-zh-rTW/strings.xml | 19 +- .../src/main/res/values/strings.xml | 19 +- 48 files changed, 1783 insertions(+), 1514 deletions(-) create mode 100644 LayerController/src/main/java/com/movtery/layer_controller/data/JoystickData.kt create mode 100644 LayerController/src/main/java/com/movtery/layer_controller/layout/Joystick.kt create mode 100644 LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt delete mode 100644 ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickDirection.kt rename ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/{EditStyleDialog.kt => JoystickStyleEditor.kt} (52%) create mode 100644 ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditJoystickSettings.kt create mode 100644 ZalithLauncher/src/main/res/drawable/ic_joystick.xml diff --git a/LayerController/src/main/java/com/movtery/layer_controller/ControlEditor.kt b/LayerController/src/main/java/com/movtery/layer_controller/ControlEditor.kt index 8393330d5..5a1cd6970 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/ControlEditor.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/ControlEditor.kt @@ -67,10 +67,12 @@ import com.movtery.layer_controller.data.ButtonSize import com.movtery.layer_controller.data.MAX_SIZE_PERCENTAGE import com.movtery.layer_controller.data.MIN_SIZE_DP import com.movtery.layer_controller.data.MIN_SIZE_PERCENTAGE +import com.movtery.layer_controller.layout.JoystickButton import com.movtery.layer_controller.layout.TextButton import com.movtery.layer_controller.observable.ObservableButtonStyle import com.movtery.layer_controller.observable.ObservableControlLayer import com.movtery.layer_controller.observable.ObservableControlLayout +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.layer_controller.observable.ObservableWidget import com.movtery.layer_controller.utils.getWidgetPosition import com.movtery.layer_controller.utils.snap.GuideLine @@ -94,7 +96,7 @@ import kotlin.math.roundToInt fun ControlEditorLayer( observedLayout: ObservableControlLayout, selectedWidget: ObservableWidget?, - onButtonTap: (data: ObservableWidget, layer: ObservableControlLayer) -> Unit, + onButtonTap: (data: ObservableWidget, layer: ObservableControlLayer?) -> Unit, onBackgroundClick: () -> Unit, floatingButtons: @Composable RowScope.() -> Unit, enableSnap: Boolean, @@ -338,10 +340,14 @@ fun ControlEditorLayer( change.consume() if (isTopLeft) { val newTL = dragTL + dragAmount - val finalTL = Offset( + var finalTL = Offset( newTL.x.coerceIn(maxOf(0f, dragBR.x - maxWidth), dragBR.x - minWidth), newTL.y.coerceIn(maxOf(0f, dragBR.y - maxHeight), dragBR.y - minHeight) ) + if (widget is ObservableJoystickData) { + val size = minOf(dragBR.x - finalTL.x, dragBR.y - finalTL.y) + finalTL = Offset(dragBR.x - size, dragBR.y - size) + } dragTL = finalTL widget.movingOffset = finalTL val finalSize = IntSize( @@ -351,10 +357,14 @@ fun ControlEditorLayer( updateSizeAndPos(finalTL, finalSize) } else { val newBR = dragBR + dragAmount - val finalBR = Offset( + var finalBR = Offset( newBR.x.coerceIn(dragTL.x + minWidth, minOf(screenSize.width.toFloat(), dragTL.x + maxWidth)), newBR.y.coerceIn(dragTL.y + minHeight, minOf(screenSize.height.toFloat(), dragTL.y + maxHeight)) ) + if (widget is ObservableJoystickData) { + val size = minOf(finalBR.x - dragTL.x, finalBR.y - dragTL.y) + finalBR = Offset(dragTL.x + size, dragTL.y + size) + } dragBR = finalBR val finalSize = IntSize( (finalBR.x - dragTL.x).roundToInt(), @@ -469,11 +479,11 @@ private fun ControlWidgetRenderer( snapMode: SnapMode, localSnapRange: Dp, snapThresholdValue: Dp, - onButtonTap: (data: ObservableWidget, layer: ObservableControlLayer) -> Unit, + onButtonTap: (data: ObservableWidget, layer: ObservableControlLayer?) -> Unit, drawLine: (ObservableWidget, List) -> Unit, onLineCancel: (ObservableWidget) -> Unit ) { - val allWidgetsMap = remember { mutableStateMapOf>() } + val allWidgetsMap = remember { mutableStateMapOf>() } val snapInAllLayers1 by rememberUpdatedState(snapInAllLayers) @Composable @@ -482,30 +492,57 @@ private fun ControlWidgetRenderer( layer: ObservableControlLayer, isPressed: Boolean ) { - TextButton( - isEditMode = true, - data = data, - allStyles = styles, - screenSize = screenSize, - isDark = isDark, - enableSnap = enableSnap, - snapMode = snapMode, - localSnapRange = localSnapRange, - getOtherWidgets = { - allWidgetsMap - .filter { (layer1, _) -> - snapInAllLayers1 || layer1 == layer + when (data) { + is ObservableJoystickData -> { + JoystickButton( + data = data, + screenSize = screenSize, + isDark = isDark, + enableSnap = enableSnap, + snapMode = snapMode, + localSnapRange = localSnapRange, + getOtherWidgets = { + allWidgetsMap + .filter { (layer1, _) -> + snapInAllLayers1 || layer1 == layer + } + .values.flatten().filter { it != data } + }, + snapThresholdValue = snapThresholdValue, + drawLine = drawLine, + onLineCancel = onLineCancel, + onTapInEditMode = { + onButtonTap(data, layer) + } + ) + } + else -> { + TextButton( + isEditMode = true, + data = data, + allStyles = styles, + screenSize = screenSize, + isDark = isDark, + enableSnap = enableSnap, + snapMode = snapMode, + localSnapRange = localSnapRange, + getOtherWidgets = { + allWidgetsMap + .filter { (layer1, _) -> + snapInAllLayers1 || layer1 == layer + } + .values.flatten().filter { it != data } + }, + snapThresholdValue = snapThresholdValue, + drawLine = drawLine, + onLineCancel = onLineCancel, + isPressed = isPressed, + onTapInEditMode = { + onButtonTap(data, layer) } - .values.flatten().filter { it != data } - }, - snapThresholdValue = snapThresholdValue, - drawLine = drawLine, - onLineCancel = onLineCancel, - isPressed = isPressed, - onTapInEditMode = { - onButtonTap(data, layer) + ) } - ) + } } Layout( @@ -514,8 +551,9 @@ private fun ControlWidgetRenderer( renderingLayers.forEach { layer -> val normalButtons by layer.normalButtons.collectAsStateWithLifecycle() val textBoxes by layer.textBoxes.collectAsStateWithLifecycle() + val joysticks by layer.joysticks.collectAsStateWithLifecycle() - val widgetsInLayer = normalButtons + textBoxes + val widgetsInLayer = normalButtons + textBoxes + joysticks allWidgetsMap[layer] = widgetsInLayer textBoxes.forEach { data -> @@ -525,6 +563,10 @@ private fun ControlWidgetRenderer( normalButtons.forEach { data -> RenderWidget(data, layer, data.isPressed) } + + joysticks.forEach { data -> + RenderWidget(data, layer, isPressed = false) + } } } ) { measurables, constraints -> @@ -544,6 +586,7 @@ private fun ControlWidgetRenderer( renderingLayers.fastForEach { layer -> layer.textBoxes.value.fastForEach { it.putSize() } layer.normalButtons.value.fastForEach { it.putSize() } + layer.joysticks.value.fastForEach { it.putSize() } } layout(constraints.maxWidth, constraints.maxHeight) { @@ -564,6 +607,7 @@ private fun ControlWidgetRenderer( renderingLayers.fastForEach { layer -> layer.textBoxes.value.fastForEach { it.place() } layer.normalButtons.value.fastForEach { it.place() } + layer.joysticks.value.fastForEach { it.place() } } } } diff --git a/LayerController/src/main/java/com/movtery/layer_controller/Layout.kt b/LayerController/src/main/java/com/movtery/layer_controller/Layout.kt index 80d1912fa..178c9e1a1 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/Layout.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/Layout.kt @@ -26,9 +26,11 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.LinearProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.remember @@ -45,11 +47,11 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.movtery.layer_controller.data.HideLayerWhen import com.movtery.layer_controller.data.VisibilityType import com.movtery.layer_controller.event.EventHandler +import com.movtery.layer_controller.layout.StyleableJoystick import com.movtery.layer_controller.layout.TextButton import com.movtery.layer_controller.observable.ObservableButtonStyle import com.movtery.layer_controller.observable.ObservableControlLayer @@ -57,7 +59,9 @@ import com.movtery.layer_controller.observable.ObservableControlLayout import com.movtery.layer_controller.observable.ObservableWidget import com.movtery.layer_controller.observable.TouchProcessor import com.movtery.layer_controller.observable.TouchSession +import com.movtery.layer_controller.utils.buttonSize import com.movtery.layer_controller.utils.getWidgetPosition +import androidx.compose.runtime.collectAsState /** * 控制布局画布 @@ -67,15 +71,15 @@ import com.movtery.layer_controller.utils.getWidgetPosition * @param opacity 控制布局画布整体不透明度 0f~1f * @param markPointerAsMoveOnly 标记指针为仅接受滑动处理 * @param hideLayerWhen 根据情况决定是否隐藏指定控件层 - * @param isUsingJoystick 是否正在使用摇杆组件 */ @Composable fun ControlBoxLayout( modifier: Modifier = Modifier, observedLayout: ObservableControlLayout? = null, eventHandler: EventHandler = EventHandler(), - isUsingJoystick: Boolean, isCursorGrabbing: Boolean, + enableJoystick: Boolean = true, + isEditMode: Boolean = false, checkOccupiedPointers: (PointerId) -> Boolean, @FloatRange(0.0, 1.0) opacity: Float = 1f, markPointerAsMoveOnly: (PointerId) -> Unit = {}, @@ -95,6 +99,7 @@ fun ControlBoxLayout( ) } } + else -> { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { key(observedLayout.hashCode()) { @@ -108,8 +113,9 @@ fun ControlBoxLayout( checkOccupiedPointers = checkOccupiedPointers, opacity = opacity, markPointerAsMoveOnly = markPointerAsMoveOnly, - isUsingJoystick = isUsingJoystick, isCursorGrabbing = isCursorGrabbing, + enableJoystick = enableJoystick, + isEditMode = isEditMode, hideLayerWhen = hideLayerWhen, isDark = isDark, content = content @@ -132,16 +138,17 @@ private fun BoxWithConstraintsScope.BaseControlBoxLayout( checkOccupiedPointers: (PointerId) -> Boolean, @FloatRange(0.0, 1.0) opacity: Float, markPointerAsMoveOnly: (PointerId) -> Unit, - isUsingJoystick: Boolean, isCursorGrabbing: Boolean, + enableJoystick: Boolean, + isEditMode: Boolean, hideLayerWhen: HideLayerWhen, isDark: Boolean, content: @Composable BoxScope.() -> Unit ) { -// val isDarkMode by rememberUpdatedState(isSystemInDarkTheme()) - val layers by observedLayout.layers.collectAsStateWithLifecycle() - val reversedLayers = remember(layers) { layers.reversed() } + /** 渲染层级:底部最先渲染,顶部最后渲染 */ + val renderingOrderLayers = remember(layers) { layers.reversed() } + val styles by observedLayout.styles.collectAsStateWithLifecycle() val currentCheckOccupiedPointers by rememberUpdatedState(checkOccupiedPointers) @@ -168,7 +175,7 @@ private fun BoxWithConstraintsScope.BaseControlBoxLayout( Box( modifier = modifier - .pointerInput(reversedLayers, hideLayerWhen) { + .pointerInput(layers, hideLayerWhen, enableJoystick, isEditMode) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent(pass = PointerEventPass.Initial) @@ -179,7 +186,7 @@ private fun BoxWithConstraintsScope.BaseControlBoxLayout( if (!change.pressed) { touchSession.endPointer(pointerId).forEach { widget -> //释放该指针事件 - widget.onReleaseEvent(eventHandler, reversedLayers) + widget.onReleaseEvent(eventHandler, layers) } return@forEach } @@ -192,15 +199,16 @@ private fun BoxWithConstraintsScope.BaseControlBoxLayout( val visibleWidgets = collectVisibleWidgets( layers = layers, hideLayerWhen = currentHideLayerWhen, - isUsingJoystick = isUsingJoystick, isCursorGrabbing = currentIsCursorGrabbing, + enableJoystick = enableJoystick, + isEditMode = isEditMode ) touchProcessor.processFrame( session = touchSession, change = change, visibleWidgets = visibleWidgets, - allLayers = reversedLayers, + allLayers = layers, consumeEvent = { it.consume() }, markPointerAsMoveOnly = markPointerAsMoveOnly, ) @@ -214,12 +222,12 @@ private fun BoxWithConstraintsScope.BaseControlBoxLayout( ControlsRendererLayer( isDark = isDark, opacity = opacity, - layers = reversedLayers, + layers = renderingOrderLayers, styles = styles, screenSize = screenSize, eventHandler = eventHandler, - isUsingJoystick = isUsingJoystick, isCursorGrabbing = currentIsCursorGrabbing, + enableJoystick = enableJoystick, hideLayerWhen = currentHideLayerWhen ) } @@ -233,8 +241,8 @@ private fun ControlsRendererLayer( styles: List, screenSize: IntSize, eventHandler: EventHandler, - isUsingJoystick: Boolean, isCursorGrabbing: Boolean, + enableJoystick: Boolean, hideLayerWhen: HideLayerWhen ) { Layout( @@ -245,42 +253,82 @@ private fun ControlsRendererLayer( val layerVisibility = checkLayerVisibility( layer = layer, hideLayerWhen = hideLayerWhen, - isUsingJoystick = isUsingJoystick, isCursorGrabbing = isCursorGrabbing, visibilityType = layer.visibilityType ) - val normalButtons by layer.normalButtons.collectAsStateWithLifecycle() - val textBoxes by layer.textBoxes.collectAsStateWithLifecycle() - textBoxes.forEach { data -> + // 注意:这里必须和 layout 块中的顺序完全一致 + layer.textBoxes.collectAsState().value.forEach { data -> + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + data.onCheckVisibilityType() + ) TextButton( isEditMode = false, data = data, allStyles = styles, screenSize = screenSize, isDark = isDark, - visible = layerVisibility && checkVisibility(isCursorGrabbing, data.visibilityType), - getOtherWidgets = { emptyList() }, //不需要计算吸附 + visible = isVisible, + getOtherWidgets = { emptyList() }, snapThresholdValue = 4.dp, eventHandler = eventHandler, - isPressed = false //文本框不需要按压状态 + isPressed = false ) } - normalButtons.forEach { data -> + layer.normalButtons.collectAsState().value.forEach { data -> + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + data.onCheckVisibilityType() + ) + val isPressed = data.isPressed TextButton( isEditMode = false, data = data, allStyles = styles, screenSize = screenSize, isDark = isDark, - visible = layerVisibility && checkVisibility(isCursorGrabbing, data.visibilityType), - getOtherWidgets = { emptyList() }, //不需要计算吸附 + visible = isVisible, + getOtherWidgets = { emptyList() }, snapThresholdValue = 4.dp, eventHandler = eventHandler, - isPressed = data.isPressed + isPressed = isPressed ) } + + if (enableJoystick) { + layer.joysticks.collectAsState().value.forEach { data -> + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + data.onCheckVisibilityType() + ) + + key(data.uuid) { + LaunchedEffect(isVisible) { + if (!isVisible) data.resetState() + } + + if (isVisible) { + StyleableJoystick( + modifier = Modifier.buttonSize(data, screenSize), + isDarkTheme = isDark, + style = data.style, + deadZoneRatio = data.deadZoneRatio, + lockThreshold = data.lockThreshold, + canLock = data.canLock, + enablePointerInput = false, + externalOffset = data.joystickOffset, + externalIsLocked = data.isLockedState, + externalCanLockInternal = data.canLockState + ) + } else { + // 占位,否则索引会乱 + Box(Modifier.size(0.dp)) + } + } + } + } } } ) { measurables, constraints -> @@ -288,38 +336,71 @@ private fun ControlsRendererLayer( measurable.measure(constraints) } - var index = 0 - fun ObservableWidget.putSize() { - if (index < placeables.size) { - val placeable = placeables[index] - this.internalRenderSize = IntSize(placeable.width, placeable.height) - index++ - } - } - - layers.fastForEach { layer -> - layer.textBoxes.value.fastForEach { it.putSize() } - layer.normalButtons.value.fastForEach { it.putSize() } - } + var placeableIndex = 0 layout(constraints.maxWidth, constraints.maxHeight) { - var placeableIndex = 0 - fun ObservableWidget.place() { - if (placeableIndex < placeables.size) { - val placeable = placeables[placeableIndex] - val position = getWidgetPosition( - data = this, - widgetSize = IntSize(placeable.width, placeable.height), - screenSize = screenSize - ) - placeable.place(position.x.toInt(), position.y.toInt()) - placeableIndex++ + layers.forEach { layer -> + val layerVisibility = checkLayerVisibility( + layer = layer, + hideLayerWhen = hideLayerWhen, + isCursorGrabbing = isCursorGrabbing, + visibilityType = layer.visibilityType + ) + + // 摆放逻辑必须遍历所有列表(即使不可见也要消耗掉 placeableIndex) + layer.textBoxes.value.forEach { widget -> + if (placeableIndex < placeables.size) { + val placeable = placeables[placeableIndex++] + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + widget.onCheckVisibilityType() + ) + if (isVisible) { + val size = IntSize(placeable.width, placeable.height) + val position = getWidgetPosition(widget, size, screenSize) + // 更新位置供触控检测 + widget.internalRenderSize = size + widget.internalRenderOffsetPx = position + placeable.place(position.x.toInt(), position.y.toInt()) + } + } } - } - layers.fastForEach { layer -> - layer.textBoxes.value.fastForEach { it.place() } - layer.normalButtons.value.fastForEach { it.place() } + layer.normalButtons.value.forEach { widget -> + if (placeableIndex < placeables.size) { + val placeable = placeables[placeableIndex++] + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + widget.onCheckVisibilityType() + ) + if (isVisible) { + val size = IntSize(placeable.width, placeable.height) + val position = getWidgetPosition(widget, size, screenSize) + widget.internalRenderSize = size + widget.internalRenderOffsetPx = position + placeable.place(position.x.toInt(), position.y.toInt()) + } + } + } + + if (enableJoystick) { + layer.joysticks.value.forEach { widget -> + if (placeableIndex < placeables.size) { + val placeable = placeables[placeableIndex++] + val isVisible = layerVisibility && checkVisibility( + isCursorGrabbing, + widget.onCheckVisibilityType() + ) + if (isVisible) { + val size = IntSize(placeable.width, placeable.height) + val position = getWidgetPosition(widget, size, screenSize) + widget.internalRenderSize = size + widget.internalRenderOffsetPx = position + placeable.place(position.x.toInt(), position.y.toInt()) + } + } + } + } } } } @@ -332,35 +413,43 @@ private fun ControlsRendererLayer( private fun collectVisibleWidgets( layers: List, hideLayerWhen: HideLayerWhen, - isUsingJoystick: Boolean, isCursorGrabbing: Boolean, + enableJoystick: Boolean, + isEditMode: Boolean ): List { - return layers + val list = mutableListOf() + + // 从顶层到底层收集 + layers .filter { layer -> checkLayerVisibility( layer = layer, hideLayerWhen = hideLayerWhen, - isUsingJoystick = isUsingJoystick, isCursorGrabbing = isCursorGrabbing, visibilityType = layer.visibilityType, ) } - .flatMap { layer -> - //反转,从顶到底 - layer.normalButtons.value.reversed() - } - .filter { widget -> - widget.canTouch() && checkVisibility( - isCursorGrabbing = isCursorGrabbing, - visibilityType = widget.onCheckVisibilityType() - ) + .forEach { layer -> + // 在同一层内,优先级:摇杆 > 普通按钮 > 文本框 + + if (enableJoystick && !isEditMode) { + list.addAll(layer.joysticks.value.reversed()) + } + + list.addAll(layer.normalButtons.value.reversed()) } + + return list.filter { widget -> + widget.canTouch() && checkVisibility( + isCursorGrabbing = isCursorGrabbing, + visibilityType = widget.onCheckVisibilityType() + ) + } } private fun checkLayerVisibility( layer: ObservableControlLayer, hideLayerWhen: HideLayerWhen, - isUsingJoystick: Boolean, isCursorGrabbing: Boolean, visibilityType: VisibilityType ): Boolean { @@ -374,7 +463,7 @@ private fun checkLayerVisibility( HideLayerWhen.None -> false } - return !(hideConditionMet || (isUsingJoystick && layer.hideWhenJoystick)) + return !hideConditionMet } /** diff --git a/LayerController/src/main/java/com/movtery/layer_controller/data/JoystickData.kt b/LayerController/src/main/java/com/movtery/layer_controller/data/JoystickData.kt new file mode 100644 index 000000000..9befd43f3 --- /dev/null +++ b/LayerController/src/main/java/com/movtery/layer_controller/data/JoystickData.kt @@ -0,0 +1,79 @@ +/* + * Zalith Launcher 2 + * Copyright (C) 2025 MovTery and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.movtery.layer_controller.data + +import com.movtery.layer_controller.observable.Modifiable +import com.movtery.layer_controller.utils.randomUUID +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * 摇杆数据 + * @param uuid 唯一标识符 + * @param position 位置 + * @param size 大小 + * @param style 样式 + * @param deadZoneRatio 死区范围,作为半分比,根据组件整体大小计算 + * @param lockThreshold 前进锁判定范围(在组件的外部,正上方),作为百分比,根据组件整体大小计算 + * @param canLock 是否可以进行前进锁 + */ +@Serializable +data class JoystickData( + @SerialName("uuid") + val uuid: String, + @SerialName("position") + val position: ButtonPosition, + @SerialName("size") + val size: ButtonSize, + @SerialName("style") + val style: JoystickStyle, + @SerialName("deadZoneRatio") + val deadZoneRatio: Float = 0.3f, + @SerialName("lockThreshold") + val lockThreshold: Float = 0.3f, + @SerialName("canLock") + val canLock: Boolean = true, + @SerialName("lockSpring") + val lockSpring: Boolean = true +) : Widget, Modifiable { + override fun isModified(other: JoystickData): Boolean { + return this.uuid != other.uuid || + this.position.isModified(other.position) || + this.size.isModified(other.size) || + this.style.isModified(other.style) || + this.deadZoneRatio != other.deadZoneRatio || + this.lockThreshold != other.lockThreshold || + this.canLock != other.canLock || + this.lockSpring != other.lockSpring + } +} + +/** + * 默认摇杆数据 + */ +val DefaultJoystickData = JoystickData( + uuid = randomUUID(), + position = BottomStartPosition.copy(x = 1500, y = 8500), + size = DefaultSize.copy(widthPercentage = 2500, heightPercentage = 2500), + style = DefaultJoystickStyle, + deadZoneRatio = 0.3f, + lockThreshold = 0.3f, + canLock = true, + lockSpring = true +) diff --git a/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayer.kt b/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayer.kt index 43d673fbb..be444f3c9 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayer.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayer.kt @@ -18,6 +18,7 @@ package com.movtery.layer_controller.layout +import com.movtery.layer_controller.data.JoystickData import com.movtery.layer_controller.data.NormalData import com.movtery.layer_controller.data.TextData import com.movtery.layer_controller.data.VisibilityType @@ -33,7 +34,6 @@ import kotlinx.serialization.Serializable * @param hide 是否隐藏层级 * @param hideWhenMouse 是否在实体鼠标操控后隐藏 * @param hideWhenGamepad 是否在手柄操控后隐藏 - * @param hideWhenJoystick 是否在使用摇杆时隐藏 * @param visibilityType 层级的可见场景 * @param normalButtons 普通的按钮列表 * @param textBoxes 文本显示框列表 @@ -50,14 +50,14 @@ data class ControlLayer( val hideWhenMouse: Boolean = true, @SerialName("hideWhenGamepad") val hideWhenGamepad: Boolean = true, - @SerialName("hideWhenJoystick") - val hideWhenJoystick: Boolean = false, @SerialName("visibilityType") val visibilityType: VisibilityType, @SerialName("normalButtons") val normalButtons: List = emptyList(), @SerialName("textBoxes") - val textBoxes: List = emptyList() + val textBoxes: List = emptyList(), + @SerialName("joysticks") + val joysticks: List = emptyList() ): Modifiable { override fun isModified(other: ControlLayer): Boolean { return this.name != other.name || @@ -65,10 +65,10 @@ data class ControlLayer( this.hide != other.hide || this.hideWhenMouse != other.hideWhenMouse || this.hideWhenGamepad != other.hideWhenGamepad || - this.hideWhenJoystick != other.hideWhenJoystick || this.visibilityType != other.visibilityType || this.normalButtons.isModified(other.normalButtons) || - this.textBoxes.isModified(other.textBoxes) + this.textBoxes.isModified(other.textBoxes) || + this.joysticks.isModified(other.joysticks) } } diff --git a/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayout.kt b/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayout.kt index c086268ab..4d4030221 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayout.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/layout/ControlLayout.kt @@ -20,7 +20,6 @@ package com.movtery.layer_controller.layout import com.movtery.layer_controller.EDITOR_VERSION import com.movtery.layer_controller.data.ButtonStyle -import com.movtery.layer_controller.data.JoystickStyle import com.movtery.layer_controller.data.lang.EmptyTranslatableString import com.movtery.layer_controller.data.lang.TranslatableString import com.movtery.layer_controller.layout.ControlLayout.Info @@ -55,7 +54,7 @@ data class ControlLayout( val special: Special = Special(), @SerialName("editorVersion") val editorVersion: Int -): Modifiable { +) : Modifiable { @Serializable data class Info( @SerialName("name") @@ -68,7 +67,7 @@ data class ControlLayout( val versionCode: Int, @SerialName("versionName") val versionName: String - ): Modifiable { + ) : Modifiable { override fun isModified(other: Info): Boolean { return other.name.isModified(this.name) || other.author.isModified(this.author) || @@ -80,23 +79,14 @@ data class ControlLayout( /** * 特殊设定,比如由控制布局改变启动器的部分组件属性之类的 - * @param joystickStyle 控制启动器层的摇杆的样式 */ @Serializable data class Special( - val joystickStyle: JoystickStyle? = null + val _dummy: Int = 0 //将来可扩展更多设定 - ): Modifiable { + ) : Modifiable { override fun isModified(other: Special): Boolean { - val joystickStyle0 = this.joystickStyle - val joystickModified = if (joystickStyle0 == null) { - other.joystickStyle != null - } else { - if (other.joystickStyle == null) true - else joystickStyle0.isModified(other.joystickStyle) - } - - return joystickModified + return false } } diff --git a/LayerController/src/main/java/com/movtery/layer_controller/layout/Joystick.kt b/LayerController/src/main/java/com/movtery/layer_controller/layout/Joystick.kt new file mode 100644 index 000000000..58170a661 --- /dev/null +++ b/LayerController/src/main/java/com/movtery/layer_controller/layout/Joystick.kt @@ -0,0 +1,577 @@ +/* + * Zalith Launcher 2 + * Copyright (C) 2025 MovTery and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.movtery.layer_controller.layout + +import android.graphics.Rect +import android.graphics.Region +import androidx.annotation.FloatRange +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToDown +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChanged +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.movtery.layer_controller.observable.ObservableJoystickData +import com.movtery.layer_controller.observable.ObservableJoystickStyle +import com.movtery.layer_controller.observable.ObservableWidget +import com.movtery.layer_controller.utils.buttonSize +import com.movtery.layer_controller.utils.editMode +import com.movtery.layer_controller.utils.snap.GuideLine +import com.movtery.layer_controller.utils.snap.SnapMode +import kotlinx.coroutines.coroutineScope +import kotlin.math.atan2 +import kotlin.math.sqrt + +/** + * 摇杆、方向键通用方向 + */ +enum class JoystickDirection { + East, + NorthEast, + North, + NorthWest, + West, + SouthWest, + South, + SouthEast, + /** + * 无方向 + */ + None +} + +/** + * 根据可观察的摇杆样式对象一键设置样式的移动摇杆控件 + * @param isDarkTheme 是否处于暗色模式中,用于选择样式 + * @param style 可观察的摇杆样式 + * @param deadZoneRatio 死区范围,作为半分比,根据组件整体大小计算 + * @param lockThreshold 前进锁判定范围(在组件的外部,正上方),作为百分比,根据组件整体大小计算 + * @param onDirectionChanged 当摇杆的方向变更时,使用这个函数回调 + * @param canLock 是否可以进行前进锁 + * @param onCanLock 当遥感可以进行前进锁定时,或者不能进行前进锁定时,使用这个函数回调 + * @param onLock 当摇杆触发前进锁,或者离开锁定状态时,使用这个函数回调 + */ +@Composable +fun StyleableJoystick( + modifier: Modifier = Modifier, + isDarkTheme: Boolean, + style: ObservableJoystickStyle, + @FloatRange(from = 0.0, to = 1.0) + deadZoneRatio: Float = 0.5f, + @FloatRange(from = 0.0, to = 1.0) + lockThreshold: Float = 0.3f, + onDirectionChanged: (JoystickDirection) -> Unit = {}, + canLock: Boolean = true, + onCanLock: (Boolean) -> Unit = {}, + onLock: (Boolean) -> Unit = {}, + enablePointerInput: Boolean = true, + // 如果提供了外部状态,则使用外部状态,否则内部管理状态 + externalOffset: Offset? = null, + externalIsLocked: Boolean? = null, + externalCanLockInternal: Boolean? = null +) { + val theme = if (isDarkTheme) style.darkStyle else style.lightStyle + + val backgroundShape = remember(theme.backgroundShape) { + RoundedCornerShape(percent = theme.backgroundShape) + } + + val joystickShape = remember(theme.joystickShape) { + RoundedCornerShape(percent = theme.joystickShape) + } + + val borderWidthRatio = remember(theme.borderWidthRatio) { + (theme.borderWidthRatio.toFloat() / 100f).coerceIn(0.0f, 0.5f) + } + + Joystick( + modifier = modifier, + alpha = theme.alpha, + backgroundColor = theme.backgroundColor, + joystickColor = theme.joystickColor, + joystickCanLockColor = theme.joystickCanLockColor, + joystickLockedColor = theme.joystickLockedColor, + lockMarkColor = theme.lockMarkColor, + borderColor = theme.borderColor, + borderWidthRatio = borderWidthRatio, + backgroundShape = backgroundShape, + joystickShape = joystickShape, + joystickSize = theme.joystickSize, + deadZoneRatio = deadZoneRatio, + lockThreshold = lockThreshold, + onDirectionChanged = onDirectionChanged, + canLock = canLock, + onCanLock = onCanLock, + onLock = onLock, + enablePointerInput = enablePointerInput, + externalOffset = externalOffset, + externalIsLocked = externalIsLocked, + externalCanLockInternal = externalCanLockInternal + ) +} + +/** + * 移动摇杆控件 + */ +@Composable +fun Joystick( + modifier: Modifier = Modifier, + @FloatRange(from = 0.0, to = 1.0) + alpha: Float = 1.0f, + backgroundColor: Color = Color.Black.copy(alpha = 0.5f), + joystickColor: Color = Color.White.copy(alpha = 0.5f), + joystickCanLockColor: Color = Color.Yellow.copy(alpha = 0.5f), + joystickLockedColor: Color = Color.Green.copy(alpha = 0.5f), + lockMarkColor: Color = Color.White, + borderColor: Color = Color.White, + @FloatRange(from = 0.0, to = 0.5) + borderWidthRatio: Float = 0f, + backgroundShape: Shape = CircleShape, + joystickShape: Shape = CircleShape, + @FloatRange(from = 0.0, to = 1.0) + joystickSize: Float = 0.5f, + @FloatRange(from = 0.0, to = 1.0) + deadZoneRatio: Float = 0.5f, + @FloatRange(from = 0.0, to = 1.0) + lockThreshold: Float = 0.3f, + onDirectionChanged: (JoystickDirection) -> Unit = {}, + canLock: Boolean = true, + onCanLock: (Boolean) -> Unit = {}, + onLock: (Boolean) -> Unit = {}, + enablePointerInput: Boolean = true, + externalOffset: Offset? = null, + externalIsLocked: Boolean? = null, + externalCanLockInternal: Boolean? = null +) { + //使用这个标记来判断是否渲染摇杆组件,未完全初始化时,可能导致组件闪烁 + var initialized by remember { mutableStateOf(false) } + + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + + val currentBackgroundColor = + remember(backgroundColor, alpha) { backgroundColor.applyAlpha(alpha) } + val currentJoystickColor = remember(joystickColor, alpha) { joystickColor.applyAlpha(alpha) } + val currentJoystickCanLockColor = + remember(joystickCanLockColor, alpha) { joystickCanLockColor.applyAlpha(alpha) } + val currentJoystickLockedColor = + remember(joystickLockedColor, alpha) { joystickLockedColor.applyAlpha(alpha) } + val currentLockMarkColor = remember(lockMarkColor, alpha) { lockMarkColor.applyAlpha(alpha) } + val currentBorderColor = remember(borderColor, alpha) { borderColor.applyAlpha(alpha) } + val currentCanLock by rememberUpdatedState(canLock) + + var backgroundSizePx by remember { mutableFloatStateOf(0f) } + + val backgroundRegion = remember(backgroundShape, backgroundSizePx) { + backgroundShape.toRegion( + size = Size(backgroundSizePx, backgroundSizePx), + density = density, + layoutDirection = layoutDirection + ) + } + val currentBackgroundRegion by rememberUpdatedState(backgroundRegion) + + val joystickSizePx = remember(backgroundSizePx, joystickSize) { + backgroundSizePx * joystickSize.coerceIn(0.0f, 1.0f) + } + val currentJoystickSizePx by rememberUpdatedState(joystickSizePx) + + val centerPoint = + remember(backgroundSizePx) { Offset(backgroundSizePx / 2, backgroundSizePx / 2) } + val currentCenterPoint by rememberUpdatedState(centerPoint) + + val deadZoneRadius = + remember(backgroundSizePx, deadZoneRatio) { backgroundSizePx * deadZoneRatio / 2 } + val currentDeadZoneRadius by rememberUpdatedState(deadZoneRadius) + + val lockThresholdPx = + remember(backgroundSizePx, lockThreshold) { backgroundSizePx * lockThreshold } + val currentLockThresholdPx by rememberUpdatedState(lockThresholdPx) + + val lockPosition = remember(centerPoint) { Offset(centerPoint.x, 0f) } + val currentLockPosition by rememberUpdatedState(lockPosition) + + // 内部状态 + var internalCanLock by remember { mutableStateOf(false) } + var internalDirection by remember { mutableStateOf(JoystickDirection.None) } + var internalJoystickPosition by remember { mutableStateOf(currentCenterPoint) } + var internalIsLocked by remember { mutableStateOf(false) } + var lastDragPosition by remember { mutableStateOf(Offset.Zero) } + + // 使用外部偏移量时,需要转换(外部是相对于中心的偏移,内部是绝对坐标) + val joystickPosition = + externalOffset?.let { currentCenterPoint + it } ?: internalJoystickPosition + val isLocked = externalIsLocked ?: internalIsLocked + val canLockInternal = externalCanLockInternal ?: internalCanLock + + fun updateJoystickState(position: Offset = currentCenterPoint) { + val clampedPosition = position.clampToRegion( + region = currentBackgroundRegion, + center = currentCenterPoint + ) + + internalJoystickPosition = clampedPosition + + val direction = calculateDirection( + joystickPosition = clampedPosition, + backgroundCenter = currentCenterPoint, + deadZoneRadius = currentDeadZoneRadius + ) + internalDirection = direction + + internalCanLock = + currentCanLock && + direction == JoystickDirection.North && + lastDragPosition.y < -currentLockThresholdPx + } + + LaunchedEffect(backgroundSizePx) { + initialized = false + updateJoystickState() + initialized = true + } + + LaunchedEffect(backgroundShape) { + updateJoystickState(internalJoystickPosition) + } + + if (externalOffset == null) { + LaunchedEffect(internalDirection) { onDirectionChanged(internalDirection) } + LaunchedEffect(internalCanLock) { onCanLock(internalCanLock) } + LaunchedEffect(internalIsLocked) { onLock(internalIsLocked) } + } + + DisposableEffect(Unit) { + onDispose { + if (externalOffset == null) onDirectionChanged(JoystickDirection.None) + initialized = false + } + } + + Box( + modifier = modifier + .onSizeChanged { + backgroundSizePx = it.width.toFloat() + }, + contentAlignment = Alignment.Center + ) { + Canvas( + modifier = Modifier + .fillMaxSize() + .then( + if (enablePointerInput) { + Modifier.pointerInput(Unit) { + simpleDrag( + hitTest = { pos -> + currentBackgroundRegion.contains(pos.x.toInt(), pos.y.toInt()) + }, + onPointerMove = { offset -> + lastDragPosition = offset + if (internalIsLocked) internalIsLocked = false + updateJoystickState(offset) + }, + onPointerRelease = { + if (internalCanLock) { + internalIsLocked = true + updateJoystickState(currentLockPosition) + } else { + internalIsLocked = false + updateJoystickState(currentCenterPoint) + } + } + ) + } + } else Modifier + ) + ) { + if (initialized) { + val minSide = minOf(this@Canvas.size.width, this@Canvas.size.height) + + drawBackgroundLayer( + layoutDirection = layoutDirection, + size = this@Canvas.size, + shape = backgroundShape, + backgroundColor = currentBackgroundColor, + borderColor = currentBorderColor, + borderWidthPx = (minSide * borderWidthRatio).coerceAtLeast(0f) + ) + + drawJoystick( + layoutDirection = layoutDirection, + color = when { + isLocked -> currentJoystickLockedColor + canLockInternal -> currentJoystickCanLockColor + else -> currentJoystickColor + }, + center = joystickPosition, + size = currentJoystickSizePx, + shape = joystickShape + ) + + if (isLocked) { + drawCircle( + color = currentLockMarkColor, + center = currentLockPosition, + radius = 4f + ) + } + } + } + } +} + +/** + * 编辑模式下的摇杆控件 + */ +@Composable +internal fun JoystickButton( + data: ObservableWidget, + screenSize: IntSize, + isDark: Boolean, + enableSnap: Boolean, + snapMode: SnapMode, + localSnapRange: Dp, + getOtherWidgets: () -> List, + snapThresholdValue: Dp, + drawLine: (ObservableWidget, List) -> Unit, + onLineCancel: (ObservableWidget) -> Unit, + onTapInEditMode: () -> Unit +) { + val joystickData = data as? ObservableJoystickData ?: return + + StyleableJoystick( + modifier = Modifier + .buttonSize(data, screenSize) + .editMode( + isEditMode = true, + data = data, + screenSize = screenSize, + enableSnap = enableSnap, + snapMode = snapMode, + localSnapRange = localSnapRange, + getOtherWidgets = getOtherWidgets, + snapThresholdValue = snapThresholdValue, + drawLine = drawLine, + onLineCancel = onLineCancel, + onTapInEditMode = onTapInEditMode + ), + isDarkTheme = isDark, + style = joystickData.style, + deadZoneRatio = joystickData.deadZoneRatio, + lockThreshold = joystickData.lockThreshold, + canLock = joystickData.canLock, + //编辑模式下禁用真实交互 + onDirectionChanged = {}, + onCanLock = {}, + onLock = {}, + enablePointerInput = false + ) +} + +/** + * 以下代码块从 ZalithLauncher 迁移,略作调整以适应库环境 + */ + +private suspend fun PointerInputScope.simpleDrag( + hitTest: (Offset) -> Boolean, + onPointerMove: (position: Offset) -> Unit, + onPointerRelease: () -> Unit +) { + coroutineScope { + var activePointer: PointerId? = null + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + event.changes + .filter { it.changedToDown() } + .forEach { change -> + val pointerId = change.id + if (activePointer == null) { + val pos = change.position + if (hitTest(pos)) { + activePointer = pointerId + onPointerMove(pos) + } + } + } + activePointer?.let { pointerId -> + event.changes + .firstOrNull { it.id == pointerId && it.positionChanged() && !it.isConsumed } + ?.let { moveChange -> + onPointerMove(moveChange.position) + moveChange.consume() + } + } + event.changes + .filter { it.changedToUpIgnoreConsumed() } + .forEach { change -> + val pointerId = change.id + if (pointerId == activePointer) { + onPointerRelease() + activePointer = null + } + } + } + } + } +} + +private fun DrawScope.drawBackgroundLayer( + layoutDirection: LayoutDirection, + size: Size, + shape: Shape, + backgroundColor: Color, + borderColor: Color, + borderWidthPx: Float +) { + val outline = shape.createOutline( + size = size, + layoutDirection = layoutDirection, + density = this + ) + val clipPath = when (outline) { + is Outline.Generic -> outline.path + is Outline.Rounded -> Path().apply { addRoundRect(outline.roundRect) } + is Outline.Rectangle -> Path().apply { addRect(outline.rect) } + } + clipPath(clipPath) { + drawOutline(outline = outline, color = backgroundColor) + if (borderWidthPx > 0f) { + drawOutline( + outline = outline, + color = borderColor, + style = Stroke(width = borderWidthPx) + ) + } + } +} + +private fun DrawScope.drawJoystick( + layoutDirection: LayoutDirection, + color: Color, + center: Offset, + size: Float, + shape: Shape +) { + val halfSize = size / 2 + val topLeftX = center.x - halfSize + val topLeftY = center.y - halfSize + val outline = shape.createOutline( + size = Size(size, size), + layoutDirection = layoutDirection, + density = this + ) + translate(left = topLeftX, top = topLeftY) { + drawOutline(outline = outline, color = color) + } +} + +fun Shape.toRegion(size: Size, density: Density, layoutDirection: LayoutDirection): Region { + val outline: Outline = this.createOutline(size, layoutDirection, density) + val composePath: Path = when (outline) { + is Outline.Rectangle -> Path().apply { addRect(outline.rect) } + is Outline.Rounded -> Path().apply { addRoundRect(outline.roundRect) } + is Outline.Generic -> outline.path + } + val androidPath = composePath.asAndroidPath() + val region = Region() + val rect = Rect(0, 0, size.width.toInt(), size.height.toInt()) + region.setPath(androidPath, Region(rect)) + return region +} + +fun Offset.clampToRegion(region: Region, center: Offset): Offset { + if (region.contains(x.toInt(), y.toInt())) return this + var low = 0f + var high = 1f + var result = center + repeat(10) { + val mid = (low + high) / 2 + val testPoint = center + (this - center) * mid + if (region.contains(testPoint.x.toInt(), testPoint.y.toInt())) { + result = testPoint + low = mid + } else { + high = mid + } + } + return result +} + +private fun calculateDirection( + joystickPosition: Offset, + backgroundCenter: Offset, + deadZoneRadius: Float +): JoystickDirection { + if (joystickPosition == backgroundCenter) return JoystickDirection.None + val vector = joystickPosition - backgroundCenter + val distance = sqrt(vector.x * vector.x + vector.y * vector.y) + if (distance < deadZoneRadius) return JoystickDirection.None + val angle = Math.toDegrees(atan2(vector.y.toDouble(), vector.x.toDouble())).toFloat() + return when { + angle >= -22.5f && angle < 22.5f -> JoystickDirection.East + angle in 22.5f..<67.5f -> JoystickDirection.SouthEast + angle in 67.5f..<112.5f -> JoystickDirection.South + angle in 112.5f..<157.5f -> JoystickDirection.SouthWest + angle >= 157.5f || angle < -157.5f -> JoystickDirection.West + angle >= -157.5f && angle < -112.5f -> JoystickDirection.NorthWest + angle >= -112.5f && angle < -67.5f -> JoystickDirection.North + angle >= -67.5f && angle < -22.5f -> JoystickDirection.NorthEast + else -> JoystickDirection.None + } +} + +private fun Color.applyAlpha(multiplier: Float): Color { + return copy(alpha = this.alpha * multiplier) +} diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayer.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayer.kt index 5db2df94d..aa5afcf3f 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayer.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayer.kt @@ -21,6 +21,7 @@ package com.movtery.layer_controller.observable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.movtery.layer_controller.data.JoystickData import com.movtery.layer_controller.data.NormalData import com.movtery.layer_controller.data.TextData import com.movtery.layer_controller.layout.ControlLayer @@ -42,7 +43,6 @@ class ObservableControlLayer( var hide by mutableStateOf(editorHide) var hideWhenMouse by mutableStateOf(layer.hideWhenMouse) var hideWhenGamepad by mutableStateOf(layer.hideWhenGamepad) - var hideWhenJoystick by mutableStateOf(layer.hideWhenJoystick) var visibilityType by mutableStateOf(layer.visibilityType) private val _normalButtons = MutableStateFlow(layer.normalButtons.map { ObservableNormalData(it) }) @@ -51,6 +51,9 @@ class ObservableControlLayer( private val _textBoxes = MutableStateFlow(layer.textBoxes.map { ObservableTextData(it) }) val textBoxes = _textBoxes.asStateFlow() + private val _joysticks = MutableStateFlow(layer.joysticks.map { ObservableJoystickData(it) }) + val joysticks = _joysticks.asStateFlow() + /** * 添加一个普通的按钮 */ @@ -111,6 +114,41 @@ class ObservableControlLayer( } } + var onJoysticksChanged: () -> Unit = {} + + /** + * 添加摇杆 + */ + fun addJoystick(joystick: JoystickData) { + addJoystick(ObservableJoystickData(joystick)) + } + + /** + * 添加摇杆 + */ + fun addJoystick(joystick: ObservableJoystickData) { + _joysticks.update { it + joystick } + onJoysticksChanged() + } + + /** + * 批量添加摇杆 + */ + fun addAllJoystick(joysticks: List) { + _joysticks.update { it + joysticks } + onJoysticksChanged() + } + + /** + * 移除摇杆 + */ + fun removeJoystick(uuid: String) { + _joysticks.update { oldList -> + oldList.filterNot { it.uuid == uuid } + } + onJoysticksChanged() + } + override fun pack(): ControlLayer { return ControlLayer( name = name, @@ -118,10 +156,10 @@ class ObservableControlLayer( hide = editorHide, hideWhenMouse = hideWhenMouse, hideWhenGamepad = hideWhenGamepad, - hideWhenJoystick = hideWhenJoystick, visibilityType = visibilityType, normalButtons = _normalButtons.value.map { it.packNormal() }, - textBoxes = _textBoxes.value.map { it.packText() } + textBoxes = _textBoxes.value.map { it.packText() }, + joysticks = _joysticks.value.map { it.packJoystick() } ) } diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayout.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayout.kt index f2384b8ef..18d032c7f 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayout.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableControlLayout.kt @@ -18,6 +18,9 @@ package com.movtery.layer_controller.observable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.movtery.layer_controller.EDITOR_VERSION import com.movtery.layer_controller.data.ButtonStyle import com.movtery.layer_controller.layout.ControlLayer @@ -34,9 +37,31 @@ class ObservableControlLayout( ): Packable { val info = ObservableControlInfo(layout.info) - private val _layers = MutableStateFlow(layout.layers.map { ObservableControlLayer(it) }) + private val _layers = MutableStateFlow(layout.layers.map { initLayer(it) }) val layers = _layers.asStateFlow() - + + /** + * 全局唯一的摇杆(如果存在) + */ + var globalJoystick by mutableStateOf(findGlobalJoystick()) + private set + + private fun findGlobalJoystick(): ObservableJoystickData? { + _layers.value.forEach { layer -> + val found = layer.joysticks.value.firstOrNull() + if (found != null) return found + } + return null + } + + private fun initLayer(layer: ControlLayer): ObservableControlLayer { + return ObservableControlLayer(layer).apply { + onJoysticksChanged = { + globalJoystick = findGlobalJoystick() + } + } + } + private val _styles = MutableStateFlow(layout.styles.map { ObservableButtonStyle(it) }) val styles = _styles.asStateFlow() @@ -48,18 +73,32 @@ class ObservableControlLayout( * @return 新添加的可观察控件层 */ fun addLayer(layer: ControlLayer): ObservableControlLayer { - val newLayer = ObservableControlLayer(layer) - // 在顶部添加 - _layers.update { listOf(newLayer) + it } + val newLayer = initLayer(layer) + // 在顶部添加 + _layers.update { oldList -> + val newList = listOf(newLayer) + oldList + globalJoystick = findGlobalJoystickInternal(newList) + newList + } return newLayer } + private fun findGlobalJoystickInternal(currentLayers: List): ObservableJoystickData? { + for (layer in currentLayers) { + val found = layer.joysticks.value.firstOrNull() + if (found != null) return found + } + return null + } + /** * 移除控件层 */ fun removeLayer(uuid: String) { _layers.update { oldLayers -> - oldLayers.filterNot { it.uuid == uuid } + val newList = oldLayers.filterNot { it.uuid == uuid } + globalJoystick = findGlobalJoystickInternal(newList) + newList } } @@ -88,7 +127,12 @@ class ObservableControlLayout( downLayer.addAllTextBox(it) } + layer.joysticks.value.takeIf { it.isNotEmpty() }?.let { + downLayer.addAllJoystick(it) + } + layers.removeAt(index) + globalJoystick = findGlobalJoystickInternal(layers) layers } } @@ -100,6 +144,7 @@ class ObservableControlLayout( _layers.update { oldLayers -> oldLayers.toMutableList().apply { add(toIndex, removeAt(fromIndex)) + globalJoystick = findGlobalJoystickInternal(this) } } } @@ -162,4 +207,4 @@ class ObservableControlLayout( override fun isModified(): Boolean { return this.layout.isModified(pack()) } -} \ No newline at end of file +} diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt new file mode 100644 index 000000000..1a6cee50c --- /dev/null +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt @@ -0,0 +1,249 @@ +/* + * Zalith Launcher 2 + * Copyright (C) 2025 MovTery and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.movtery.layer_controller.observable + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import com.movtery.layer_controller.data.ButtonPosition +import com.movtery.layer_controller.data.ButtonSize +import com.movtery.layer_controller.data.JoystickData +import com.movtery.layer_controller.data.VisibilityType +import com.movtery.layer_controller.event.EventHandler +import com.movtery.layer_controller.layout.JoystickDirection +import kotlin.math.atan2 +import kotlin.math.sqrt + +/** + * 可观察的JoystickData包装类 + */ +class ObservableJoystickData(data: JoystickData) : ObservableWidget() { + val uuid: String = data.uuid + var position by mutableStateOf(data.position) + var size by mutableStateOf(data.size) + val style = ObservableJoystickStyle(data.style) + + var deadZoneRatio by mutableFloatStateOf(data.deadZoneRatio) + var lockThreshold by mutableFloatStateOf(data.lockThreshold) + var canLock by mutableStateOf(data.canLock) + var lockSpring by mutableStateOf(data.lockSpring) + + /** 摇杆当前相对于中心的偏移量 (Px) */ + var joystickOffset by mutableStateOf(Offset.Zero) + + /** 摇杆当前方向 */ + var currentDirection by mutableStateOf(JoystickDirection.None) + + /** 摇杆当前是否可锁定 */ + var canLockState by mutableStateOf(false) + + /** 摇杆当前是否已锁定 */ + var isLockedState by mutableStateOf(false) + + var onDirectionChanged: (JoystickDirection) -> Unit = {} + var onCanLock: (Boolean) -> Unit = {} + var onLock: (Boolean) -> Unit = {} + + override val behavior: InteractionBehavior + get() = InteractionBehavior.Press + + override val internalRenderPosition: ButtonPosition + get() = position + + override fun putRenderPosition(position: ButtonPosition) { + this.position = position + } + + override val styleId: String? + get() = null + + override val widgetSize: ButtonSize + get() = size + + override fun putWidgetSize(size: ButtonSize) { + //确保宽高一致 + this.size = size.copy( + heightDp = size.widthDp, + heightPercentage = size.widthPercentage, + heightReference = size.widthReference + ) + } + + override fun onCompositionStart(eventHandler: EventHandler?) { + } + + override fun onCompositionDispose(eventHandler: EventHandler?) { + currentDirection = JoystickDirection.None + onDirectionChanged(JoystickDirection.None) + } + + override fun onCheckVisibilityType(): VisibilityType { + return VisibilityType.ALWAYS + } + + override fun supportsDeepTouchDetection(): Boolean { + return true + } + + override fun canProcess(): Boolean { + // 返回 true,表示一旦选中摇杆,它将独占该指针事件,不再向下透传给底层按钮 + return true + } + + override fun onTouchEvent( + eventHandler: EventHandler, + allLayers: List, + change: PointerInputChange, + activeWidgets: List, + addThis: () -> Unit, + consumeEvent: (Boolean) -> Unit + ) { + // 使用相对中心点的偏移量进行计算 + val center = Offset( + internalRenderOffsetPx.x + internalRenderSize.width / 2f, + internalRenderOffsetPx.y + internalRenderSize.height / 2f + ) + val pos = change.position + val deadZoneRadius = (internalRenderSize.width * deadZoneRatio) / 2f + val lockThresholdPx = internalRenderSize.width * lockThreshold + + if (change.pressed) { + addThis() + if (isLockedState) { + isLockedState = false + // 如果当前处于锁定状态,按下时应立即通知外部解锁 + onLock(false) + } + + updateState(pos, center, deadZoneRadius, lockThresholdPx) + consumeEvent(true) + } + } + + private fun updateState( + pos: Offset, + center: Offset, + deadZoneRadius: Float, + lockThresholdPx: Float + ) { + val vector = pos - center + val distance = sqrt(vector.x * vector.x + vector.y * vector.y) + val maxDistance = internalRenderSize.width / 2f + + val clampedVector = if (distance > 0f) { + if (distance > maxDistance) { + vector * (maxDistance / distance) + } else { + vector + } + } else { + Offset.Zero + } + + joystickOffset = clampedVector + + val newDirection = calculateDirection(clampedVector, deadZoneRadius) + if (currentDirection != newDirection) { + currentDirection = newDirection + onDirectionChanged(newDirection) + } + + val newCanLock = + canLock && currentDirection == JoystickDirection.North && pos.y < center.y - lockThresholdPx + if (canLockState != newCanLock) { + canLockState = newCanLock + onCanLock(newCanLock) + } + } + + private fun calculateDirection(vector: Offset, deadZoneRadius: Float): JoystickDirection { + val distance = sqrt(vector.x * vector.x + vector.y * vector.y) + if (distance < deadZoneRadius) return JoystickDirection.None + val angle = Math.toDegrees(atan2(vector.y.toDouble(), vector.x.toDouble())).toFloat() + return when { + angle >= -22.5f && angle < 22.5f -> JoystickDirection.East + angle in 22.5f..<67.5f -> JoystickDirection.SouthEast + angle in 67.5f..<112.5f -> JoystickDirection.South + angle in 112.5f..<157.5f -> JoystickDirection.SouthWest + angle >= 157.5f || angle < -157.5f -> JoystickDirection.West + angle >= -157.5f && angle < -112.5f -> JoystickDirection.NorthWest + angle >= -112.5f && angle < -67.5f -> JoystickDirection.North + angle >= -67.5f && angle < -22.5f -> JoystickDirection.NorthEast + else -> JoystickDirection.None + } + } + + override fun isReleaseOnOutOfBounds(): Boolean { + return false + } + + override fun onPointerBackInBounds( + eventHandler: EventHandler, + allLayers: List + ) { + } + + override fun onReleaseEvent( + eventHandler: EventHandler, + allLayers: List + ) { + if (canLockState) { + isLockedState = true + joystickOffset = Offset(0f, -internalRenderSize.height / 2f) + currentDirection = JoystickDirection.North + onDirectionChanged(JoystickDirection.North) + onLock(true) + } else { + isLockedState = false + joystickOffset = Offset.Zero + currentDirection = JoystickDirection.None + onDirectionChanged(JoystickDirection.None) + onLock(false) + } + } + + fun packJoystick(): JoystickData { + return JoystickData( + uuid = uuid, + position = position, + size = size, + style = style.pack(), + deadZoneRatio = deadZoneRatio, + lockThreshold = lockThreshold, + canLock = canLock, + lockSpring = lockSpring + ) + } + + /** + * 重置摇杆状态(偏移量、方向、锁定状态等) + */ + fun resetState() { + joystickOffset = Offset.Zero + currentDirection = JoystickDirection.None + onDirectionChanged(JoystickDirection.None) + canLockState = false + onCanLock(false) + isLockedState = false + onLock(false) + } +} diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableSpecial.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableSpecial.kt index f5f9e232e..0b828141a 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableSpecial.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableSpecial.kt @@ -19,31 +19,13 @@ package com.movtery.layer_controller.observable import com.movtery.layer_controller.layout.ControlLayout -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update class ObservableSpecial( private val special: ControlLayout.Special -): Packable { - private val _joystickStyle = MutableStateFlow( - special.joystickStyle?.let { style -> - ObservableJoystickStyle(style) - } - ) - val joystickStyle = _joystickStyle.asStateFlow() - - /** - * 设置摇杆独立样式 - */ - fun setJoystickStyle(joystickStyle: ObservableJoystickStyle?) { - this._joystickStyle.update { joystickStyle } - } +) : Packable { override fun pack(): ControlLayout.Special { - return ControlLayout.Special( - joystickStyle = _joystickStyle.value?.pack() - ) + return ControlLayout.Special() } override fun isModified(): Boolean { diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableWidget.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableWidget.kt index e0721abf8..2cf159d65 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableWidget.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableWidget.kt @@ -43,6 +43,11 @@ abstract class ObservableWidget { */ var movingOffset by mutableStateOf(Offset.Zero) + /** + * 控件的内部渲染位置(相对于画布的像素偏移量) + */ + internal var internalRenderOffsetPx by mutableStateOf(Offset.Zero) + /** * 控件的内部渲染大小 */ diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchProcessor.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchProcessor.kt index 1c1ae81f2..835f3a506 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchProcessor.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchProcessor.kt @@ -40,14 +40,8 @@ class TouchProcessor( position.y in offset.y..(offset.y + size.height) } - - /** * 处理单帧指针事件 - * @param visibleWidgets 预过滤后的可见控件列表 - * @param allLayers 所有控件层 - * @param consumeEvent 消费事件的回调 - * @param markPointerAsMoveOnly 标记指针为仅移动的回调 */ fun processFrame( session: TouchSession, @@ -75,9 +69,6 @@ class TouchProcessor( ) } - /** - * 从可见控件中找出当前指针命中的目标控件 - */ private fun findTargets( visibleWidgets: List, position: Offset, @@ -87,20 +78,15 @@ class TouchProcessor( } if (hitList.isEmpty()) return emptyList() - //找到第一个支持深度检测的控件 - val firstDeepWidget = hitList - .firstOrNull { it.supportsDeepTouchDetection() } + // 找到第一个支持深度检测的控件(通常是拦截后续分发的终点,如摇杆) + val firstDeepWidget = hitList.firstOrNull { it.supportsDeepTouchDetection() } ?: return hitList val topIndex = hitList.indexOf(firstDeepWidget) - //只保留该控件及其上方的可穿透控件 + // 返回从顶层到该深层控件之间的所有控件 return hitList.subList(0, topIndex + 1) - .filter { !it.canProcess() } } - /** - * 处理活跃控件的越界释放 - */ private fun handleOutOfBounds( session: TouchSession, pointerId: PointerId, @@ -121,7 +107,6 @@ class TouchProcessor( widget.onReleaseEvent(eventHandler, allLayers) removed.add(widget) } else { - //指针重新回到控件边界内 backInBounds.add(widget) } } @@ -134,23 +119,16 @@ class TouchProcessor( widget.onPointerBackInBounds(eventHandler, allLayers) } - val currentWidgets = session.activeWidgets(pointerId) - if ( - currentWidgets.isEmpty() && - //越界前存在可滑动控件 - preSnapshot.any { it.behavior is InteractionBehavior.Swipable } - ) { + if (currentWidgets.isEmpty() && preSnapshot.any { it.behavior is InteractionBehavior.Swipable }) { session.enterSwipeChain(pointerId) } - //指针回到了控件上,退出滑动链 if (currentWidgets.isNotEmpty() && session.isInSwipeChain(pointerId)) { session.exitSwipeChain(pointerId) } } - private fun routeToTargets( session: TouchSession, change: PointerInputChange, @@ -160,18 +138,16 @@ class TouchProcessor( consumeEvent: (PointerInputChange) -> Unit, markPointerAsMoveOnly: (PointerId) -> Unit, ) { - if (targets.isEmpty()) return - val activeWidgets = session.activeWidgets(pointerId) + + // 优先处理已激活的控件(捕获模式),确保在拖动过程中不被其他控件拦截 + // 其次处理新命中的目标 + val combinedTargets = (activeWidgets + targets).distinct() - for (target in targets) { - if (target.canProcess()) return + if (combinedTargets.isEmpty()) return - //只允许可滑动且非可切换的控件通过 - if ( - session.isInSwipeChain(pointerId) && - !target.behavior.canBeSwipedTo - ) { + for (target in combinedTargets) { + if (session.isInSwipeChain(pointerId) && !target.behavior.canBeSwipedTo) { continue } @@ -191,6 +167,8 @@ class TouchProcessor( } }, ) + + if (target.canProcess()) break } } } diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchSession.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchSession.kt index 77381cdc3..3e7958c4f 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchSession.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/TouchSession.kt @@ -72,7 +72,10 @@ class TouchSession { * 将控件加入指定指针的活跃列表。 */ fun addActiveWidget(pointerId: PointerId, widget: ObservableWidget) { - _activeWidgets.getOrPut(pointerId) { mutableListOf() }.add(widget) + val list = _activeWidgets.getOrPut(pointerId) { mutableListOf() } + if (!list.contains(widget)) { + list.add(widget) + } } /** diff --git a/ZalithLauncher/src/main/assets/default_layout.json b/ZalithLauncher/src/main/assets/default_layout.json index 64d2f26be..b5e13f133 100644 --- a/ZalithLauncher/src/main/assets/default_layout.json +++ b/ZalithLauncher/src/main/assets/default_layout.json @@ -539,7 +539,6 @@ "name": "move", "uuid": "befebb63d5bf", "hide": false, - "hideWhenJoystick": true, "visibilityType": "in_game", "normalButtons": [ { diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/gamepad/Joystick.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/gamepad/Joystick.kt index 041aa00a4..9d61458ef 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/gamepad/Joystick.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/gamepad/Joystick.kt @@ -19,7 +19,7 @@ package com.movtery.zalithlauncher.ui.control.gamepad import androidx.compose.ui.geometry.Offset -import com.movtery.zalithlauncher.ui.control.joystick.JoystickDirection +import com.movtery.layer_controller.layout.JoystickDirection import com.movtery.zalithlauncher.viewmodel.GamepadViewModel.Event import kotlin.math.abs import kotlin.math.atan2 diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickControl.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickControl.kt index d3b931975..a59ae6c94 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickControl.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickControl.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -63,9 +62,8 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.movtery.layer_controller.data.DefaultJoystickStyle import com.movtery.layer_controller.data.loadFromFile -import com.movtery.layer_controller.data.saveToFile +import com.movtery.layer_controller.layout.JoystickDirection import com.movtery.layer_controller.observable.ObservableJoystickStyle -import com.movtery.zalithlauncher.setting.enums.isLauncherInDarkTheme import com.movtery.zalithlauncher.utils.logging.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope @@ -98,91 +96,6 @@ suspend fun loadJoystickStyle( return ObservableJoystickStyle(style ?: DefaultJoystickStyle) } -/** - * 保存启动器默认的摇杆样式 - */ -suspend fun saveJoystickStyle( - path: File, - style: ObservableJoystickStyle, - onFailed: (Throwable) -> Unit, - onSuccess: suspend () -> Unit = {} -) { - val styleFile = File(path, JSON_FILE_NAME) - runCatching { - withContext(Dispatchers.IO) { - saveToFile(style.pack(), styleFile) - } - }.onFailure { - onFailed(it) - }.onSuccess { - onSuccess() - } -} - -/** - * 根据可观察的摇杆样式对象一键设置样式的移动摇杆控件 - * @param isDarkTheme 是否处于暗色模式中,用于选择样式 - * @param style 可观察的摇杆样式 - * @param size 组件整体大小 - * @param deadZoneRatio 死区范围,作为半分比,根据组件整体大小计算 - * @param lockThreshold 前进锁判定范围(在组件的外部,正上方),作为百分比,根据组件整体大小计算 - * @param onDirectionChanged 当摇杆的方向变更时,使用这个函数回调 - * @param canLock 是否可以进行前进锁 - * @param onCanLock 当遥感可以进行前进锁定时,或者不能进行前进锁定时,使用这个函数回调 - * @param onLock 当摇杆触发前进锁,或者离开锁定状态时,使用这个函数回调 - */ -@Composable -fun StyleableJoystick( - modifier: Modifier = Modifier, - isDarkTheme: Boolean = isLauncherInDarkTheme(), - style: ObservableJoystickStyle, - size: Dp = 120.dp, - @FloatRange(from = 0.0, to = 1.0) - deadZoneRatio: Float = 0.5f, - @FloatRange(from = 0.0, to = 1.0) - lockThreshold: Float = 0.3f, - onDirectionChanged: (JoystickDirection) -> Unit = {}, - canLock: Boolean = true, - onCanLock: (Boolean) -> Unit = {}, - onLock: (Boolean) -> Unit = {} -) { - val theme = if (isDarkTheme) style.darkStyle else style.lightStyle - - val backgroundShape = remember(theme.backgroundShape) { - RoundedCornerShape(percent = theme.backgroundShape) - } - - val joystickShape = remember(theme.joystickShape) { - RoundedCornerShape(percent = theme.joystickShape) - } - - val borderWidthRatio = remember(theme.borderWidthRatio) { - (theme.borderWidthRatio.toFloat() / 100f).coerceIn(0.0f, 0.5f) - } - - Joystick( - modifier = modifier, - alpha = theme.alpha, - backgroundColor = theme.backgroundColor, - joystickColor = theme.joystickColor, - joystickCanLockColor = theme.joystickCanLockColor, - joystickLockedColor = theme.joystickLockedColor, - lockMarkColor = theme.lockMarkColor, - borderColor = theme.borderColor, - borderWidthRatio = borderWidthRatio, - backgroundShape = backgroundShape, - joystickShape = joystickShape, - size = size, - joystickSize = theme.joystickSize, - deadZoneRatio = deadZoneRatio, - lockThreshold = lockThreshold, - onDirectionChanged = onDirectionChanged, - canLock = canLock, - onCanLock = onCanLock, - onLock = onLock - ) -} - /** * 移动摇杆控件 * @param alpha 整体不透明度 diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickDirection.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickDirection.kt deleted file mode 100644 index 681d7ee1f..000000000 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/JoystickDirection.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Zalith Launcher 2 - * Copyright (C) 2025 MovTery and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.movtery.zalithlauncher.ui.control.joystick - -/** - * 摇杆、方向键通用方向 - */ -enum class JoystickDirection { - East, - NorthEast, - North, - NorthWest, - West, - SouthWest, - South, - SouthEast, - /** - * 无方向 - */ - None -} \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/_Movement.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/_Movement.kt index af6580122..83cd2efe3 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/_Movement.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/control/joystick/_Movement.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import com.movtery.layer_controller.event.ClickEvent +import com.movtery.layer_controller.layout.JoystickDirection import com.movtery.zalithlauncher.game.keycodes.MOVEMENT_BACK import com.movtery.zalithlauncher.game.keycodes.MOVEMENT_BACK_VALUE import com.movtery.zalithlauncher.game.keycodes.MOVEMENT_FORWARD diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/GameScreen.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/GameScreen.kt index c4aacccf8..f69207589 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/GameScreen.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/GameScreen.kt @@ -18,7 +18,6 @@ package com.movtery.zalithlauncher.ui.screens.game -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -51,15 +49,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -74,9 +68,6 @@ import com.movtery.layer_controller.layout.EmptyControlLayout import com.movtery.layer_controller.layout.loadLayoutFromFile import com.movtery.layer_controller.observable.DefaultObservableJoystickStyle import com.movtery.layer_controller.observable.ObservableControlLayout -import com.movtery.layer_controller.observable.ObservableJoystickStyle -import com.movtery.layer_controller.observable.ObservableSpecial -import com.movtery.layer_controller.utils.widgetPosition import com.movtery.zalithlauncher.R import com.movtery.zalithlauncher.bridge.CURSOR_DISABLED import com.movtery.zalithlauncher.bridge.ZLBridgeStates @@ -113,14 +104,11 @@ import com.movtery.zalithlauncher.ui.control.gyroscope.isGyroscopeAvailable import com.movtery.zalithlauncher.ui.control.hotbarPercentage import com.movtery.zalithlauncher.ui.control.input.TextInputMode import com.movtery.zalithlauncher.ui.control.joystick.JoystickDirectionListener -import com.movtery.zalithlauncher.ui.control.joystick.StyleableJoystick import com.movtery.zalithlauncher.ui.control.joystick.loadJoystickStyle -import com.movtery.zalithlauncher.ui.control.joystick.saveJoystickStyle import com.movtery.zalithlauncher.ui.control.mouse.SwitchableMouseLayout import com.movtery.zalithlauncher.ui.screens.game.elements.DraggableGameBall import com.movtery.zalithlauncher.ui.screens.game.elements.ForceCloseOperation import com.movtery.zalithlauncher.ui.screens.game.elements.GameMenuSubscreen -import com.movtery.zalithlauncher.ui.screens.game.elements.JoystickManageOperation import com.movtery.zalithlauncher.ui.screens.game.elements.LogBox import com.movtery.zalithlauncher.ui.screens.game.elements.LogState import com.movtery.zalithlauncher.ui.screens.game.elements.ReplacementControlOperation @@ -131,7 +119,6 @@ import com.movtery.zalithlauncher.ui.screens.game.multiplayer.TerracottaOperatio import com.movtery.zalithlauncher.ui.screens.game.multiplayer.rememberTerracottaViewModel import com.movtery.zalithlauncher.ui.screens.main.control_editor.ControlEditor import com.movtery.zalithlauncher.utils.logging.Logger -import com.movtery.zalithlauncher.utils.string.getMessageOrToString import com.movtery.zalithlauncher.viewmodel.EditorViewModel import com.movtery.zalithlauncher.viewmodel.ErrorViewModel import com.movtery.zalithlauncher.viewmodel.EventViewModel @@ -200,20 +187,6 @@ private class GameViewModel( /** 启动器默认摇杆样式 */ var launcherJoystickStyle by mutableStateOf(DefaultObservableJoystickStyle) - /** 保存启动器默认摇杆样式 */ - fun saveLauncherJoystickStyle( - onFailed: (Throwable) -> Unit, - onSuccess: suspend () -> Unit - ) { - viewModelScope.launch { - saveJoystickStyle( - path = PathManager.DIR_STYLES, - launcherJoystickStyle, - onFailed = onFailed, - onSuccess = onSuccess - ) - } - } var editorRefresh by mutableIntStateOf(0) private set @@ -612,8 +585,8 @@ fun GameScreen( checkOccupiedPointers = { viewModel.occupiedPointers.contains(it) }, opacity = (AllSettings.controlsOpacity.state.toFloat() / 100f).coerceIn(0f, 1f), markPointerAsMoveOnly = { viewModel.moveOnlyPointers.add(it) }, - isUsingJoystick = isGrabbing && AllSettings.enableJoystickControl.state, isCursorGrabbing = isGrabbing, + enableJoystick = isGrabbing && AllSettings.enableJoystickControl.state, hideLayerWhen = viewModel.controlLayerHideState, isDark = isLauncherInDarkTheme() ) { @@ -651,21 +624,32 @@ fun GameScreen( onOccupiedPointer = { viewModel.occupiedPointers.add(it) }, onReleasePointer = { viewModel.occupiedPointers.remove(it) } ) + } - //摇杆控制层 - viewModel.observableLayout?.let { layout -> - val special by layout.special.collectAsStateWithLifecycle() - JoystickControlLayout( - screenSize = screenSize, - isGrabbing = isGrabbing, - special = special, - defaultStyle = viewModel.launcherJoystickStyle, - hideLayerWhen = viewModel.controlLayerHideState, - viewModel = joystickMovementViewModel, - onKeyEvent = { event, pressed -> - viewModel.onKeyEvent(event, pressed) + //连接布局内的摇杆事件 + viewModel.observableLayout?.let { layout -> + val layoutJoystick = layout.globalJoystick + + LaunchedEffect(layoutJoystick, isGrabbing) { + if (layoutJoystick != null && isGrabbing && AllSettings.enableJoystickControl.state) { + layoutJoystick.onDirectionChanged = { + joystickMovementViewModel.onListen(it) } - ) + layoutJoystick.onCanLock = { lock -> + if (layoutJoystick.lockSpring) { + mapToControlEvent(SPRING, SPRING_VALUE)?.let { key -> + val event = ClickEvent( + type = ClickEvent.Type.Key, + key = key + ) + viewModel.onKeyEvent(event, lock) + } + } + } + } else if (layoutJoystick != null) { + layoutJoystick.onDirectionChanged = {} + layoutJoystick.onCanLock = {} + } } } @@ -722,9 +706,6 @@ fun GameScreen( }, onSendKeycode = { viewModel.sendKeycodeState = SendKeycodeState.ShowDialog }, onReplacementControl = { viewModel.replacementControlState = ReplacementControlState.Show }, - onManageJoystick = { - joystickMovementViewModel.operation = JoystickManageOperation.Manage - }, onEditLayout = { viewModel.startControlEditor( editorVM = editorViewModel @@ -794,30 +775,11 @@ fun GameScreen( } } - //摇杆管理状态操作 - //包含覆盖全屏类UI组件,只能放到顶部 - val saveStyleFailed = stringResource(R.string.game_styles_save_failed) - val savedStyle = stringResource(R.string.generic_saved) - JoystickManageOperation( - operation = joystickMovementViewModel.operation, - onChanged = { joystickMovementViewModel.operation = it }, - launcherJoystick = viewModel.launcherJoystickStyle, - onSaveStyle = { - viewModel.saveLauncherJoystickStyle( - onFailed = { th -> - submitError( - ErrorViewModel.ThrowableMessage( - title = saveStyleFailed, - message = th.getMessageOrToString() - ) - ) - }, - onSuccess = { - withContext(Dispatchers.Main) { - Toast.makeText(context, savedStyle, Toast.LENGTH_SHORT).show() - } - } - ) + JoystickDirectionListener( + viewModel = joystickMovementViewModel, + isGrabbing = isGrabbing, + onKeyEvent = { event, pressed -> + viewModel.onKeyEvent(event, pressed) } ) @@ -1046,113 +1008,3 @@ private fun Offset.sendPosition() { private fun Float.sumPosition(): Float { return (this * (AllSettings.resolutionRatio.state / 100f)) } - -/** - * 摇杆控制层 - * @param isGrabbing 当前游戏是否处于抓获鼠标的状态,根据这个判断是否处于游戏中 - * @param special 由控制布局提供的特殊设定,摇杆会根据这里的配置应用样式 - * @param defaultStyle 由启动器提供的摇杆样式,如果控制布局未提供样式,则使用这个 - * @param hideLayerWhen 复用控制布局的隐藏层逻辑,在鼠标、手柄操作时,根据设置判断是否隐藏摇杆 - * @param viewModel 摇杆移动监听 ViewModel - * @param onKeyEvent 由 - */ -@Composable -private fun JoystickControlLayout( - isGrabbing: Boolean, - screenSize: IntSize, - special: ObservableSpecial, - defaultStyle: ObservableJoystickStyle, - hideLayerWhen: HideLayerWhen, - viewModel: JoystickMovementViewModel, - onKeyEvent: (ClickEvent, pressed: Boolean) -> Unit -) { - val joystickStyle by special.joystickStyle.collectAsStateWithLifecycle() - - val density = LocalDensity.current - val layoutDirection = LocalLayoutDirection.current - - val hideState = when (hideLayerWhen) { - HideLayerWhen.WhenMouse -> AllSettings.joystickHideWhenMouse.state - HideLayerWhen.WhenGamepad -> AllSettings.joystickHideWhenGamepad.state - HideLayerWhen.None -> false - } - - //仅在游戏中或者管理摇杆状态中,显示摇杆 - if ( - ((isGrabbing && !hideState) || viewModel.operation == JoystickManageOperation.Manage) && - AllSettings.enableJoystickControl.state - ) { - val size = AllSettings.joystickControlSize.state.dp - val x = AllSettings.joystickControlX.state - val y = AllSettings.joystickControlY.state - - val position = remember(screenSize, size, x, y, layoutDirection) { - val widgetSize = with(density) { - val pixelSize = size.roundToPx() - IntSize( - width = pixelSize, - height = pixelSize - ) - } - - val originalPosition = widgetPosition( - xPercentage = x / 10000f, - yPercentage = y / 10000f, - widgetSize = widgetSize, - screenSize = screenSize - ) - - if (layoutDirection == LayoutDirection.Rtl) { - //fix: RTL布局下需手动反转X轴,避免使用absoluteOffset - // 不然控件会飞到屏幕外面去 #1034 - val mirroredX = screenSize.width - originalPosition.x - widgetSize.width - Offset(x = mirroredX, y = originalPosition.y) - } else { - originalPosition - } - } - - StyleableJoystick( - modifier = Modifier - .offset { - IntOffset(x = position.x.toInt(), y = position.y.toInt()) - }, - style = if (AllSettings.joystickUseStyleByLayout.state) { - //启用后,优先使用控制布局提供的样式 - joystickStyle ?: defaultStyle - } else { - defaultStyle - }, - size = size, - onDirectionChanged = { direction -> - viewModel.onListen(direction) - }, - deadZoneRatio = AllSettings.joystickDeadZoneRatio.state / 100f, - lockThreshold = AllSettings.joystickLockThreshold.state / 100f, - canLock = AllSettings.joystickControlCanLock.state, - onCanLock = { lock -> - if (AllSettings.joystickControlLockSpring.state) { - mapToControlEvent(SPRING, SPRING_VALUE)?.let { key -> - val event = ClickEvent( - type = ClickEvent.Type.Key, - key = key - ) - if (lock) { - onKeyEvent(event, true) - } else { - onKeyEvent(event, false) - } - } - } - } - ) - } - - JoystickDirectionListener( - viewModel = viewModel, - isGrabbing = isGrabbing, - onKeyEvent = { event, pressed -> - onKeyEvent(event, pressed) - } - ) -} \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/GameMenuSubscreen.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/GameMenuSubscreen.kt index e1d2eb75b..de8f5bd3c 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/GameMenuSubscreen.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/GameMenuSubscreen.kt @@ -94,7 +94,6 @@ fun GameMenuSubscreen( onInputMethod: () -> Unit, onSendKeycode: () -> Unit, onReplacementControl: () -> Unit, - onManageJoystick: () -> Unit, onEditLayout: () -> Unit ) { DualMenuSubscreen( @@ -147,7 +146,6 @@ fun GameMenuSubscreen( onInputMethod = onInputMethod, onSendKeycode = onSendKeycode, onReplacementControl = onReplacementControl, - onManageJoystick = onManageJoystick, onEditLayout = onEditLayout ) } @@ -339,7 +337,6 @@ private fun ControlOverview( onInputMethod: () -> Unit, onSendKeycode: () -> Unit, onReplacementControl: () -> Unit, - onManageJoystick: () -> Unit, onEditLayout: () -> Unit ) { LazyColumn( @@ -413,20 +410,6 @@ private fun ControlOverview( contentColor = contentColor, ) } - - //管理摇杆 - item { - MenuTextButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.game_styles_joystick), - onClick = { - onManageJoystick() - closeScreen() - }, - color = color, - contentColor = contentColor, - ) - } } } @@ -932,4 +915,4 @@ private fun ControlGyroscope( ) } } -} \ No newline at end of file +} diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/JoystickManageOperationUI.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/JoystickManageOperationUI.kt index acc599333..e69de29bb 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/JoystickManageOperationUI.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/game/elements/JoystickManageOperationUI.kt @@ -1,361 +0,0 @@ -/* - * Zalith Launcher 2 - * Copyright (C) 2025 MovTery and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.movtery.zalithlauncher.ui.screens.game.elements - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -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.draw.alpha -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.movtery.layer_controller.observable.ObservableJoystickStyle -import com.movtery.zalithlauncher.R -import com.movtery.zalithlauncher.setting.AllSettings -import com.movtery.zalithlauncher.setting.unit.floatRange -import com.movtery.zalithlauncher.ui.components.MarqueeText -import com.movtery.zalithlauncher.ui.components.fadeEdge -import com.movtery.zalithlauncher.ui.components.verticalScrollWithBar -import com.movtery.zalithlauncher.ui.screens.content.elements.DisabledAlpha -import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutSliderItem -import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutSwitchItem -import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutTextItem -import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick.EditJoystickStyleDialog -import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick.EditJoystickStyleMode -import com.movtery.zalithlauncher.ui.screens.rememberSwapTween -import com.movtery.zalithlauncher.ui.theme.cardColor -import com.movtery.zalithlauncher.ui.theme.onCardColor - -/** 启动器默认摇杆管理操作状态 */ -sealed interface JoystickManageOperation { - data object None : JoystickManageOperation - /** 打开摇杆管理页面 */ - data object Manage : JoystickManageOperation - /** 打开摇杆样式编辑页面 */ - data object Edit : JoystickManageOperation -} - -@Composable -fun JoystickManageOperation( - operation: JoystickManageOperation, - onChanged: (JoystickManageOperation) -> Unit, - launcherJoystick: ObservableJoystickStyle, - onSaveStyle: () -> Unit -) { - //摇杆管理Dialog - JoystickManageDialog( - visible = operation == JoystickManageOperation.Manage, - onDismissRequest = { - onChanged(JoystickManageOperation.None) - }, - onEditStyle = { - onChanged(JoystickManageOperation.Edit) - } - ) - - //摇杆样式编辑Dialog - EditJoystickStyleDialog( - visible = operation == JoystickManageOperation.Edit, - style = launcherJoystick, - mode = EditJoystickStyleMode.Launcher, - onClose = { - onChanged(JoystickManageOperation.None) - }, - onInfoButtonClick = onSaveStyle - ) -} - -/** - * 默认摇杆移动组件属性管理对话框 - * **不再真正使用Dialog,真的会有性能问题!** - */ -@Composable -private fun JoystickManageDialog( - visible: Boolean, - onDismissRequest: () -> Unit, - onEditStyle: () -> Unit, -) { - val tween = rememberSwapTween() - - AnimatedVisibility( - modifier = Modifier.fillMaxSize(), - visible = visible, - enter = fadeIn(animationSpec = tween), - exit = fadeOut(animationSpec = tween) - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - //作为背景层,被点击时关闭Dialog - if (visible) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(0f) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = onDismissRequest - ) - ) - } - - /** 设置属性时降低对话框透明度进行预览 */ - var enableAlpha by remember { mutableStateOf(false) } - val alpha by animateFloatAsState( - targetValue = if (enableAlpha) DisabledAlpha else 1.0f - ) - - Box( - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(0.65f) - .alpha(alpha), - contentAlignment = Alignment.Center - ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(all = 3.dp), - shadowElevation = 3.dp, - color = cardColor(false), - contentColor = onCardColor(), - shape = MaterialTheme.shapes.extraLarge - ) { - Column( - modifier = Modifier.padding(all = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - MarqueeText( - text = stringResource(R.string.game_styles_joystick), - style = MaterialTheme.typography.titleMedium - ) - - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fadeEdge(state = scrollState) - .weight(1f, fill = false) - .fillMaxWidth() - .verticalScrollWithBar(state = scrollState), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - //启用摇杆 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_enable), - value = AllSettings.enableJoystickControl.state, - onValueChange = { AllSettings.enableJoystickControl.save(it) } - ) - - //x - InfoLayoutSliderItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_x), - value = AllSettings.joystickControlX.state / 100f, - onValueChange = { - AllSettings.joystickControlX.updateState((it * 100).toInt()) - enableAlpha = true - }, - valueRange = 0f..100f, - onValueChangeFinished = { - AllSettings.joystickControlX.save() - enableAlpha = false - }, - decimalFormat = "#0.00", - suffix = "%", - enabled = AllSettings.enableJoystickControl.state - ) - - //y - InfoLayoutSliderItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_y), - value = AllSettings.joystickControlY.state / 100f, - onValueChange = { - AllSettings.joystickControlY.updateState((it * 100).toInt()) - enableAlpha = true - }, - valueRange = 0f..100f, - onValueChangeFinished = { - AllSettings.joystickControlY.save() - enableAlpha = false - }, - decimalFormat = "#0.00", - suffix = "%", - enabled = AllSettings.enableJoystickControl.state - ) - - //大小 - InfoLayoutSliderItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_size), - value = AllSettings.joystickControlSize.state.toFloat(), - onValueChange = { - AllSettings.joystickControlSize.updateState(it.toInt()) - enableAlpha = true - }, - valueRange = AllSettings.joystickControlSize.floatRange, - onValueChangeFinished = { - AllSettings.joystickControlSize.save() - enableAlpha = false - }, - decimalFormat = "#0", - suffix = "dp", - fineTuningStep = 1.0f, - enabled = AllSettings.enableJoystickControl.state - ) - - Spacer(Modifier) - - //编辑摇杆样式 - InfoLayoutTextItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_edit), - onClick = onEditStyle - ) - - //使用控制布局提供的样式 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_edit_use_control_layout), - value = AllSettings.joystickUseStyleByLayout.state, - onValueChange = { - AllSettings.joystickUseStyleByLayout.save(it) - }, - enabled = AllSettings.enableJoystickControl.state - ) - - Spacer(Modifier) - - //在实体鼠标操作时隐藏 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_hide_when_mouse), - value = AllSettings.joystickHideWhenMouse.state, - onValueChange = { - AllSettings.joystickHideWhenMouse.save(it) - }, - enabled = AllSettings.enableJoystickControl.state - ) - - //在手柄操作时隐藏 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_hide_when_gamepad), - value = AllSettings.joystickHideWhenGamepad.state, - onValueChange = { - AllSettings.joystickHideWhenGamepad.save(it) - }, - enabled = AllSettings.enableJoystickControl.state - ) - - Spacer(Modifier) - - //摇杆死区范围 - InfoLayoutSliderItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_deadzone), - value = AllSettings.joystickDeadZoneRatio.state.toFloat(), - onValueChange = { AllSettings.joystickDeadZoneRatio.updateState(it.toInt()) }, - onValueChangeFinished = { AllSettings.joystickDeadZoneRatio.save() }, - valueRange = AllSettings.joystickDeadZoneRatio.floatRange, - decimalFormat = "#0", - suffix = "%", - fineTuningStep = 1.0f, - enabled = AllSettings.enableJoystickControl.state - ) - - //摇杆前进锁判定范围 - InfoLayoutSliderItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_lock_threshold), - value = AllSettings.joystickLockThreshold.state.toFloat(), - onValueChange = { AllSettings.joystickLockThreshold.updateState(it.toInt()) }, - onValueChangeFinished = { AllSettings.joystickLockThreshold.save() }, - valueRange = AllSettings.joystickLockThreshold.floatRange, - decimalFormat = "#0", - suffix = "%", - fineTuningStep = 1.0f, - enabled = AllSettings.enableJoystickControl.state - ) - - //前进锁定 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_can_lock), - value = AllSettings.joystickControlCanLock.state, - onValueChange = { - AllSettings.joystickControlCanLock.save(it) - }, - enabled = AllSettings.enableJoystickControl.state - ) - - //锁定时强制疾跑 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.game_styles_joystick_lock_sprint), - value = AllSettings.joystickControlLockSpring.state, - onValueChange = { - AllSettings.joystickControlLockSpring.save(it) - }, - enabled = AllSettings.enableJoystickControl.state && AllSettings.joystickControlCanLock.state - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { - Button( - onClick = onDismissRequest - ) { - MarqueeText(text = stringResource(R.string.generic_close)) - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditor.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditor.kt index 773a56803..93d99766f 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditor.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditor.kt @@ -30,6 +30,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,10 +45,12 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.movtery.layer_controller.ControlEditorLayer import com.movtery.layer_controller.data.ButtonSize import com.movtery.layer_controller.data.CenterPosition +import com.movtery.layer_controller.data.DefaultJoystickData import com.movtery.layer_controller.data.NormalData import com.movtery.layer_controller.data.TextData import com.movtery.layer_controller.data.VisibilityType @@ -56,11 +59,13 @@ import com.movtery.layer_controller.data.createWidgetWithUUID import com.movtery.layer_controller.data.lang.createTranslatable import com.movtery.layer_controller.event.ClickEvent import com.movtery.layer_controller.layout.createNewLayer -import com.movtery.layer_controller.observable.DefaultObservableJoystickStyle import com.movtery.layer_controller.observable.ObservableButtonStyle import com.movtery.layer_controller.observable.ObservableControlLayer +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.layer_controller.observable.ObservableWidget import com.movtery.zalithlauncher.R +import com.movtery.zalithlauncher.bridge.CURSOR_DISABLED +import com.movtery.zalithlauncher.bridge.ZLBridgeStates import com.movtery.zalithlauncher.setting.AllSettings import com.movtery.zalithlauncher.setting.enums.isLauncherInDarkTheme import com.movtery.zalithlauncher.ui.components.MenuState @@ -68,8 +73,6 @@ import com.movtery.zalithlauncher.ui.components.ProgressDialog import com.movtery.zalithlauncher.ui.components.SimpleAlertDialog import com.movtery.zalithlauncher.ui.components.SimpleEditDialog import com.movtery.zalithlauncher.ui.components.rememberBoxSize -import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick.EditJoystickStyleDialog -import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick.EditJoystickStyleMode import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_layer.EditControlLayerDialog import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_layer.EditSwitchLayersVisibilityDialog import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_style.EditButtonStyleDialog @@ -80,6 +83,7 @@ import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_widget.Sel import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_widget.SelectedWidgetData import com.movtery.zalithlauncher.utils.string.getMessageOrToString import com.movtery.zalithlauncher.viewmodel.EditorViewModel +import com.movtery.zalithlauncher.viewmodel.JoystickMovementViewModel import java.io.File /** @@ -94,15 +98,19 @@ fun BoxWithConstraintsScope.ControlEditor( exit: () -> Unit, menuExit: () -> Unit ) { - val layers by viewModel.observableLayout.layers.collectAsStateWithLifecycle() - val styles by viewModel.observableLayout.styles.collectAsStateWithLifecycle() - val special by viewModel.observableLayout.special.collectAsStateWithLifecycle() - val joystickStyle by special.joystickStyle.collectAsStateWithLifecycle() + val observedLayout = viewModel.observableLayout + + val layers by observedLayout.layers.collectAsStateWithLifecycle() + val styles by observedLayout.styles.collectAsStateWithLifecycle() + + val globalJoystick = observedLayout.globalJoystick /** 默认新建的控件层的名称 */ val defaultLayerName = stringResource(R.string.control_editor_edit_layer_default) + /** 默认新建的按键的名称 */ val defaultButtonName = stringResource(R.string.control_editor_edit_button_default) + /** 默认新建的文本框的名称 */ val defaultTextName = stringResource(R.string.control_editor_edit_text_default) @@ -110,12 +118,27 @@ fun BoxWithConstraintsScope.ControlEditor( val screenSize = rememberBoxSize() if (viewModel.isPreviewMode) { + val joystickMovementViewModel: JoystickMovementViewModel = hiltViewModel() + val cursorMode by ZLBridgeStates.cursorMode.collectAsStateWithLifecycle() + val isGrabbing = remember(cursorMode) { + cursorMode == CURSOR_DISABLED + } + + LaunchedEffect(globalJoystick, isGrabbing) { + if (globalJoystick != null && isGrabbing) { + globalJoystick.onDirectionChanged = { + joystickMovementViewModel.onListen(it) + } + } else if (globalJoystick != null) { + globalJoystick.onDirectionChanged = {} + } + } + PreviewControlBox( modifier = Modifier.fillMaxSize(), - observableLayout = viewModel.observableLayout, + observableLayout = observedLayout, previewScenario = viewModel.previewScenario, - previewHideLayerWhen = viewModel.previewHideLayerWhen, - enableJoystick = viewModel.enableJoystick + previewHideLayerWhen = viewModel.previewHideLayerWhen ) } else { ControlEditorLayer( @@ -145,29 +168,28 @@ fun BoxWithConstraintsScope.ControlEditor( } ) //复制控件 - ActionButton( - painter = painterResource(R.drawable.ic_file_copy_filled), - text = stringResource(R.string.control_editor_edit_dialog_clone_widget), - onClick = { - val widget = viewModel.selectedWidget - if (widget != null) { + val widget = viewModel.selectedWidget + if (widget?.layer != null && widget.data !is ObservableJoystickData) { + ActionButton( + painter = painterResource(R.drawable.ic_file_copy_filled), + text = stringResource(R.string.control_editor_edit_dialog_clone_widget), + onClick = { val data = widget.data val layer = widget.layer - viewModel.editorWidgetOperation = EditorWidgetOperation.CloneButton(data, layer) + viewModel.editorWidgetOperation = + EditorWidgetOperation.CloneButton(data, layer) } - } - ) + ) + } //删除 ActionButton( painter = painterResource(R.drawable.ic_delete_filled), text = stringResource(R.string.generic_delete), onClick = { - val widget = viewModel.selectedWidget - if (widget != null) { - val data = widget.data - val layer = widget.layer - viewModel.editorWidgetOperation = EditorWidgetOperation.DeleteButton(data, layer) - } + val data = widget?.data ?: return@ActionButton + val layer = widget.layer + viewModel.editorWidgetOperation = + EditorWidgetOperation.DeleteButton(data, layer) } ) }, @@ -248,11 +270,16 @@ fun BoxWithConstraintsScope.ControlEditor( openStyleList = { viewModel.editorOperation = EditorOperation.OpenStyleList }, + hasJoystick = globalJoystick != null, onEditJoystickStyle = { - if (joystickStyle == null) { - viewModel.editorOperation = EditorOperation.CreateJoystickStyle + if (globalJoystick == null) { + viewModel.addWidget(layers) { layer -> + layer.addJoystick(DefaultJoystickData) + } } else { - viewModel.editorOperation = EditorOperation.EditJoystickStyle + val layer = layers.find { it.joysticks.value.contains(globalJoystick) } + viewModel.selectedWidget = SelectedWidgetData(globalJoystick, layer) + viewModel.editorOperation = EditorOperation.SelectButton } }, isLayerFocus = viewModel.isLayerFocus, @@ -270,20 +297,13 @@ fun BoxWithConstraintsScope.ControlEditor( onPreviewHideLayerChanged = { hideWhen -> viewModel.previewHideLayerWhen = hideWhen }, - enableJoystick = viewModel.enableJoystick, - onJoystickSwitch = { value -> - viewModel.enableJoystick = value - }, - onJoystickTip = { - viewModel.editorOperation = EditorOperation.TipJoystick - }, onSave = { viewModel.save(targetFile, onSaved = {}) }, saveAndExit = { viewModel.save(targetFile, onSaved = exit) }, - onExit = menuExit, + onExit = menuExit ) MenuBox( @@ -311,7 +331,8 @@ fun BoxWithConstraintsScope.ControlEditor( viewModel.editorWidgetOperation = EditorWidgetOperation.EditWidgetText(string) }, switchControlLayers = { data, type -> - viewModel.editorWidgetOperation = EditorWidgetOperation.SwitchLayersVisibility(data, type) + viewModel.editorWidgetOperation = + EditorWidgetOperation.SwitchLayersVisibility(data, type) }, sendText = { data -> viewModel.editorWidgetOperation = EditorWidgetOperation.SendText(data) @@ -329,18 +350,6 @@ fun BoxWithConstraintsScope.ControlEditor( } ) - EditJoystickStyleDialog( - visible = viewModel.editorOperation == EditorOperation.EditJoystickStyle, - style = joystickStyle, - mode = EditJoystickStyleMode.ControlLayout, - onClose = { - viewModel.editorOperation = EditorOperation.None - }, - onInfoButtonClick = { - viewModel.editorOperation = EditorOperation.DeleteJoystickStyle - } - ) - EditorOperation( operation = viewModel.editorOperation, changeOperation = { viewModel.editorOperation = it }, @@ -363,7 +372,6 @@ fun BoxWithConstraintsScope.ControlEditor( hide = baseLayer.hide, hideWhenMouse = baseLayer.hideWhenMouse, hideWhenGamepad = baseLayer.hideWhenGamepad, - hideWhenJoystick = baseLayer.hideWhenJoystick, visibilityType = baseLayer.visibilityType, normalButtons = baseLayer.normalButtons, textBoxes = baseLayer.textBoxes @@ -389,14 +397,6 @@ fun BoxWithConstraintsScope.ControlEditor( onDeleteStyle = { style -> viewModel.removeStyle(style) }, - onCreateJoystickStyle = { - special.setJoystickStyle(DefaultObservableJoystickStyle) - viewModel.editorOperation = EditorOperation.EditJoystickStyle - }, - onDeleteJoystickStyle = { - special.setJoystickStyle(null) - viewModel.editorOperation = EditorOperation.None - }, styles = styles ) @@ -467,15 +467,12 @@ private fun EditorOperation( onCreateStyle: (name: String) -> Unit, onCloneStyle: (ObservableButtonStyle) -> Unit, onDeleteStyle: (ObservableButtonStyle) -> Unit, - onCreateJoystickStyle: () -> Unit, - onDeleteJoystickStyle: () -> Unit, styles: List ) { when (operation) { is EditorOperation.None, is EditorOperation.SelectButton, - is EditorOperation.EditButtonStyle, - is EditorOperation.EditJoystickStyle -> {} + is EditorOperation.EditButtonStyle -> {} is EditorOperation.EditLayer -> { val layer = operation.layer @@ -498,6 +495,7 @@ private fun EditorOperation( }, ) } + is EditorOperation.DeleteLayer -> { val layer = operation.layer SimpleAlertDialog( @@ -512,6 +510,7 @@ private fun EditorOperation( } ) } + is EditorOperation.OpenStyleList -> { StyleListDialog( styles = styles, @@ -530,6 +529,7 @@ private fun EditorOperation( } ) } + is EditorOperation.CreateStyle -> { var name by remember { mutableStateOf("") } SimpleEditDialog( @@ -546,6 +546,7 @@ private fun EditorOperation( } ) } + is EditorOperation.DeleteButtonStyle -> { val style = operation.style SimpleAlertDialog( @@ -560,42 +561,13 @@ private fun EditorOperation( } ) } - is EditorOperation.TipJoystick -> { - SimpleAlertDialog( - title = stringResource(R.string.control_editor_special_joystick_style_tip_title), - text = stringResource(R.string.control_editor_special_joystick_style_tip_summary), - onDismiss = { - changeOperation(EditorOperation.None) - } - ) - } - is EditorOperation.CreateJoystickStyle -> { - SimpleAlertDialog( - title = stringResource(R.string.control_editor_special_joystick_style_create_title), - text = stringResource(R.string.control_editor_special_joystick_style_create_summary), - confirmText = stringResource(R.string.control_manage_create_new), - onConfirm = onCreateJoystickStyle, - onDismiss = { - changeOperation(EditorOperation.None) - } - ) - } - is EditorOperation.DeleteJoystickStyle -> { - SimpleAlertDialog( - title = stringResource(R.string.control_editor_special_joystick_style_delete_title), - text = stringResource(R.string.control_editor_special_joystick_style_delete_summary), - confirmText = stringResource(R.string.generic_delete), - onConfirm = onDeleteJoystickStyle, - onDismiss = { - changeOperation(EditorOperation.None) - } - ) - } + is EditorOperation.Saving -> { ProgressDialog( title = stringResource(R.string.control_manage_saving) ) } + is EditorOperation.SaveFailed -> { SimpleAlertDialog( title = stringResource(R.string.control_manage_failed_to_save), @@ -613,7 +585,7 @@ private fun EditorWidgetOperation( changeOperation: (EditorWidgetOperation) -> Unit, controlLayers: List, onCloneWidgets: (ObservableWidget, List) -> Unit, - onDeleteWidget: (ObservableWidget, ObservableControlLayer) -> Unit, + onDeleteWidget: (ObservableWidget, ObservableControlLayer?) -> Unit, ) { when (operation) { is EditorWidgetOperation.None -> {} @@ -621,7 +593,7 @@ private fun EditorWidgetOperation( val data = operation.data val layer = operation.layer SelectLayers( - layers= controlLayers, + layers = controlLayers, initLayer = layer, onDismissRequest = { changeOperation(EditorWidgetOperation.None) @@ -634,6 +606,7 @@ private fun EditorWidgetOperation( } ) } + is EditorWidgetOperation.DeleteButton -> { val data = operation.data val layer = operation.layer @@ -649,6 +622,7 @@ private fun EditorWidgetOperation( } ) } + is EditorWidgetOperation.EditWidgetText -> { EditTranslatableTextDialog( text = operation.string, @@ -658,11 +632,14 @@ private fun EditorWidgetOperation( } ) } + is EditorWidgetOperation.SendText -> { val data = operation.data //文本内容 var value by remember { - mutableStateOf(data.clickEvents.find { it.type == ClickEvent.Type.SendText }?.key ?: "") + mutableStateOf( + data.clickEvents.find { it.type == ClickEvent.Type.SendText }?.key ?: "" + ) } SimpleEditDialog( title = stringResource(R.string.control_editor_edit_event_launcher_send_text), @@ -690,6 +667,7 @@ private fun EditorWidgetOperation( } ) } + is EditorWidgetOperation.SwitchLayersVisibility -> { val data = operation.data val type = operation.type @@ -720,6 +698,7 @@ private fun EditorWarningOperation( changeOperation(EditorWarningOperation.None) } } + is EditorWarningOperation.WarningNoSelectLayer -> { SimpleAlertDialog( title = stringResource(R.string.control_editor_menu_no_selected_layer_title), @@ -729,4 +708,4 @@ private fun EditorWarningOperation( } } } -} \ No newline at end of file +} diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditorElements.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditorElements.kt index d2765e44a..b74eed5c5 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditorElements.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/ControlEditorElements.kt @@ -104,14 +104,6 @@ sealed interface EditorOperation { data object EditButtonStyle : EditorOperation /** 删除控件外观 */ data class DeleteButtonStyle(val style: ObservableButtonStyle) : EditorOperation - /** 创建摇杆样式独立设定 */ - data object CreateJoystickStyle : EditorOperation - /** 关于摇杆的提醒 */ - data object TipJoystick : EditorOperation - /** 打开摇杆样式独立设定页面 */ - data object EditJoystickStyle : EditorOperation - /** 删除摇杆样式独立设定 */ - data object DeleteJoystickStyle : EditorOperation /** 控制布局正在保存中 */ data object Saving : EditorOperation /** 控制布局保存失败 */ @@ -126,7 +118,7 @@ sealed interface EditorWidgetOperation { /** 选择了一个控件, 并询问用户将其复制到哪些控制层 */ data class CloneButton(val data: ObservableWidget, val layer: ObservableControlLayer) : EditorWidgetOperation /** 删除一个控件 */ - data class DeleteButton(val data: ObservableWidget, val layer: ObservableControlLayer) : EditorWidgetOperation + data class DeleteButton(val data: ObservableWidget, val layer: ObservableControlLayer?) : EditorWidgetOperation /** 编辑控件的显示文本 */ data class EditWidgetText(val string: ObservableTranslatableString) : EditorWidgetOperation /** 编辑切换控件层可见性事件 */ @@ -217,6 +209,7 @@ fun EditorMenu( addNewButton: () -> Unit, addNewText: () -> Unit, openStyleList: () -> Unit, + hasJoystick: Boolean, onEditJoystickStyle: () -> Unit, isLayerFocus: Boolean, onLayerFocusChanged: (Boolean) -> Unit, @@ -226,9 +219,6 @@ fun EditorMenu( onPreviewScenarioChanged: (PreviewScenario) -> Unit, previewHideLayerWhen: HideLayerWhen, onPreviewHideLayerChanged: (HideLayerWhen) -> Unit, - enableJoystick: Boolean, - onJoystickSwitch: (Boolean) -> Unit, - onJoystickTip: () -> Unit, onSave: () -> Unit, saveAndExit: () -> Unit, onExit: () -> Unit @@ -250,6 +240,7 @@ fun EditorMenu( addNewButton = addNewButton, addNewText = addNewText, openStyleList = openStyleList, + hasJoystick = hasJoystick, onEditJoystickStyle = onEditJoystickStyle, isPreviewMode = isPreviewMode, onPreviewChanged = onPreviewChanged, @@ -257,9 +248,6 @@ fun EditorMenu( onPreviewScenarioChanged = onPreviewScenarioChanged, previewHideLayerWhen = previewHideLayerWhen, onPreviewHideLayerChanged = onPreviewHideLayerChanged, - enableJoystick = enableJoystick, - onJoystickSwitch = onJoystickSwitch, - onJoystickTip = onJoystickTip, onSave = onSave, saveAndExit = saveAndExit, onExit = onExit @@ -316,6 +304,7 @@ private fun EditorMenuContent( addNewButton: () -> Unit, addNewText: () -> Unit, openStyleList: () -> Unit, + hasJoystick: Boolean, onEditJoystickStyle: () -> Unit, isPreviewMode: Boolean, onPreviewChanged: (Boolean) -> Unit, @@ -323,9 +312,6 @@ private fun EditorMenuContent( onPreviewScenarioChanged: (PreviewScenario) -> Unit, previewHideLayerWhen: HideLayerWhen, onPreviewHideLayerChanged: (HideLayerWhen) -> Unit, - enableJoystick: Boolean, - onJoystickSwitch: (Boolean) -> Unit, - onJoystickTip: () -> Unit, onSave: () -> Unit, saveAndExit: () -> Unit, onExit: () -> Unit, @@ -362,14 +348,14 @@ private fun EditorMenuContent( ) } - //控件外观列表 + //添加摇杆 item { MenuTextButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.control_editor_edit_style_config), - enabled = isPreviewMode.not(), + enabled = isPreviewMode.not() && !hasJoystick, + text = stringResource(R.string.control_editor_menu_new_widget_joystick), onClick = { - openStyleList() + onEditJoystickStyle() closeScreen() }, color = color, @@ -377,29 +363,14 @@ private fun EditorMenuContent( ) } - //摇杆样式 + //控件外观列表 item { MenuTextButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.control_editor_special_joystick_style), + text = stringResource(R.string.control_editor_edit_style_config), enabled = isPreviewMode.not(), - appendLayout = { - //摇杆提示弹窗 - IconButton( - onClick = { - onJoystickTip() - closeScreen() - }, - enabled = isPreviewMode.not() - ) { - Icon( - painter = painterResource(R.drawable.ic_help_outlined), - contentDescription = stringResource(R.string.generic_tip) - ) - } - }, onClick = { - onEditJoystickStyle() + openStyleList() closeScreen() }, color = color, @@ -476,21 +447,6 @@ private fun EditorMenuContent( ) } - //启用摇杆 - item { - MenuSwitchButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.game_styles_joystick_enable), - switch = enableJoystick, - onSwitch = { value -> - onJoystickSwitch(value) - }, - color = color, - contentColor = contentColor, - enabled = isPreviewMode - ) - } - item { Spacer(modifier = Modifier.height(8.dp)) } diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/PreviewLayout.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/PreviewLayout.kt index 379bc9fcf..b757fefbc 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/PreviewLayout.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/PreviewLayout.kt @@ -20,29 +20,18 @@ package com.movtery.zalithlauncher.ui.screens.main.control_editor import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerId -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.movtery.layer_controller.ControlBoxLayout import com.movtery.layer_controller.data.HideLayerWhen -import com.movtery.layer_controller.observable.DefaultObservableJoystickStyle import com.movtery.layer_controller.observable.ObservableControlLayout -import com.movtery.layer_controller.observable.ObservableSpecial -import com.movtery.layer_controller.utils.widgetPosition -import com.movtery.zalithlauncher.setting.AllSettings import com.movtery.zalithlauncher.setting.enums.isLauncherInDarkTheme import com.movtery.zalithlauncher.ui.components.rememberBoxSize -import com.movtery.zalithlauncher.ui.control.joystick.StyleableJoystick import com.movtery.zalithlauncher.ui.control.mouse.SwitchableMouseLayout /** @@ -57,7 +46,6 @@ fun BoxWithConstraintsScope.PreviewControlBox( observableLayout: ObservableControlLayout, previewScenario: PreviewScenario, previewHideLayerWhen: HideLayerWhen, - enableJoystick: Boolean, modifier: Modifier = Modifier, ) { val occupiedPointers = remember(observableLayout) { mutableStateSetOf() } @@ -70,8 +58,8 @@ fun BoxWithConstraintsScope.PreviewControlBox( observedLayout = observableLayout, checkOccupiedPointers = { occupiedPointers.contains(it) }, markPointerAsMoveOnly = { moveOnlyPointers.add(it) }, - isUsingJoystick = previewScenario.isCursorGrabbing && enableJoystick, isCursorGrabbing = previewScenario.isCursorGrabbing, + enableJoystick = true, hideLayerWhen = previewHideLayerWhen, isDark = isLauncherInDarkTheme() ) { @@ -87,16 +75,6 @@ fun BoxWithConstraintsScope.PreviewControlBox( previewScenario = previewScenario ) } - - //预览摇杆 - val special by observableLayout.special.collectAsStateWithLifecycle() - PreviewJoystickControlLayout( - special = special, - screenSize = screenSize, - enableJoystick = enableJoystick, - previewHideLayerWhen = previewHideLayerWhen, - previewScenario = previewScenario - ) } /** @@ -127,62 +105,4 @@ private fun PreviewMouseLayout( onReleasePointer = onReleasePointer ) } -} - -/** - * 预览摇杆控制层 - * @param special 由控制布局提供的特殊设定,摇杆会根据这里的配置应用样式 - * @param previewScenario 控制布局预览的场景 - */ -@Composable -private fun PreviewJoystickControlLayout( - screenSize: IntSize, - special: ObservableSpecial, - enableJoystick: Boolean, - previewHideLayerWhen: HideLayerWhen, - previewScenario: PreviewScenario -) { - val joystickStyle by special.joystickStyle.collectAsStateWithLifecycle() - - val density = LocalDensity.current - - val hideState = when (previewHideLayerWhen) { - HideLayerWhen.WhenMouse -> AllSettings.joystickHideWhenMouse.state - HideLayerWhen.WhenGamepad -> AllSettings.joystickHideWhenGamepad.state - HideLayerWhen.None -> false - } - - if (enableJoystick && previewScenario.isCursorGrabbing && !hideState) { - val size = AllSettings.joystickControlSize.state.dp - val x = AllSettings.joystickControlX.state - val y = AllSettings.joystickControlY.state - - val position = remember(screenSize, size, x, y) { - val widgetSize = with(density) { - val pixelSize = size.roundToPx() - IntSize( - width = pixelSize, - height = pixelSize - ) - } - - widgetPosition( - xPercentage = x / 10000f, - yPercentage = y / 10000f, - widgetSize = widgetSize, - screenSize = screenSize - ) - } - - StyleableJoystick( - modifier = Modifier - .absoluteOffset { - IntOffset(x = position.x.toInt(), y = position.y.toInt()) - }, - style = joystickStyle ?: DefaultObservableJoystickStyle, //预览模式不显示启动器默认样式 - size = size, - deadZoneRatio = AllSettings.joystickDeadZoneRatio.state / 100f, - canLock = AllSettings.joystickControlCanLock.state - ) - } } \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/EditStyleDialog.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/JoystickStyleEditor.kt similarity index 52% rename from ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/EditStyleDialog.kt rename to ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/JoystickStyleEditor.kt index 239b3da29..b5c792655 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/EditStyleDialog.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_joystick/JoystickStyleEditor.kt @@ -18,28 +18,17 @@ package com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SecondaryTabRow -import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -47,11 +36,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.movtery.layer_controller.data.BORDER_RADIO_RANGE @@ -62,174 +47,74 @@ import com.movtery.layer_controller.observable.ObservableJoystickStyleConfig import com.movtery.zalithlauncher.R import com.movtery.zalithlauncher.setting.unit.toFloatRange import com.movtery.zalithlauncher.ui.components.MarqueeText -import com.movtery.zalithlauncher.ui.control.joystick.StyleableJoystick import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutColorItem import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutSliderItem -import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutTextItem -import com.movtery.zalithlauncher.ui.screens.rememberSwapTween import com.movtery.zalithlauncher.ui.theme.cardColor -import com.movtery.zalithlauncher.ui.theme.itemColor -import com.movtery.zalithlauncher.ui.theme.onCardColor -import com.movtery.zalithlauncher.ui.theme.onItemColor -private data class TabItem(val titleRes: Int) +internal data class JoystickTabItem(val titleRes: Int) -/** - * 编辑摇杆样式模式 - */ -enum class EditJoystickStyleMode { - /** 控制布局编辑其独立的样式 */ - ControlLayout, - /** 编辑启动器默认样式 */ - Launcher -} - -/** - * 摇杆样式编辑对话框 - * **不再真正使用Dialog,真的会有性能问题!** - * @param mode 编辑样式的模式 - * @param onInfoButtonClick 根据模式变更文本的按钮被点击时的回调 - */ @Composable -fun EditJoystickStyleDialog( - visible: Boolean, - style: ObservableJoystickStyle?, - mode: EditJoystickStyleMode, - onClose: () -> Unit, - onInfoButtonClick: () -> Unit +fun JoystickStyleEditor( + modifier: Modifier = Modifier, + style: ObservableJoystickStyle, + onTabChanged: (Int) -> Unit = {} ) { - val tween = rememberSwapTween() - - AnimatedVisibility( - modifier = Modifier.fillMaxSize(), - visible = visible, - enter = fadeIn(animationSpec = tween), - exit = fadeOut(animationSpec = tween) - ) { - val tabs = remember { - listOf( - TabItem(R.string.control_editor_edit_style_config_light), - TabItem(R.string.control_editor_edit_style_config_dark) - ) - } + val tabs = remember { + listOf( + JoystickTabItem(R.string.control_editor_edit_style_config_light), + JoystickTabItem(R.string.control_editor_edit_style_config_dark) + ) + } - val pagerState = rememberPagerState(pageCount = { tabs.size }) - var selectedTabIndex by remember { mutableIntStateOf(0) } + val pagerState = rememberPagerState(pageCount = { tabs.size }) + var selectedTabIndex by remember { mutableIntStateOf(0) } - LaunchedEffect(selectedTabIndex) { - pagerState.animateScrollToPage(selectedTabIndex) - } + LaunchedEffect(selectedTabIndex) { + pagerState.animateScrollToPage(selectedTabIndex) + onTabChanged(selectedTabIndex) + } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Column( + modifier = modifier + ) { + //顶贴标签栏 + SecondaryTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = cardColor(false) ) { - //作为背景层,被点击时关闭Dialog - if (visible) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(0f) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = onClose - ) + tabs.forEachIndexed { index, item -> + Tab( + selected = index == selectedTabIndex, + onClick = { + selectedTabIndex = index + }, + text = { + MarqueeText(text = stringResource(item.titleRes)) + } ) } + } - if (style != null) { - Surface( - modifier = Modifier - .fillMaxWidth(0.8f) - .fillMaxHeight() - .padding(all = 16.dp), - shadowElevation = 3.dp, - color = cardColor(false), - contentColor = onCardColor(), - shape = MaterialTheme.shapes.extraLarge - ) { - Row( - modifier = Modifier.fillMaxHeight() - ) { - Column( - modifier = Modifier - .fillMaxHeight() - .padding(all = 12.dp) - .weight(0.4f), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - RenderBox( - modifier = Modifier.weight(1f), - style = style, - isDarkMode = selectedTabIndex == 1 - ) - - val buttonText = when (mode) { - EditJoystickStyleMode.ControlLayout -> { - //在编辑控制布局独立样式时,这里是删除样式按钮 - stringResource(R.string.generic_delete) - } - EditJoystickStyleMode.Launcher -> { - //在编辑启动器默认样式时,这里是保存按钮 - stringResource(R.string.generic_save) - } - } - InfoLayoutTextItem( - modifier = Modifier.fillMaxWidth(), - title = buttonText, - onClick = onInfoButtonClick, - showArrow = false - ) - } - - Column( - modifier = Modifier - .weight(0.6f) - .fillMaxHeight() - ) { - //顶贴标签栏 - SecondaryTabRow( - selectedTabIndex = selectedTabIndex, - containerColor = cardColor(false) - ) { - tabs.forEachIndexed { index, item -> - Tab( - selected = index == selectedTabIndex, - onClick = { - selectedTabIndex = index - }, - text = { - MarqueeText(text = stringResource(item.titleRes)) - } - ) - } - } + HorizontalPager( + state = pagerState, + userScrollEnabled = false, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { page -> + when (page) { + 0 -> { + StyleConfigEditor( + modifier = Modifier.fillMaxSize(), + config = style.lightStyle, + ) + } - HorizontalPager( - state = pagerState, - userScrollEnabled = false, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { page -> - when (page) { - 0 -> { - StyleConfigEditor( - modifier = Modifier.fillMaxSize(), - config = style.lightStyle, - ) - } - 1 -> { - StyleConfigEditor( - modifier = Modifier.fillMaxSize(), - config = style.darkStyle, - ) - } - } - } - } - } + 1 -> { + StyleConfigEditor( + modifier = Modifier.fillMaxSize(), + config = style.darkStyle, + ) } } } @@ -237,7 +122,7 @@ fun EditJoystickStyleDialog( } @Composable -private fun StyleConfigEditor( +internal fun StyleConfigEditor( modifier: Modifier = Modifier, config: ObservableJoystickStyleConfig ) { @@ -409,36 +294,3 @@ private fun StyleConfigEditor( } } } - -/** - * 渲染摇杆样式预览 - */ -@Composable -private fun RenderBox( - style: ObservableJoystickStyle, - isDarkMode: Boolean, - modifier: Modifier = Modifier, - color: Color = itemColor(false), - contentColor: Color = onItemColor(), - shape: Shape = MaterialTheme.shapes.large -) { - Surface( - modifier = modifier, - color = color, - contentColor = contentColor, - shape = shape - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(all = 16.dp), - contentAlignment = Alignment.Center - ) { - StyleableJoystick( - modifier = Modifier.size(120.dp), - style = style, - isDarkTheme = isDarkMode - ) - } - } -} \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_layer/EditControlLayerDialog.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_layer/EditControlLayerDialog.kt index 0655887e8..c63bf67e4 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_layer/EditControlLayerDialog.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_layer/EditControlLayerDialog.kt @@ -181,14 +181,6 @@ fun EditControlLayerDialog( onValueChange = { layer.hideWhenGamepad = it } ) - //在摇杆组件启用时隐藏 - InfoLayoutSwitchItem( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.control_editor_layers_attribute_hide_when_joystick), - value = layer.hideWhenJoystick, - onValueChange = { layer.hideWhenJoystick = it } - ) - //合并控件至下层 InfoLayoutTextItem( modifier = Modifier.fillMaxWidth(), diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/BasicInfoEdit.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/BasicInfoEdit.kt index 567299d50..241f3db85 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/BasicInfoEdit.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/BasicInfoEdit.kt @@ -39,6 +39,7 @@ import com.movtery.layer_controller.data.ButtonSize import com.movtery.layer_controller.data.MIN_SIZE_DP import com.movtery.layer_controller.data.SIZE_PERCENTAGE_EDITOR import com.movtery.layer_controller.data.VisibilityType +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.layer_controller.observable.ObservableNormalData import com.movtery.layer_controller.observable.ObservableTextData import com.movtery.layer_controller.observable.ObservableWidget @@ -95,6 +96,7 @@ fun EditWidgetInfo( onButtonSizeChanged = { data.buttonSize = it } ) } + is ObservableNormalData -> { commonInfos( onPreviewRequested = onPreviewRequested, @@ -109,8 +111,156 @@ fun EditWidgetInfo( onButtonSizeChanged = { data.buttonSize = it } ) } + + is ObservableJoystickData -> { + joystickInfos( + onPreviewRequested = onPreviewRequested, + onDismissRequested = onDismissRequested, + screenWidth = screenWidth, + screenHeight = screenHeight, + data = data + ) + } + } + } + } +} + +private fun LazyListScope.joystickInfos( + onPreviewRequested: () -> Unit, + onDismissRequested: () -> Unit, + screenWidth: Float, + screenHeight: Float, + data: ObservableJoystickData +) { + val position = data.position + val buttonSize = data.size + + //x + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.control_editor_edit_position_x), + value = position.x / 100f, + onValueChange = { + data.position = position.copy(x = (it * 100).toInt()) + onPreviewRequested() + }, + valueRange = 0f..100f, + onValueChangeFinished = onDismissRequested, + decimalFormat = "#0.00", + suffix = "%" + ) + } + + //y + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.control_editor_edit_position_y), + value = position.y / 100f, + onValueChange = { + data.position = position.copy(y = (it * 100).toInt()) + onPreviewRequested() + }, + valueRange = 0f..100f, + onValueChangeFinished = onDismissRequested, + decimalFormat = "#0.00", + suffix = "%" + ) + } + + item { + Spacer(modifier = Modifier.height(4.dp)) + } + + //尺寸类型 + item { + InfoLayoutListItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.control_editor_edit_size_type), + items = listOf(ButtonSize.Type.Dp, ButtonSize.Type.Percentage), + selectedItem = buttonSize.type, + onItemSelected = { + data.size = buttonSize.copy(type = it) + }, + getItemText = { type -> + val textRes = when (type) { + ButtonSize.Type.Dp -> R.string.control_editor_edit_size_type_dp + ButtonSize.Type.Percentage -> R.string.control_editor_edit_size_type_percentage + ButtonSize.Type.WrapContent -> R.string.control_editor_edit_size_type_wrap_content + } + stringResource(textRes) + } + ) + } + + when (buttonSize.type) { + ButtonSize.Type.Dp -> { + //绝对大小 + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_size), + value = buttonSize.widthDp, + onValueChange = { + data.size = buttonSize.copy(widthDp = it, heightDp = it) + onPreviewRequested() + }, + valueRange = MIN_SIZE_DP..minOf(screenWidth, screenHeight), + onValueChangeFinished = onDismissRequested, + suffix = "Dp" + ) + } + } + + ButtonSize.Type.Percentage -> { + @Composable + fun ButtonSize.Reference.getReferenceText(): String { + val textRes = when (this) { + ButtonSize.Reference.ScreenWidth -> R.string.control_editor_edit_size_reference_screen_width + ButtonSize.Reference.ScreenHeight -> R.string.control_editor_edit_size_reference_screen_height + } + return stringResource(textRes) + } + + //百分比大小 + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_size), + value = buttonSize.widthPercentage / 100f, + onValueChange = { + val percentage = (it * 100).toInt() + data.size = buttonSize.copy( + widthPercentage = percentage, + heightPercentage = percentage + ) + onPreviewRequested() + }, + valueRange = SIZE_PERCENTAGE_EDITOR, + onValueChangeFinished = onDismissRequested, + decimalFormat = "#0.00", + suffix = "%" + ) + } + + //控件尺寸参考对象 + item { + InfoLayoutListItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.control_editor_edit_size_width_reference), + items = ButtonSize.Reference.entries, + selectedItem = buttonSize.widthReference, + onItemSelected = { + data.size = buttonSize.copy(widthReference = it, heightReference = it) + }, + getItemText = { it.getReferenceText() } + ) } } + + ButtonSize.Type.WrapContent -> {} } } @@ -233,8 +383,10 @@ private fun LazyListScope.commonInfos( ) } } + ButtonSize.Type.Percentage -> { - @Composable fun ButtonSize.Reference.getReferenceText(): String { + @Composable + fun ButtonSize.Reference.getReferenceText(): String { val textRes = when (this) { ButtonSize.Reference.ScreenWidth -> R.string.control_editor_edit_size_reference_screen_width ButtonSize.Reference.ScreenHeight -> R.string.control_editor_edit_size_reference_screen_height @@ -304,6 +456,7 @@ private fun LazyListScope.commonInfos( ) } } + ButtonSize.Type.WrapContent -> {} } } \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditJoystickSettings.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditJoystickSettings.kt new file mode 100644 index 000000000..9e55a3cee --- /dev/null +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditJoystickSettings.kt @@ -0,0 +1,108 @@ +/* + * Zalith Launcher 2 + * Copyright (C) 2025 MovTery and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_widget + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.movtery.layer_controller.observable.ObservableJoystickData +import com.movtery.zalithlauncher.R +import com.movtery.zalithlauncher.ui.base.BaseScreen +import com.movtery.zalithlauncher.ui.screens.TitledNavKey +import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutSliderItem +import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutSwitchItem + +/** + * 编辑摇杆设置 + */ +@Composable +fun EditJoystickSettings( + screenKey: TitledNavKey, + currentKey: TitledNavKey?, + data: ObservableJoystickData +) { + BaseScreen( + screenKey = screenKey, + currentKey = currentKey + ) { + LazyColumn( + modifier = Modifier + .padding(start = 4.dp, end = 8.dp) + .fillMaxSize(), + contentPadding = PaddingValues(vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + //摇杆死区范围 + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_deadzone), + value = data.deadZoneRatio * 100f, + onValueChange = { data.deadZoneRatio = it / 100f }, + valueRange = 0f..100f, + decimalFormat = "#0", + suffix = "%", + fineTuningStep = 1.0f + ) + } + + //摇杆前进锁判定范围 + item { + InfoLayoutSliderItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_lock_threshold), + value = data.lockThreshold * 100f, + onValueChange = { data.lockThreshold = it / 100f }, + valueRange = 0f..100f, + decimalFormat = "#0", + suffix = "%", + fineTuningStep = 1.0f + ) + } + + //前进锁定 + item { + InfoLayoutSwitchItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_can_lock), + value = data.canLock, + onValueChange = { data.canLock = it } + ) + } + + //锁定时强制疾跑 + item { + InfoLayoutSwitchItem( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.game_styles_joystick_lock_sprint), + value = data.lockSpring, + onValueChange = { data.lockSpring = it }, + enabled = data.canLock + ) + } + } + } +} diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetDialog.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetDialog.kt index a3b7a8675..ee70ac434 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetDialog.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetDialog.kt @@ -64,7 +64,9 @@ import com.movtery.layer_controller.observable.ObservableButtonStyle import com.movtery.layer_controller.observable.ObservableControlLayer import com.movtery.layer_controller.observable.ObservableNormalData import com.movtery.layer_controller.observable.ObservableTranslatableString +import com.movtery.layer_controller.observable.ObservableTextData import com.movtery.layer_controller.observable.ObservableWidget +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.zalithlauncher.R import com.movtery.zalithlauncher.ui.components.EdgeDirection import com.movtery.zalithlauncher.ui.components.MarqueeText @@ -105,7 +107,7 @@ fun EditWidgetDialog( data: SelectedWidgetData?, styles: List, onDismissRequest: () -> Unit, - onDelete: (ObservableWidget, ObservableControlLayer) -> Unit, + onDelete: (ObservableWidget, ObservableControlLayer?) -> Unit, onClone: (ObservableWidget, ObservableControlLayer) -> Unit, onEditWidgetText: (ObservableTranslatableString) -> Unit, switchControlLayers: (ObservableNormalData, ClickEvent.Type) -> Unit, @@ -149,10 +151,11 @@ fun EditWidgetDialog( if (data != null) { val categories = remember(data) { - if (data.data is ObservableNormalData) { - editWidgetCategories - } else { - editWidgetCategories.filterNot { it.key == EditWidgetCategory.ClickEvent } + when (data.data) { + is ObservableNormalData -> editWidgetCategories.filterNot { it.key == EditWidgetCategory.JoystickSettings } + is ObservableTextData -> editWidgetCategories.filterNot { it.key == EditWidgetCategory.ClickEvent || it.key == EditWidgetCategory.JoystickSettings } + is ObservableJoystickData -> editWidgetCategories.filter { it.key == EditWidgetCategory.Info || it.key == EditWidgetCategory.JoystickSettings || it.key == EditWidgetCategory.Style } + else -> editWidgetCategories } } @@ -248,12 +251,14 @@ fun EditWidgetDialog( MarqueeText(text = stringResource(R.string.generic_delete)) } - FilledTonalButton( - onClick = { - onClone(data.data, data.layer) + if (data.data !is ObservableJoystickData) { + FilledTonalButton( + onClick = { + onClone(data.data, data.layer!!) + } + ) { + MarqueeText(text = stringResource(R.string.control_editor_edit_dialog_clone_widget)) } - ) { - MarqueeText(text = stringResource(R.string.control_editor_edit_dialog_clone_widget)) } Button( @@ -355,6 +360,13 @@ private fun EditWidgetNavigation( sendText = sendText ) } + entry { key -> + EditJoystickSettings( + screenKey = key, + currentKey = currentKey, + data = data as ObservableJoystickData + ) + } entry { key -> EditWidgetStyle( screenKey = key, diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetStyle.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetStyle.kt index c09540919..9bb742e93 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetStyle.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/EditWidgetStyle.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.movtery.layer_controller.layout.RendererStyleBox import com.movtery.layer_controller.observable.ObservableButtonStyle +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.layer_controller.observable.ObservableNormalData import com.movtery.layer_controller.observable.ObservableTextData import com.movtery.layer_controller.observable.ObservableWidget @@ -48,6 +49,7 @@ import com.movtery.zalithlauncher.ui.components.MarqueeText import com.movtery.zalithlauncher.ui.screens.TitledNavKey import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutItem import com.movtery.zalithlauncher.ui.screens.main.control_editor.InfoLayoutTextItem +import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_joystick.JoystickStyleEditor import com.movtery.zalithlauncher.utils.string.isNotEmptyOrBlank /** @@ -78,6 +80,7 @@ fun EditWidgetStyle( openStyleList = openStyleList ) } + is ObservableNormalData -> { MainContent( styles = styles, @@ -86,6 +89,13 @@ fun EditWidgetStyle( openStyleList = openStyleList ) } + + is ObservableJoystickData -> { + JoystickStyleEditor( + modifier = Modifier.fillMaxSize(), + style = data.style + ) + } } } } @@ -151,7 +161,8 @@ private fun ChoseStyleItem( Spacer(modifier = Modifier.height(4.dp)) MarqueeText( modifier = Modifier.fillMaxWidth(), - text = style.name.takeIf { it.isNotEmptyOrBlank() } ?: stringResource(R.string.generic_unspecified), + text = style.name.takeIf { it.isNotEmptyOrBlank() } + ?: stringResource(R.string.generic_unspecified), textAlign = TextAlign.Center ) RadioButton( diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/SelectedWidgetData.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/SelectedWidgetData.kt index 90b4e7af5..39b311fdd 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/SelectedWidgetData.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/SelectedWidgetData.kt @@ -28,5 +28,5 @@ import com.movtery.layer_controller.observable.ObservableWidget */ data class SelectedWidgetData( val data: ObservableWidget, - val layer: ObservableControlLayer + val layer: ObservableControlLayer? ) diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/_DialogNavigation.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/_DialogNavigation.kt index 19b77ae64..f8087269b 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/_DialogNavigation.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/ui/screens/main/control_editor/edit_widget/_DialogNavigation.kt @@ -31,6 +31,8 @@ sealed interface EditWidgetCategory : TitledNavKey { @Serializable data object TextStyle : EditWidgetCategory /** 点击事件 */ @Serializable data object ClickEvent : EditWidgetCategory + /** 摇杆设置 */ + @Serializable data object JoystickSettings : EditWidgetCategory /** 控件样式 */ @Serializable data object Style : EditWidgetCategory } @@ -42,5 +44,6 @@ val editWidgetCategories = listOf( CategoryItem(EditWidgetCategory.Info, { CategoryIcon(R.drawable.ic_info_outlined, R.string.control_editor_edit_category_info) }, R.string.control_editor_edit_category_info), CategoryItem(EditWidgetCategory.TextStyle, { CategoryIcon(R.drawable.ic_text_format, R.string.control_editor_edit_text) }, R.string.control_editor_edit_text), CategoryItem(EditWidgetCategory.ClickEvent, { CategoryIcon(R.drawable.ic_touch_app_outlined, R.string.control_editor_edit_category_event) }, R.string.control_editor_edit_category_event), + CategoryItem(EditWidgetCategory.JoystickSettings, { CategoryIcon(R.drawable.ic_joystick, R.string.control_editor_edit_category_joystick) }, R.string.control_editor_edit_category_joystick), CategoryItem(EditWidgetCategory.Style, { CategoryIcon(R.drawable.ic_style_outlined, R.string.control_editor_edit_category_style) }, R.string.control_editor_edit_category_style) ) \ No newline at end of file diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/EditorViewModel.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/EditorViewModel.kt index 7cb42227e..b3813c242 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/EditorViewModel.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/EditorViewModel.kt @@ -31,6 +31,7 @@ import com.movtery.layer_controller.layout.ControlLayout import com.movtery.layer_controller.observable.ObservableButtonStyle import com.movtery.layer_controller.observable.ObservableControlLayer import com.movtery.layer_controller.observable.ObservableControlLayout +import com.movtery.layer_controller.observable.ObservableJoystickData import com.movtery.layer_controller.observable.ObservableNormalData import com.movtery.layer_controller.observable.ObservableTextData import com.movtery.layer_controller.observable.ObservableWidget @@ -45,17 +46,20 @@ import com.movtery.zalithlauncher.ui.screens.main.control_editor.EditorWidgetOpe import com.movtery.zalithlauncher.ui.screens.main.control_editor.PreviewScenario import com.movtery.zalithlauncher.ui.screens.main.control_editor.edit_widget.SelectedWidgetData import com.movtery.zalithlauncher.ui.theme.showThemed +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File +import javax.inject.Inject /** * 控制布局编辑器 */ -class EditorViewModel() : ViewModel() { +@HiltViewModel +class EditorViewModel @Inject constructor() : ViewModel() { lateinit var observableLayout: ObservableControlLayout private set @@ -117,13 +121,6 @@ class EditorViewModel() : ViewModel() { */ var previewHideLayerWhen by mutableStateOf(HideLayerWhen.None) - /** - * 预览控制布局时是否启用摇杆 - */ - var enableJoystick by mutableStateOf(false) - - - fun initLayout(layout: ControlLayout) { if (!::observableLayout.isInitialized) { this.observableLayout = ObservableControlLayout(layout) @@ -164,10 +161,11 @@ class EditorViewModel() : ViewModel() { /** * 在控件层移除控件 */ - fun removeWidget(layer: ObservableControlLayer, widget: ObservableWidget) { + fun removeWidget(layer: ObservableControlLayer?, widget: ObservableWidget) { when (widget) { - is ObservableNormalData -> layer.removeNormalButton(widget.uuid) - is ObservableTextData -> layer.removeTextBox(widget.uuid) + is ObservableNormalData -> layer?.removeNormalButton(widget.uuid) + is ObservableTextData -> layer?.removeTextBox(widget.uuid) + is ObservableJoystickData -> layer?.removeJoystick(widget.uuid) } } diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/GamepadViewModel.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/GamepadViewModel.kt index 62b7bb350..999b82308 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/GamepadViewModel.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/GamepadViewModel.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.core.util.set import androidx.lifecycle.ViewModel +import com.movtery.layer_controller.layout.JoystickDirection import com.movtery.zalithlauncher.setting.AllSettings import com.movtery.zalithlauncher.ui.control.gamepad.DpadDirection import com.movtery.zalithlauncher.ui.control.gamepad.GamepadMap @@ -36,7 +37,6 @@ import com.movtery.zalithlauncher.ui.control.gamepad.Joystick import com.movtery.zalithlauncher.ui.control.gamepad.JoystickType import com.movtery.zalithlauncher.ui.control.gamepad.keyMappingListMMKV import com.movtery.zalithlauncher.ui.control.gamepad.keyMappingMMKV -import com.movtery.zalithlauncher.ui.control.joystick.JoystickDirection import io.ktor.util.collections.ConcurrentSet private const val BUTTON_PRESS_THRESHOLD = 0.85f diff --git a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/JoystickMovementViewModel.kt b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/JoystickMovementViewModel.kt index b63aa5b1f..e0e377def 100644 --- a/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/JoystickMovementViewModel.kt +++ b/ZalithLauncher/src/main/java/com/movtery/zalithlauncher/viewmodel/JoystickMovementViewModel.kt @@ -18,19 +18,16 @@ package com.movtery.zalithlauncher.viewmodel -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import com.movtery.zalithlauncher.ui.control.joystick.JoystickDirection -import com.movtery.zalithlauncher.ui.screens.game.elements.JoystickManageOperation +import com.movtery.layer_controller.layout.JoystickDirection +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject /** * 摇杆、方向键组件处理 ViewModel */ -class JoystickMovementViewModel: ViewModel() { - var operation by mutableStateOf(JoystickManageOperation.None) - +@HiltViewModel +class JoystickMovementViewModel @Inject constructor() : ViewModel() { private val listeners = mutableListOf<(JoystickDirection) -> Unit>() /** diff --git a/ZalithLauncher/src/main/res/drawable/ic_joystick.xml b/ZalithLauncher/src/main/res/drawable/ic_joystick.xml new file mode 100644 index 000000000..fd0f2382b --- /dev/null +++ b/ZalithLauncher/src/main/res/drawable/ic_joystick.xml @@ -0,0 +1,24 @@ + + + + diff --git a/ZalithLauncher/src/main/res/values-ar/strings.xml b/ZalithLauncher/src/main/res/values-ar/strings.xml index c2f973dc7..04f2ba946 100644 --- a/ZalithLauncher/src/main/res/values-ar/strings.xml +++ b/ZalithLauncher/src/main/res/values-ar/strings.xml @@ -357,19 +357,10 @@ تم تعطيل تراكب قائمة اللعبة. يمكنك فتح قائمة اللعبة باستخدام زر الرجوع في النظام. تحكم في شفافية التخطيط محدد تنسيق التاريخ الإقليمي، والذي يسمح بتحديد التنسيق وفقًا لاتفاقيات اللغة المحلية، ويتطلب وضع أي نص حرفي بين علامتي اقتباس مفردتين \'...\' حتى لا يتم تفسيره على أنه رموز تنسيق. - إدارة عصا التحكم - تفعيل عصا التحكم - موقع عصا التحكم (محور X) - موقع عصا التحكم (محور Y) حجم عصا التحكم - تعديل نمط عصا التحكم - استخدام النمط المحدد في تخطيط عناصر التحكم نطاق منطقة الخمول لعصا التحكم قفل التحرك للأمام فرض الركض عند القفل - إخفاء عند استخدام ماوس خارجي - إخفاء عند استخدام ذراع التحكم - فشل حفظ النمط دعم إصدارات MC: العارض الحالي %s لا يدعم إصدار هذه اللعبة. الاستمرار في استخدامه قد يؤدي إلى أخطاء في العرض أو توقف مفاجئ! هل أنت متأكد من رغبتك في المتابعة؟ يمكن ضبط العارض الحالي %s عبر ملف إعدادات. يرجى تفعيل إذن الوصول إلى وحدة التخزين الخارجية للمشغل أولاً، وإلا فلن يتمكن من قراءة وتحميل إعدادات العارض! diff --git a/ZalithLauncher/src/main/res/values-es/strings.xml b/ZalithLauncher/src/main/res/values-es/strings.xml index 402fa5d5b..814fd690c 100644 --- a/ZalithLauncher/src/main/res/values-es/strings.xml +++ b/ZalithLauncher/src/main/res/values-es/strings.xml @@ -360,7 +360,6 @@ Error al importar Ocurrió un error al instalar el modpack: Opacidad del menú superpuesto - Usar estilo del diseño de controles Diseño de controles Nuevo diseño de controles Nombre del diseño @@ -376,21 +375,10 @@ No se han creado capas de control para este diseño de controles. Por favor, crea una capa de control primero. Previsualizar diseño de controles No hay apariencias de control en este diseño de controles - Debido a la alta complejidad de implementación, el sistema de diseño de controles no puede integrar directamente componentes de joystick. Para usar el joystick, solo puedes habilitar el joystick universal en el menú del juego. Sin embargo, aún puedes definir su apariencia mediante el diseño de controles para mantener un estilo consistente. - Crear una configuración de estilo de joystick proporcionada por el diseño de controles. Si este no proporciona un estilo de joystick, se utilizará el estilo de joystick del launcher. - Eliminar la configuración de estilo del joystick para este diseño de controles y usar la del launcher. - Gestionar joystick - Habilitar joystick - Posición X del joystick - Posición Y del joystick Tamaño del joystick - Editar estilo del joystick Rango de zona muerta del joystick Bloqueo hacia adelante Forzar sprint al bloquear - Ocultar al usar ratón físico - Ocultar al usar gamepad - Error al guardar estilo El renderizador actual %s puede configurarse mediante un archivo de configuración. ¡Por favor, habilita primero el permiso de almacenamiento externo para el launcher, de lo contrario no podrá leer ni cargar la configuración del renderizador! El renderizador actual %s puede configurar semediante un archivo de configuración. ¡Por favor, habilita primero el permiso “Administrar todos los archivos” para el launcher, de lo contrario no podrá leer ni cargar la configuración del renderizador! No hay cuentas disponibles @@ -983,10 +971,6 @@ Esquina superior derecha Esquina inferior izquierda Esquina inferior derecha - Estilo de joystick - ¿No puedes crear joystick? - Crear estilo de joystick - Eliminar estilo de joystick Color del joystick Color del joystick (Bloqueable) Color del joystick (Bloqueado) diff --git a/ZalithLauncher/src/main/res/values-in/strings.xml b/ZalithLauncher/src/main/res/values-in/strings.xml index 6fbac4582..985483b55 100644 --- a/ZalithLauncher/src/main/res/values-in/strings.xml +++ b/ZalithLauncher/src/main/res/values-in/strings.xml @@ -989,15 +989,7 @@ Urutkan berdasarkan terakhir diubah Urutkan berdasarkan terakhir dimainkan Transparansi Overlay Menu - Atur Joystick - Nyalakan Joystick - Posisi X Joystick - Posisi Y Joystick Ukuran Joystick - Edit Gaya Joystick - Sembunyikan Saat Menggunakan Mouse Fisik - Sembunyikan Saat Menggunakan Gamepad - Gagal menyimpan gaya Volume Video Latar Belakang Jika latarnya berupa video, kamu bisa mengatur volumenya lewat opsi ini. Ukuran teks @@ -1005,18 +997,12 @@ Warna Joystick (Bisa dikunci) Warna Joystick (Terkunci) Ukuran Joystick - Buat Gaya Joystick - Buat konfigurasi gaya joystick berdasarkan layout kontrol. Jika layout kontrol saat ini tidak menyediakan gaya joystick, maka gaya joystick dari launcher akan digunakan. Kunci Gerak Maju Warna Indikator Kunci Gerak Maju Menyimpan semua cape ke cache Sematkan - Gaya Joystick - Hapus Gaya Joystick - Hapus konfigurasi gaya joystick untuk layout kontrol saat ini dan gunakan gaya joystick dari launcher. Radius Sudut Latar Belakang Sudut Membulat Joystick - Gunakan gaya yang disediakan oleh layout kontrol Paksa Lari Saat Dikunci Cek Pembaruan Versi Baru Ditemukan @@ -1029,11 +1015,9 @@ Gagal cek pembaruan. Coba lagi nanti. Kamu sudah menggunakan versi terbaru Rentang Deadzone Joystick - Karena tingkat implementasinya cukup rumit, sistem layout kontrol belum bisa langsung mengintegrasikan komponen joystick. Untuk pakai joystick, kamu hanya bisa mengaktifkan joystick universal lewat menu di dalam game. Meski begitu, kamu tetap bisa menentukan tampilan joystick lewat layout kontrol supaya gayanya tetap konsisten. Penyimpanan Cloud Kuda Zombi Ukuran Teks Kustom - Tidak bisa membuat joystick? Sumber Pencarian Platform Aset Sumber Unduhan Platform Aset %s menggunakan sumber mirror MCIM @@ -1148,7 +1132,6 @@ Apakah Anda yakin ingin menghapus layer kontrol %s? Sembunyikan saat menggunakan mouse Sembunyikan saat menggunakan gamepad - Sembunyikan saat joystick diaktifkan Apakah Anda yakin ingin menghapus widget ini? Abaikan tema gelap Apakah Anda yakin ingin menghapus tampilan kontrol %s? diff --git a/ZalithLauncher/src/main/res/values-ja/strings.xml b/ZalithLauncher/src/main/res/values-ja/strings.xml index 33156b92a..f8cbe30e9 100644 --- a/ZalithLauncher/src/main/res/values-ja/strings.xml +++ b/ZalithLauncher/src/main/res/values-ja/strings.xml @@ -994,24 +994,11 @@ テキストサイズ カスタムテキストサイズ 保存済み - ジョイスティックを管理 - ジョイスティックを有効化 - ジョイスティックのX座標 - ジョイスティックのY座標 ジョイスティックの大きさ - ジョイスティックのスタイルを編集 前方に固定 ロック時にダッシュを強制 - 物理マウス使用時に非表示 - ゲームパッド使用時に非表示 - スタイルの保存に失敗しました すべてのマントをキャッシュ Zombie Horseマント - ジョイスティックのスタイル - ジョイスティックのスタイルを作成 - コントロールレイアウトによって提供されるジョイスティックのスタイルを作成します。現在のコントロールがジョイスティックのスタイルを提供していない場合、ランチャーのジョイスティックのスタイルを使用します。 - ジョイスティックのスタイルを削除 - 現在のコントロールレイアウトのジョイスティックのスタイルを削除してランチャーのジョイスティックのスタイルを使用します。 ジョイスティックの色 ジョイスティックの色(ロック可能) ジョイスティックの色(ロック済み) @@ -1019,7 +1006,6 @@ 背景の角の半径 ジョイスティックの角の半径 ジョイスティックの大きさ - コントロールレイアウトが提供するスタイルを使用する アップデートを確認 新しいバージョンが見つかりました 表示 @@ -1032,8 +1018,6 @@ すでに最新バージョンです クラウドドライブ ジョイスティックのデッドゾーン範囲 - ジョイスティックが作れませんか? - 実装がとても複雑なため、コントロールレイアウトシステムはジョイスティックコンポーネントを直接統合できません。ジョイスティックコンポーネントを使用するには、ゲーム内メニューでジョイスティックコンポーネントを有効にすることしかできません。ただし、コントロールレイアウトを通してジョイスティックコンポーネントの見た目をカスタマイズし、一貫したスタイルにすることは可能です。 アセットプラットフォームの検索ソース アセットプラットフォームのダウンロードソース %s はMCIMミラーソースを使っています @@ -1172,7 +1156,6 @@ コントロールレイヤー %s を削除してもよろしいですか? 物理マウス使用時は非表示にする ゲームパッド使用時は非表示にする - ジョイスティック使用時は非表示にする このコントロールを削除してもよろしいでしょうか? ダークテーマを区別しない コントロールの外観 %s を削除してもよろしいですか? diff --git a/ZalithLauncher/src/main/res/values-ko/strings.xml b/ZalithLauncher/src/main/res/values-ko/strings.xml index ff16eac30..9e37ef300 100644 --- a/ZalithLauncher/src/main/res/values-ko/strings.xml +++ b/ZalithLauncher/src/main/res/values-ko/strings.xml @@ -378,20 +378,11 @@ 메뉴 오버레이 켜기 게임 메뉴 오버레이가 비활성화되었습니다. 시스템 뒤로 가기 버튼을 사용하여 게임 메뉴를 열 수 있습니다 조작 레이아웃 투명도 - 조이스틱 관리 - 조이스틱 켜기 - 조이스틱 X축 좌표 - 조이스틱 Y축 좌표 조이스틱 크기 - 조이스틱 스타일 편집 - 레이아웃 기본 스타일 적용 조이스틱 데드존 범위 자동 달리기 활성화 거리 전진 고정 고정 시 자동 달리기 - 마우스 연결 시 숨기기 - 게임패드 연결 시 숨기기 - 스타일 저장 실패 게임을 성공적으로 %s회 실행했습니다! 이 앱이 유용했다면 개발자를 후원해 주시는 건 어떨까요? :) MC 버전 지원: 현재 렌더러 %s은(는) 이 게임 버전을 지원하지 않습니다. 계속 사용할 경우 그래픽 오류나 튕김 현상이 발생할 수 있습니다! 정말 진행하시겠습니까? diff --git a/ZalithLauncher/src/main/res/values-pt-rBR/strings.xml b/ZalithLauncher/src/main/res/values-pt-rBR/strings.xml index fbdd34e9e..c88560a70 100644 --- a/ZalithLauncher/src/main/res/values-pt-rBR/strings.xml +++ b/ZalithLauncher/src/main/res/values-pt-rBR/strings.xml @@ -346,7 +346,6 @@ Clique Direito Mudar layout do HUD Opacidade do HUD - Usar estilo fornecido pelo layout de controle Layout de controle Novo Layout de Controle Nome Do Layout @@ -363,9 +362,6 @@ Nenhuma camada de controle foi criada para o layout de controle atual. Crie uma camada de controle primeiro. Layout de controle de visualização Nenhuma aparição de controle no layout de controle atual layout de controle - Devido à alta complexidade de implementação, o sistema de layout de controle não pode integrar componentes de joystick diretamente. Para usar componentes de joystick, você só pode habilitar o joystick universal no menu do jogo. No entanto, você ainda pode fornecer a aparência do joystick por meio do layout de controle para manter um estilo consistente. - Cria uma configuração de estilo de joystick fornecida pelo layout de controle. Se o layout de controle atual não fornecer um estilo de joystick, o estilo de joystick do launcher será usado. - Exclua a configuração de estilo de joystick para o layout de controle atual e use o estilo de joystick do launcher. Tem certeza de que deseja fechar de maneira forçada e voltar para à tela principal? O jogo está em execução, Aguardando a tela do jogo aparecer… Versão do jogo: %s @@ -385,18 +381,10 @@ Altura da Hotbar Ativar menu flutuante O menu flutuante foi desativada. Você pode abrir a interface novamente usando o botão de voltar do sistema do seu dispositivo. - Gerenciar Joystick - Ativar Joystick - Posição X do joystick - Posição Y do Joystick Tamanho do Joystick - Editar estilo do Joystick Alcance da zona morta do joystick Bloqueio para frente Forçar corrida quando travado - Ocultar ao usar o mouse físico - Ocultar ao usar o gamepad - Falha ao salvar o estilo Suporte à versão do MC: O renderizador atual %s não é compatível com esta versão do jogo. Continuar a usá-lo pode causar erros de renderização ou travamentos! Tem certeza de que deseja prosseguir? O renderizador atual %s pode ser configurado por meio de um arquivo de configuração. Primeiro, habilite a permissão de armazenamento externo para o launcher; caso contrário, ele não poderá ler e carregar a configuração do renderizador! @@ -1020,10 +1008,6 @@ Canto superior direito Canto Inferior Esquerdo Canto Inferior Direito - Estilo do joystick - Não consegue criar um joystick? - Criar estilo de joystick - Excluir estilo de joystick Cor do Joystick Cor do Joystick (Bloqueável) Cor do Joystick (Bloqueada) @@ -1148,7 +1132,6 @@ Mano, ocê tem certeza que deseja excluir a camada %s do seu HUD? Ocultar ao usar o mouse Ocultar ao usar um gamepad - Ocultar quando o joystick estiver ativado Tem certeza de que deseja excluir este widget? Ignorar Tema Escuro Mano, ocê tem certeza que deseja excluir a aparência do HUD %s? diff --git a/ZalithLauncher/src/main/res/values-ru/strings.xml b/ZalithLauncher/src/main/res/values-ru/strings.xml index 2d225477a..244c0c583 100644 --- a/ZalithLauncher/src/main/res/values-ru/strings.xml +++ b/ZalithLauncher/src/main/res/values-ru/strings.xml @@ -1014,24 +1014,11 @@ Размер текста Пользовательский размер текста Сохранено - Настройка джойстика - Включить джойстик - Положение джойстика по оси X - Положение джойстика по оси Y Размер джойстика - Редактировать стиль джойстика Удержание направления (автоспринт) Постоянный спринт при фиксации направления - Скрывать при использовании физической мыши - Скрывать при использовании геймпада - Не удалось сохранить стиль Кэширование всех плащей Зомби-лошадь - Стиль джойстика - Создать стиль джойстика - Создайте собственный стиль джойстика для этой раскладки. Если стиль не задан, лаунчер будет использовать стиль джойстика по умолчанию. - Удалить стиль джойстика - Удалить собственный стиль джойстика для этой раскладки. После этого будет использоваться стандартный стиль джойстика лаунчера. Цвет джойстика Цвет джойстика (при фиксации) Цвет джойстика (зафиксированный) @@ -1039,7 +1026,6 @@ Радиус скругления фона Радиус скругления джойстика Размер джойстика - Использовать стиль из раскладки управления Проверить обновления Найдена новая версия Подробнее @@ -1051,9 +1037,7 @@ Не удалось проверить обновления. Пожалуйста, попробуйте позже. У вас уже установлена последняя версия Облачное хранилище - Из-за сложности реализации система раскладки не поддерживает прямую интеграцию джойстика. Для его использования включите \"Универсальный джойстик\" в игровом меню. При этом внешний вид джойстика можно настроить через раскладку управления, чтобы сохранить единый стиль Диапазон мертвой зоны джойстика - Не получается создать джойстик? Скопировано Поделиться ссылкой Лаунчер загрузит логи игры на публичную платформу и предоставит вам ссылку, которой можно удобно поделиться с другими для диагностики @@ -1207,7 +1191,6 @@ Вы уверены, что хотите удалить слой раскладки %s? Скрывать при использовании мыши Скрывать при использовании геймпада - Скрывать при включённом джойстике Вы уверены, что хотите удалить этот элемент? Не разделять по тёмной теме Вы уверены, что хотите удалить вариант оформления %s? diff --git a/ZalithLauncher/src/main/res/values-tr/strings.xml b/ZalithLauncher/src/main/res/values-tr/strings.xml index 3145cf0f8..51fe546e3 100644 --- a/ZalithLauncher/src/main/res/values-tr/strings.xml +++ b/ZalithLauncher/src/main/res/values-tr/strings.xml @@ -715,18 +715,9 @@ Menü Yer Paylaşımını Etkinleştir Oyun menüsü yer paylaşımı devre dışı bırakıldı. Oyun menüsünü sistem geri düğmesini kullanarak açabilirsiniz Kontrol Düzeni Şeffaflığı - Joystick\'i Yönetin - Joystick\'i Etkinleştir - Joystick X Konumu - Joystick Y Konumu Joystick Boyutu - Joystick Stilini Düzenle - Kontrol Düzeni Tarafından Sağlanan Stili Kullanın İleri Kilit Kilitliyken Zorla Koşma - Fiziksel Fare Kullanırken Gizle - Oyun Kumandası Kullanırken Gizle - Stili Kurtarma Başarısız Mevcut render motoru %s, bir yapılandırma dosyası aracılığıyla yapılandırılabilir. Lütfen öncelikle başlatıcı için harici depolama iznini etkinleştirin, aksi takdirde render motorunun yapılandırmasını okuyamaz ve yükleyemez! Mevcut render motoru %s, bir yapılandırma dosyası aracılığıyla yapılandırılabilir. Lütfen öncelikle başlatıcı için Tüm Dosyaları Yönet iznini etkinleştirin, aksi takdirde render motorunun yapılandırma dosyasını okuyup yükleyemez! Hesabınız Xbox Topluluk Kurallarını ihlal ettiği için yasaklandı ve artık kullanılamaz! @@ -1007,11 +998,6 @@ Önizleme Modu Metin Boyutu Özel Boyut - Joystick Stili - Yeni Stil Oluştur - Özel bir joystick görünümü tasarla. - Stili Sil - Bu stili kalıcı olarak siler. Joystick Rengi Kilitleme Rengi Kilitli Renk @@ -1032,8 +1018,6 @@ Zaten en son sürümü kullanıyorsunuz Bulut Sürücü Joystick Ölü Bölge Aralığı - Joystick oluşturamıyor musunuz? - Uygulamasının zor olması nedeniyle, kontrol düzeni sistemi joystick bileşenlerini doğrudan entegre edemez. Joystick bileşenini kullanmak istiyorsanız, yalnızca oyun içi menüde evrensel joystick bileşenini etkinleştirebilirsiniz. Ancak, kontrol düzeni aracılığıyla joystick bileşeninin görünümünü sağlayarak stil birliğini koruyabilirsiniz. Varlık Platformu Arama Kaynağı Varlık Platformu İndirme Kaynağı %s, MCIM aynalama kaynağını kullanıyor @@ -1158,7 +1142,6 @@ %s kontrol katmanını silmek istediğinizden emin misiniz? Fareyi Kullanırken Gizle Kontrolcü Kullanırken Gizle - Joystick Aktifken Gizle Bu widget\'ı silmek istediğinizden emin misiniz? Koyu Temayı Ayırt Etmeyin %s kontrol görünümünü silmek istediğinizden emin misiniz? diff --git a/ZalithLauncher/src/main/res/values-vi/strings.xml b/ZalithLauncher/src/main/res/values-vi/strings.xml index 936b77b68..d4fd5ba68 100644 --- a/ZalithLauncher/src/main/res/values-vi/strings.xml +++ b/ZalithLauncher/src/main/res/values-vi/strings.xml @@ -80,7 +80,6 @@ Không thể xử lý dữ liệu (%s) Không được cho phép (%s) Không tìm thấy tài nguyên - (%s) Lỗi từ thiết bị của bạn Lỗi bí ẩn: %s Launcher cần giải nén mấy thứ này thì Minecraft mới chạy được. Chúng đã có sẵn trong app, cần giải phóng ra. Bấm nút bên dưới để tiến hành. Launcher đang giải nén, xong sẽ tự vào màn hình chính. Chờ tí nhé. diff --git a/ZalithLauncher/src/main/res/values-zh-rCN/strings.xml b/ZalithLauncher/src/main/res/values-zh-rCN/strings.xml index 5b8cc71c9..af77e6db8 100644 --- a/ZalithLauncher/src/main/res/values-zh-rCN/strings.xml +++ b/ZalithLauncher/src/main/res/values-zh-rCN/strings.xml @@ -423,20 +423,11 @@ 开启菜单悬浮窗 已关闭游戏菜单悬浮窗,您可以通过系统返回键调用游戏菜单 控制布局不透明度 - 管理摇杆 - 启用摇杆 - 摇杆 X 坐标 - 摇杆 Y 坐标 摇杆大小 - 编辑摇杆样式 - 使用控制布局提供的样式 摇杆死区范围 摇杆前进锁判定范围 前进锁定 锁定时强制疾跑 - 在实体鼠标操作时隐藏 - 在手柄操作时隐藏 - 保存样式失败 启动器已经成功运行了 %s 次游戏!如果您觉得好用的话,可以考虑赞助支持一下作者喔!qwq Vulkan 检测 检查您的设备是否支持使用 Vulkan 运行 Minecraft 26.2+,结果仅供参考。 @@ -1098,6 +1089,7 @@ 控制布局格式错误,启动器无法识别它! 编辑器菜单 添加按钮 + 添加摇杆 添加文本框 没有控件层 当前控制布局还没有创建控件层,请先创建一个控件层。 @@ -1123,7 +1115,6 @@ 默认隐藏控件层 在实体鼠标操作时隐藏 在手柄操作时隐藏 - 在摇杆组件启用时隐藏 合并控件至下层 开启预览 关闭预览 @@ -1132,6 +1123,7 @@ 你确定要删除此控件吗? 基本信息 点击事件 + 摇杆设置 控件样式 新控件层 按键 @@ -1206,13 +1198,6 @@ 右上角 左下角 右下角 - 摇杆样式 - 无法创建摇杆? - 由于实现难度较高,控制布局系统无法直接集成摇杆组件。若要使用摇杆组件,您只能在游戏内的菜单中启用通用摇杆组件。不过您仍然可以通过控制布局提供摇杆组件的外观,以统一风格。 - 创建摇杆样式 - 创建由控制布局提供的摇杆的样式设定,如果当前控制布局未提供摇杆样式,则会使用启动器的摇杆样式。 - 删除摇杆样式 - 删除当前控制布局的摇杆样式设定,使用启动器的摇杆样式。 摇杆颜色 摇杆颜色(可锁定时) 摇杆颜色(锁定时) diff --git a/ZalithLauncher/src/main/res/values-zh-rTW/strings.xml b/ZalithLauncher/src/main/res/values-zh-rTW/strings.xml index 4ca3a4646..a8d4baa72 100644 --- a/ZalithLauncher/src/main/res/values-zh-rTW/strings.xml +++ b/ZalithLauncher/src/main/res/values-zh-rTW/strings.xml @@ -382,20 +382,11 @@ 開啟選單懸浮窗 已關閉遊戲選單懸浮窗,您可以透過系統返回鍵呼叫遊戲選單 控制佈局不透明度 - 管理搖桿 - 啟用搖桿 - 搖桿 X 座標 - 搖桿 Y 座標 搖桿大小 - 編輯搖桿樣式 - 使用控制佈局提供的樣式 搖桿死區範圍 搖桿前進鎖定判定範圍 前進鎖定 鎖定時強制疾跑 - 使用實體滑鼠時隱藏 - 使用手把時隱藏 - 儲存樣式失敗 啟動器已成功執行遊戲 %s 次!如果您覺得好用,可以考慮贊助支持作者喔!qwq Vulkan 檢測 檢查您的裝置是否支援使用 Vulkan 執行 Minecraft 26.2+,結果僅供參考 @@ -1132,6 +1123,7 @@ 匯入控制佈局檔案失敗 控制佈局格式錯誤,啟動器無法識別它! 新增按鈕 + 新增搖桿 新增文字框 沒有控制項圖層 目前控制佈局尚未建立控制項圖層,請先建立一個控制項圖層。 @@ -1157,7 +1149,6 @@ 預設隱藏控制項圖層 在實體滑鼠操作時隱藏 在手把操作時隱藏 - 在搖桿元件啟用時隱藏 合併控制項至下層 開啟預覽 關閉預覽 @@ -1166,6 +1157,7 @@ 你確定要刪除此控制項嗎? 基本資訊 點擊事件 + 搖桿設定 控制項樣式 新控制項圖層 按鍵 @@ -1240,13 +1232,6 @@ 右上角 左下角 右下角 - 搖桿樣式 - 無法建立搖桿? - 由於實作難度較高,控制佈局系統無法直接整合搖桿元件。若要使用搖桿元件,您只能在遊戲內的選單中啟用通用搖桿元件。不過您仍然可以透過控制佈局提供搖桿外觀,以統一風格。 - 建立搖桿樣式 - 建立由控制佈局提供的搖桿樣式設定;若目前的控制佈局未提供搖桿樣式,則會使用啟動器的搖桿樣式。 - 刪除搖桿樣式 - 刪除目前控制佈局的搖桿樣式設定,並改用啟動器的搖桿樣式。 搖桿顏色 搖桿顏色(可鎖定時) 搖桿顏色(鎖定時) diff --git a/ZalithLauncher/src/main/res/values/strings.xml b/ZalithLauncher/src/main/res/values/strings.xml index 9d055a2b0..fb087377f 100644 --- a/ZalithLauncher/src/main/res/values/strings.xml +++ b/ZalithLauncher/src/main/res/values/strings.xml @@ -424,20 +424,11 @@ Enable Menu Overlay Game menu overlay has been disabled. You can open the game menu using the system back button Control Layout Opacity - Manage Joystick - Enable Joystick - Joystick X Position - Joystick Y Position Joystick Size - Edit Joystick Style - Use Style Provided by Control Layout Joystick Deadzone Range Joystick Forward Lock Threshold Forward Lock Force Sprint When Locked - Hide When Using Physical Mouse - Hide When Using Gamepad - Failed to Save Style The launcher has successfully launched the game %s times! If you find it useful, please consider supporting the developer! :) Vulkan Check @@ -1101,6 +1092,7 @@ Control layout format error, the launcher cannot recognize it! Editor Menu Add Button + Add Joystick Add Text Box No Control Layers No control layers have been created for the current control layout. Please create a control layer first. @@ -1126,7 +1118,6 @@ Hide Control Layer by Default Hide when using mouse Hide when using gamepad - Hide when joystick is enabled Merge Control Downward Open Preview Close Preview @@ -1135,6 +1126,7 @@ Are you sure you want to delete this widget? Information Click Event + Joystick Settings Control Style New Control Layer Button @@ -1209,13 +1201,6 @@ Top Right Corner Bottom Left Corner Bottom Right Corner - Joystick Style - Can\'t Create Joystick? - Due to high implementation complexity, the control layout system cannot directly integrate joystick components. To use joystick components, you can only enable the universal joystick in the in-game menu. However, you can still provide the joystick appearance through the control layout to maintain a consistent style. - Create Joystick Style - Create a joystick style configuration provided by the control layout. If the current control layout does not provide a joystick style, the launcher\'s joystick style will be used. - Delete Joystick Style - Delete the joystick style configuration for the current control layout and use the launcher\'s joystick style. Joystick Color Joystick Color (Lockable) Joystick Color (Locked) From e8b72b216d6b259f878a002b9d3ddb38b463204d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B1=E9=A3=8E?= Date: Fri, 3 Jul 2026 22:34:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=91=87=E6=9D=86?= =?UTF-8?q?=E7=9A=84=E6=8C=87=E9=92=88=E5=8A=AB=E6=8C=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../observable/ObservableJoystickData.kt | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt index 1a6cee50c..323c48313 100644 --- a/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt +++ b/LayerController/src/main/java/com/movtery/layer_controller/observable/ObservableJoystickData.kt @@ -117,23 +117,29 @@ class ObservableJoystickData(data: JoystickData) : ObservableWidget() { addThis: () -> Unit, consumeEvent: (Boolean) -> Unit ) { - // 使用相对中心点的偏移量进行计算 - val center = Offset( - internalRenderOffsetPx.x + internalRenderSize.width / 2f, - internalRenderOffsetPx.y + internalRenderSize.height / 2f - ) - val pos = change.position - val deadZoneRadius = (internalRenderSize.width * deadZoneRatio) / 2f - val lockThresholdPx = internalRenderSize.width * lockThreshold - - if (change.pressed) { - addThis() - if (isLockedState) { - isLockedState = false - // 如果当前处于锁定状态,按下时应立即通知外部解锁 - onLock(false) + if (!change.pressed) return + + val isAlreadyActive = this in activeWidgets + val isFirstPress = activeWidgets.isEmpty() + + if (isAlreadyActive || isFirstPress) { + if (isFirstPress) { + addThis() + if (isLockedState) { + isLockedState = false + // 如果当前处于锁定状态,按下时应立即通知外部解锁 + onLock(false) + } } + val center = Offset( + internalRenderOffsetPx.x + internalRenderSize.width / 2f, + internalRenderOffsetPx.y + internalRenderSize.height / 2f + ) + val pos = change.position + val deadZoneRadius = (internalRenderSize.width * deadZoneRatio) / 2f + val lockThresholdPx = internalRenderSize.width * lockThreshold + updateState(pos, center, deadZoneRadius, lockThresholdPx) consumeEvent(true) }