Implement OSC snap to grid functionality

This commit is contained in:
lynxnb 2023-03-17 19:55:46 +01:00 committed by Niccolò Betto
parent 88084016a1
commit 88e6fc9888
9 changed files with 170 additions and 4 deletions

View File

@ -179,8 +179,40 @@ abstract class OnScreenButton(
* Moves this button to the given coordinates * Moves this button to the given coordinates
*/ */
open fun move(x : Float, y : Float) { open fun move(x : Float, y : Float) {
relativeX = x / width var adjustedX = x
relativeY = (y - heightDiff) / adjustedHeight 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
} }
/** /**

View File

@ -69,6 +69,7 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
lateinit var vibrator : Vibrator lateinit var vibrator : Vibrator
private val effectClick = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) 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) private val controls = Controls(this)
override fun onDraw(canvas : Canvas) { override fun onDraw(canvas : Canvas) {
@ -263,8 +264,7 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
fun setEditMode(editMode : EditMode) { fun setEditMode(editMode : EditMode) {
editInfo.editMode = editMode editInfo.editMode = editMode
setOnTouchListener(if (editMode == EditMode.None) playingTouchHandler else editingTouchHandler) setOnTouchListener(if (isEditing) editingTouchHandler else playingTouchHandler )
invalidate()
} }
fun resetControls() { fun resetControls() {
@ -286,6 +286,10 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
invalidate() invalidate()
} }
fun setSnapToGrid(snap : Boolean) {
editInfo.snapToGrid = snap
}
fun increaseOpacity() { fun increaseOpacity() {
controls.alpha = (controls.alpha + ALPHA_STEP).coerceIn(ALPHA_RANGE) controls.alpha = (controls.alpha + ALPHA_STEP).coerceIn(ALPHA_RANGE)
invalidate() invalidate()

View File

@ -12,6 +12,7 @@ import android.os.VibratorManager
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import dagger.hilt.android.AndroidEntryPoint 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<Pair<Int, () -> Unit>> = listOf( private val actions : List<Pair<Int, () -> Unit>> = listOf(
Pair(R.drawable.ic_palette, paletteAction), Pair(R.drawable.ic_palette, paletteAction),
Pair(R.drawable.ic_restore) { binding.onScreenControllerView.resetControls() }, Pair(R.drawable.ic_restore) { binding.onScreenControllerView.resetControls() },
Pair(R.drawable.ic_toggle, toggleAction), Pair(R.drawable.ic_toggle, toggleAction),
Pair(R.drawable.ic_move, moveAction), Pair(R.drawable.ic_move, moveAction),
Pair(R.drawable.ic_resize, resizeAction), 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_out) { binding.onScreenControllerView.decreaseScale() },
Pair(R.drawable.ic_zoom_in) { binding.onScreenControllerView.increaseScale() }, Pair(R.drawable.ic_zoom_in) { binding.onScreenControllerView.increaseScale() },
Pair(R.drawable.ic_opacity_minus) { binding.onScreenControllerView.decreaseOpacity() }, Pair(R.drawable.ic_opacity_minus) { binding.onScreenControllerView.decreaseOpacity() },
@ -147,6 +162,12 @@ class OnScreenEditActivity : AppCompatActivity() {
binding.onScreenControllerView.recenterSticks = appSettings.onScreenControlRecenterSticks 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 -> actions.forEach { pair ->
binding.fabParent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, binding.fabParent, false).apply { 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)) (this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))

View File

@ -5,6 +5,9 @@
package emu.skyline.input.onscreen package emu.skyline.input.onscreen
import android.util.TypedValue
import emu.skyline.SkylineApplication
enum class EditMode { enum class EditMode {
None, None,
Move, Move,
@ -26,5 +29,20 @@ class OnScreenEditInfo {
*/ */
var editButton : OnScreenButton? = null 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 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()
}
} }

View File

@ -31,6 +31,7 @@ class AppSettings @Inject constructor(@ApplicationContext private val context :
var onScreenControl by sharedPreferences(context, true) var onScreenControl by sharedPreferences(context, true)
var onScreenControlFeedback by sharedPreferences(context, true) var onScreenControlFeedback by sharedPreferences(context, true)
var onScreenControlRecenterSticks by sharedPreferences(context, true) var onScreenControlRecenterSticks by sharedPreferences(context, true)
var onScreenControlSnapToGrid by sharedPreferences(context, false)
// Other // Other
var romFormatFilter by sharedPreferences(context, 0) var romFormatFilter by sharedPreferences(context, 0)

View File

@ -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)
}
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,4v1.45l2,2L10,4h4v4h-3.45l2,2L14,10v1.45l2,2L16,10h4v4h-3.45l2,2L20,16v1.45l2,2L22,4c0,-1.1 -0.9,-2 -2,-2L4.55,2l2,2L8,4zM16,4h4v4h-4L16,4zM1.27,1.27L0,2.55l2,2L2,20c0,1.1 0.9,2 2,2h15.46l2,2 1.27,-1.27L1.27,1.27zM10,12.55L11.45,14L10,14v-1.45zM4,6.55L5.45,8L4,8L4,6.55zM8,20L4,20v-4h4v4zM8,14L4,14v-4h3.45l0.55,0.55L8,14zM14,20h-4v-4h3.45l0.55,0.54L14,20zM16,20v-1.46L17.46,20L16,20z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM8,20L4,20v-4h4v4zM8,14L4,14v-4h4v4zM8,8L4,8L4,4h4v4zM14,20h-4v-4h4v4zM14,14h-4v-4h4v4zM14,8h-4L10,4h4v4zM20,20h-4v-4h4v4zM20,14h-4v-4h4v4zM20,8h-4L16,4h4v4z" />
</vector>

View File

@ -4,6 +4,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/black"> android:background="@android:color/black">
<emu.skyline.views.AlignmentGridView
android:id="@+id/alignment_grid"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<emu.skyline.input.onscreen.OnScreenControllerView <emu.skyline.input.onscreen.OnScreenControllerView
android:id="@+id/on_screen_controller_view" android:id="@+id/on_screen_controller_view"
android:layout_width="match_parent" android:layout_width="match_parent"