Skip to content

Commit

Permalink
Feature: JoyStick added
Browse files Browse the repository at this point in the history
  • Loading branch information
umer0586 committed Jan 12, 2025
1 parent c167a52 commit ee7500f
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum class ItemType{
SWITCH,
SLIDER,
LABEL,
JOYSTICK,
BUTTON,
DPAD
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ data class SliderProperties(
}


@Serializable
data class JoyStickProperties(
val backgroundColor: ULong = Color(0xFFFDD835).value,
val handleColor: ULong = Color(0xFF2A2929).value,
){
fun toJson() = Json.encodeToString(this)
companion object {
fun fromJson(json: String) = Json.decodeFromString<JoyStickProperties>(json)
}
}


private val idToIconMap = mapOf(
0 to R.drawable.ic_power,
1 to R.drawable.ic_up_arrow,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* This file is a part of DroidPad (https://www.github.com/umer0586/DroidPad)
* Copyright (C) 2025 Umer Farooq (umerfarooq2383@gmail.com)
*
* DroidPad 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.
*
* DroidPad 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 DroidPad. If not, see <https://www.gnu.org/licenses/>.
*
*/

package com.github.umer0586.droidpad.ui.components

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.TransformableState
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
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.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.min
import com.github.umer0586.droidpad.data.properties.JoyStickProperties
import com.github.umer0586.droidpad.ui.theme.DroidPadTheme
import kotlin.math.pow
import kotlin.math.sqrt

@Composable
fun ControlPadJoyStick(
modifier: Modifier = Modifier,
offset: Offset,
rotation: Float,
scale: Float,
properties: JoyStickProperties = JoyStickProperties(),
enabled: Boolean = true,
transformableState: TransformableState? = null,
showControls: Boolean = true,
onEditClick: (() -> Unit)? = null,
onDeleteClick: (() -> Unit)? = null,
onMove: ((Float, Float) -> Unit)? = null

) {

ControlPadItemBase(
modifier = modifier,
offset = offset,
rotation = rotation,
scale = scale,
transformableState = transformableState,
showControls = showControls,
onEditClick = onEditClick,
onDeleteClick = onDeleteClick
){
Joystick(
modifier = Modifier.size(150.dp),
enable = enabled,
backgroundColor = Color(properties.backgroundColor),
handleColor = Color(properties.handleColor),
onMove = { x, y ->
onMove?.invoke(x, y)
}
)
}
}

@Composable
fun Joystick(
modifier: Modifier = Modifier,
enable: Boolean = false,
backgroundColor: Color = Color.LightGray,
handleColor: Color = Color.Blue,
onMove: (Float, Float) -> Unit
) {
val handleRadiusFactor = 0.4f // Ratio of handle radius to joystick radius
var handlePosition by remember { mutableStateOf(Offset(0f, 0f)) }
var isDraggingHandle by remember { mutableStateOf(false) } // Track if the handle is being dragged

BoxWithConstraints(modifier = modifier) {
val size = min(maxWidth, maxHeight) // Dp value for joystick size

Canvas(
modifier = Modifier
.size(size) // Use size as Dp
.pointerInput(enable) { // Reinitialize interaction based on `disable`
if (enable) {
detectDragGestures(
onDragStart = { offset ->
val canvasCenter = Offset(size.toPx() / 2, size.toPx() / 2)

// Distance from touch point to joystick handle
val distanceToHandle = (offset - (canvasCenter + handlePosition)).getDistance()

// Start dragging only if within the handle radius
val handleRadius = (size.toPx() / 2) * handleRadiusFactor
isDraggingHandle = distanceToHandle <= handleRadius
},
onDrag = { change, dragAmount ->
if (isDraggingHandle) {
// Calculate new handle position
val joystickRadius = size.toPx() / 2
val newOffset = handlePosition + Offset(dragAmount.x, dragAmount.y)

// Clamp the handle within the circular boundary
val distance = sqrt(newOffset.x.pow(2) + newOffset.y.pow(2))
handlePosition = if (distance <= joystickRadius) {
newOffset
} else {
// Scale to the boundary of the circle
val scale = joystickRadius / distance
Offset(newOffset.x * scale, newOffset.y * scale)
}

// Normalize to range [-1, 1]
// The joystick's normalized value gets very close to 1 or -1 (e.g., 0.9999324) but never reaches exactly 1 due to floating-point precision.
// TODO: To handle this, set a threshold: if the value is greater than or equal to 0.9999, treat it as 1 (or -1).
val normalizedX = handlePosition.x / joystickRadius
val normalizedY = -handlePosition.y / joystickRadius

onMove(normalizedX, normalizedY)

// Consume the gesture
change.consume()
}
},
onDragEnd = {
// Snap handle back to center

handlePosition = Offset(0f, 0f)

onMove(0f, 0f)

isDraggingHandle = false
}
)
}
}
) {
val joystickRadius = size.toPx() / 2
val canvasCenter = Offset(joystickRadius, joystickRadius)
val handleRadius = joystickRadius * handleRadiusFactor

// Draw the joystick area
drawCircle(
color = backgroundColor,
radius = joystickRadius,
center = canvasCenter
)

// Draw the joystick handle
drawCircle(
color = handleColor,
radius = handleRadius,
center = canvasCenter + handlePosition
)
}
}
}




@Preview(showBackground = true)
@Composable
private fun ControlPadJoyStickPreview() {
DroidPadTheme {
ControlPadJoyStick(
offset = Offset.Zero,
rotation = 0f,
scale = 1f,
showControls = true
)
}
}

@Preview(showBackground = true)
@Composable
private fun JoyStickPreview(modifier: Modifier = Modifier) {
DroidPadTheme {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
var cords by remember {
mutableStateOf("(0,0)")
}

Text(
modifier = Modifier.align(Alignment.TopCenter),
text = cords
)

Joystick(
modifier = modifier.size(250.dp),
enable = true,
onMove = { x, y ->
cords = "($x,$y)"
}
)
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,13 @@ import com.github.umer0586.droidpad.data.database.entities.ControlPadItem
import com.github.umer0586.droidpad.data.database.entities.ItemType
import com.github.umer0586.droidpad.data.properties.ButtonProperties
import com.github.umer0586.droidpad.data.properties.DpadProperties
import com.github.umer0586.droidpad.data.properties.JoyStickProperties
import com.github.umer0586.droidpad.data.properties.LabelProperties
import com.github.umer0586.droidpad.data.properties.SliderProperties
import com.github.umer0586.droidpad.data.properties.SwitchProperties
import com.github.umer0586.droidpad.ui.components.ControlPadButton
import com.github.umer0586.droidpad.ui.components.ControlPadDpad
import com.github.umer0586.droidpad.ui.components.ControlPadJoyStick
import com.github.umer0586.droidpad.ui.components.ControlPadSlider
import com.github.umer0586.droidpad.ui.components.ControlPadSwitch
import com.github.umer0586.droidpad.ui.theme.DroidPadTheme
Expand Down Expand Up @@ -198,6 +200,15 @@ fun ItemPropertiesEditorSheet(
)
}
)
} else if(controlPadItem.itemType == ItemType.JOYSTICK){
JoyStickPropertiesEditor(
controlPadItem = controlPadItem,
onJoyStickPropertiesChange = { joyStickProperties ->
modifiedControlPadItem = modifiedControlPadItem.copy(
properties = joyStickProperties.toJson()
)
}
)
}


Expand Down Expand Up @@ -736,6 +747,99 @@ private fun DPadPropertiesEditor(
}
}

@Composable
private fun JoyStickPropertiesEditor(
modifier: Modifier = Modifier,
controlPadItem: ControlPadItem,
onJoyStickPropertiesChange: ((JoyStickProperties) -> Unit)? = null,
) {

var joyStickProperties by remember { mutableStateOf(JoyStickProperties.fromJson(controlPadItem.properties)) }
var showColorPickerForBackground by remember { mutableStateOf(false) }
var showColorPickerForHandle by remember { mutableStateOf(false) }

Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {

ControlPadJoyStick(
offset = Offset.Zero,
scale = 1f,
rotation = 0f,
showControls = false,
enabled = false,
properties = joyStickProperties,
)


AnimatedVisibility(visible = showColorPickerForBackground) {
HsvColorPicker(
modifier = Modifier
.size(200.dp)
.padding(10.dp),
controller = rememberColorPickerController(),
initialColor = Color(joyStickProperties.backgroundColor),
onColorChanged = { colorEnvelope: ColorEnvelope ->
joyStickProperties =
joyStickProperties.copy(backgroundColor = colorEnvelope.color.value)
onJoyStickPropertiesChange?.invoke(joyStickProperties)
// do something
}
)
}

AnimatedVisibility(visible = showColorPickerForHandle) {
HsvColorPicker(
modifier = Modifier
.size(200.dp)
.padding(10.dp),
controller = rememberColorPickerController(),
initialColor = Color(joyStickProperties.handleColor),
onColorChanged = { colorEnvelope: ColorEnvelope ->
joyStickProperties = joyStickProperties.copy(handleColor = colorEnvelope.color.value)
onJoyStickPropertiesChange?.invoke(joyStickProperties)
}
)
}

ListItem(
modifier = Modifier.fillMaxWidth(0.7f),
headlineContent = { Text(text = "Handle Color") },
trailingContent = {
Box(
Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color(joyStickProperties.handleColor))
.clickable {
showColorPickerForHandle = !showColorPickerForHandle
showColorPickerForBackground = false

})
}
)

ListItem(
modifier = Modifier.fillMaxWidth(0.7f),
headlineContent = { Text(text = "Background Color") },
trailingContent = {
Box(
Modifier
.size(20.dp)
.clip(CircleShape)
.background(Color(joyStickProperties.backgroundColor))
.clickable {
showColorPickerForBackground = !showColorPickerForBackground
showColorPickerForHandle = false
})
}
)


}
}

@Composable
private fun SwitchPropertiesEditor(
Expand Down
Loading

0 comments on commit ee7500f

Please sign in to comment.