diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt index eba454a1..f47c4c3e 100644 --- a/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt @@ -179,8 +179,40 @@ abstract class OnScreenButton( * Moves this button to the given coordinates */ open fun move(x : Float, y : Float) { - relativeX = x / width - relativeY = (y - heightDiff) / adjustedHeight + var adjustedX = x + var adjustedY = y + + if (editInfo.snapToGrid) { + val centerX = width / 2f + val centerY = height / 2f + val gridSize = editInfo.gridSize + // The coordinates of the first grid line for each axis, because the grid is centered and might not start at [0,0] + val startX = centerX % gridSize + val startY = centerY % gridSize + + /** + * The offset to apply to a coordinate to snap it to the grid is the remainder of + * the coordinate divided by the grid size. + * Since the grid is centered on the screen and might not start at [0,0] we need to + * subtract the grid start offset, otherwise we'd be calculating the offset for a grid that starts at [0,0]. + * + * Example: Touch event X: 158 | Grid size: 50 | Grid start X: 40 -> Grid lines at 40, 90, 140, 190, ... + * Snap offset: 158 - 40 = 118 -> 118 % 50 = 18 + * Apply offset to X: 158 - 18 = 140 which is a grid line + * + * If we didn't subtract the grid start offset: + * Snap offset: 158 % 50 = 8 + * Apply offset to X: 158 - 8 = 150 which is not a grid line + */ + val snapOffsetX = (x - startX) % gridSize + val snapOffsetY = (y - startY) % gridSize + + adjustedX = x - snapOffsetX + adjustedY = y - snapOffsetY + } + + relativeX = adjustedX / width + relativeY = (adjustedY - heightDiff) / adjustedHeight } /** diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt index 62b2e47c..024f3330 100644 --- a/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt @@ -69,6 +69,7 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs lateinit var vibrator : Vibrator private val effectClick = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + // Ensure controls init happens after editInfo is initialized so that the buttons have a valid reference to it private val controls = Controls(this) override fun onDraw(canvas : Canvas) { @@ -263,8 +264,7 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs fun setEditMode(editMode : EditMode) { editInfo.editMode = editMode - setOnTouchListener(if (editMode == EditMode.None) playingTouchHandler else editingTouchHandler) - invalidate() + setOnTouchListener(if (isEditing) editingTouchHandler else playingTouchHandler ) } fun resetControls() { @@ -286,6 +286,10 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs invalidate() } + fun setSnapToGrid(snap : Boolean) { + editInfo.snapToGrid = snap + } + fun increaseOpacity() { controls.alpha = (controls.alpha + ALPHA_STEP).coerceIn(ALPHA_RANGE) invalidate() diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt index 4234eb90..55c2bd1e 100644 --- a/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt @@ -12,6 +12,7 @@ import android.os.VibratorManager import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.view.isGone import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton import dagger.hilt.android.AndroidEntryPoint @@ -108,12 +109,26 @@ class OnScreenEditActivity : AppCompatActivity() { } } + private val enableGridAction = { + appSettings.onScreenControlSnapToGrid = true + binding.onScreenControllerView.setSnapToGrid(true) + binding.alignmentGrid.isGone = false + } + + private val disableGridAction = { + appSettings.onScreenControlSnapToGrid = false + binding.onScreenControllerView.setSnapToGrid(false) + binding.alignmentGrid.isGone = true + } + private val actions : List Unit>> = listOf( Pair(R.drawable.ic_palette, paletteAction), Pair(R.drawable.ic_restore) { binding.onScreenControllerView.resetControls() }, Pair(R.drawable.ic_toggle, toggleAction), Pair(R.drawable.ic_move, moveAction), Pair(R.drawable.ic_resize, resizeAction), + Pair(R.drawable.ic_grid_on, enableGridAction), + Pair(R.drawable.ic_grid_off, disableGridAction), Pair(R.drawable.ic_zoom_out) { binding.onScreenControllerView.decreaseScale() }, Pair(R.drawable.ic_zoom_in) { binding.onScreenControllerView.increaseScale() }, Pair(R.drawable.ic_opacity_minus) { binding.onScreenControllerView.decreaseOpacity() }, @@ -147,6 +162,12 @@ class OnScreenEditActivity : AppCompatActivity() { binding.onScreenControllerView.recenterSticks = appSettings.onScreenControlRecenterSticks + val snapToGrid = appSettings.onScreenControlSnapToGrid + binding.onScreenControllerView.setSnapToGrid(snapToGrid) + + binding.alignmentGrid.isGone = !snapToGrid + binding.alignmentGrid.gridSize = OnScreenEditInfo.GridSize + actions.forEach { pair -> binding.fabParent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, binding.fabParent, false).apply { (this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first)) diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditInfo.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditInfo.kt index 09a5be2a..7c22bdad 100644 --- a/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditInfo.kt +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditInfo.kt @@ -5,6 +5,9 @@ package emu.skyline.input.onscreen +import android.util.TypedValue +import emu.skyline.SkylineApplication + enum class EditMode { None, Move, @@ -26,5 +29,20 @@ class OnScreenEditInfo { */ var editButton : OnScreenButton? = null + /** + * Whether the buttons should snap to a grid when in edit mode + */ + var snapToGrid : Boolean = false + + var gridSize : Int = GridSize + val isEditing get() = editMode != EditMode.None + + companion object { + + /** + * The size of the grid, calculated from the value of 8dp + */ + val GridSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, SkylineApplication.context.resources.displayMetrics).toInt() + } } diff --git a/app/src/main/java/emu/skyline/settings/AppSettings.kt b/app/src/main/java/emu/skyline/settings/AppSettings.kt index 5a85035a..42555b0f 100644 --- a/app/src/main/java/emu/skyline/settings/AppSettings.kt +++ b/app/src/main/java/emu/skyline/settings/AppSettings.kt @@ -31,6 +31,7 @@ class AppSettings @Inject constructor(@ApplicationContext private val context : var onScreenControl by sharedPreferences(context, true) var onScreenControlFeedback by sharedPreferences(context, true) var onScreenControlRecenterSticks by sharedPreferences(context, true) + var onScreenControlSnapToGrid by sharedPreferences(context, false) // Other var romFormatFilter by sharedPreferences(context, 0) diff --git a/app/src/main/java/emu/skyline/views/AlignmentGridView.kt b/app/src/main/java/emu/skyline/views/AlignmentGridView.kt new file mode 100644 index 00000000..8e093a14 --- /dev/null +++ b/app/src/main/java/emu/skyline/views/AlignmentGridView.kt @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View + +/** + * A view that draws a grid, used for aligning on-screen controller buttons + */ +class AlignmentGridView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = 0) : View(context, attrs, defStyleAttr) { + var gridSize = 0 + set(value) { + field = value + invalidate() + } + + private val gridPaint = Paint().apply { + color = Color.WHITE + alpha = 135 + } + private val gridCenterPaint = Paint().apply { + color = Color.WHITE + alpha = 55 + strokeWidth = 10f + } + + override fun onDraw(canvas : Canvas) { + super.onDraw(canvas) + drawGrid(canvas) + } + + /** + * Draws a centered grid on the given canvas + */ + private fun drawGrid(canvas : Canvas) { + val centerX = width / 2f + val centerY = height / 2f + val gridSize = gridSize + // Compute the coordinates of the first grid line for each axis, because we want a centered grid, which might not start at [0,0] + val startX = centerX % gridSize + val startY = centerY % gridSize + + // Draw the center lines with a thicker stroke + canvas.drawLine(centerX, 0f, centerX, height.toFloat(), gridCenterPaint) + canvas.drawLine(0f, centerY, width.toFloat(), centerY, gridCenterPaint) + + // Draw the rest of the grid starting from the start coordinates + // Draw vertical lines + for (i in 0..width step gridSize) { + canvas.drawLine(startX + i, 0f, startX + i, height.toFloat(), gridPaint) + } + // Draw horizontal lines + for (i in 0..height step gridSize) { + canvas.drawLine(0f, startY + i, width.toFloat(), startY + i, gridPaint) + } + } +} diff --git a/app/src/main/res/drawable/ic_grid_off.xml b/app/src/main/res/drawable/ic_grid_off.xml new file mode 100644 index 00000000..9dc47d1f --- /dev/null +++ b/app/src/main/res/drawable/ic_grid_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_grid_on.xml b/app/src/main/res/drawable/ic_grid_on.xml new file mode 100644 index 00000000..449ce2f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_grid_on.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/on_screen_edit_activity.xml b/app/src/main/res/layout/on_screen_edit_activity.xml index 20248d23..27751cc3 100644 --- a/app/src/main/res/layout/on_screen_edit_activity.xml +++ b/app/src/main/res/layout/on_screen_edit_activity.xml @@ -4,6 +4,11 @@ android:layout_height="match_parent" android:background="@android:color/black"> + +