Android haptic feedback (#56)

* Android: Add haptic feedback to overlay controls

* Android: Add haptic feedback to overlay controls

---------

Co-authored-by: João Vitor Polverari <polverari.jv@gmail.com>
This commit is contained in:
gperrio 2024-04-10 17:26:39 +02:00 committed by GitHub
parent 2c17f11596
commit b57dc53d93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 9 deletions

View file

@ -581,6 +581,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menu.apply { popupMenu.menu.apply {
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
findItem(R.id.menu_show_fps).isChecked = EmulationMenuSettings.showFps findItem(R.id.menu_show_fps).isChecked = EmulationMenuSettings.showFps
findItem(R.id.menu_haptic_feedback).isChecked = EmulationMenuSettings.hapticFeedback
findItem(R.id.menu_emulation_joystick_rel_center).isChecked = findItem(R.id.menu_emulation_joystick_rel_center).isChecked =
EmulationMenuSettings.joystickRelCenter EmulationMenuSettings.joystickRelCenter
findItem(R.id.menu_emulation_dpad_slide_enable).isChecked = findItem(R.id.menu_emulation_dpad_slide_enable).isChecked =
@ -601,6 +602,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
true true
} }
R.id.menu_haptic_feedback -> {
EmulationMenuSettings.hapticFeedback = !EmulationMenuSettings.hapticFeedback
updateShowFpsOverlay()
true
}
R.id.menu_emulation_edit_layout -> { R.id.menu_emulation_edit_layout -> {
editControlsPlacement() editControlsPlacement()
binding.drawerLayout.close() binding.drawerLayout.close()

View file

@ -85,13 +85,18 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
) )
} }
fun hapticFeedback(type:Int){
if(EmulationMenuSettings.hapticFeedback)
performHapticFeedback(type)
}
override fun onTouch(v: View, event: MotionEvent): Boolean { override fun onTouch(v: View, event: MotionEvent): Boolean {
if (isInEditMode) { if (isInEditMode) {
return onTouchWhileEditing(event) return onTouchWhileEditing(event)
} }
var shouldUpdateView = false var shouldUpdateView = false
for (button in overlayButtons) { for (button in overlayButtons) {
if (!button.updateStatus(event)) { if (!button.updateStatus(event, this)) {
continue continue
} }
@ -103,7 +108,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
shouldUpdateView = true shouldUpdateView = true
} }
for (dpad in overlayDpads) { for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) { if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide, this)) {
continue continue
} }
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.upId, dpad.upStatus) NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.upId, dpad.upStatus)
@ -125,7 +130,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
shouldUpdateView = true shouldUpdateView = true
} }
for (joystick in overlayJoysticks) { for (joystick in overlayJoysticks) {
if (!joystick.updateStatus(event)) { if (!joystick.updateStatus(event, this)) {
continue continue
} }
val axisID = joystick.joystickId val axisID = joystick.joystickId

View file

@ -9,6 +9,7 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import io.github.lime3ds.android.NativeLibrary import io.github.lime3ds.android.NativeLibrary
@ -51,7 +52,7 @@ class InputOverlayDrawableButton(
* *
* @return true if value was changed * @return true if value was changed
*/ */
fun updateStatus(event: MotionEvent): Boolean { fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
val pointerIndex = event.actionIndex val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt() val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt()
@ -67,6 +68,7 @@ class InputOverlayDrawableButton(
} }
pressedState = true pressedState = true
trackId = pointerId trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
return true return true
} }
if (isActionUp) { if (isActionUp) {
@ -75,6 +77,7 @@ class InputOverlayDrawableButton(
} }
pressedState = false pressedState = false
trackId = -1 trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
return true return true
} }
return false return false

View file

@ -9,6 +9,7 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import io.github.lime3ds.android.NativeLibrary import io.github.lime3ds.android.NativeLibrary
@ -59,7 +60,8 @@ class InputOverlayDrawableDpad(
trackId = -1 trackId = -1
} }
fun updateStatus(event: MotionEvent, dpadSlide: Boolean): Boolean { fun updateStatus(event: MotionEvent, dpadSlide: Boolean, overlay:InputOverlay): Boolean {
var isDown = false
val pointerIndex = event.actionIndex val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt() val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt()
@ -73,6 +75,7 @@ class InputOverlayDrawableDpad(
if (!bounds.contains(xPosition, yPosition)) { if (!bounds.contains(xPosition, yPosition)) {
return false return false
} }
isDown = true
trackId = pointerId trackId = pointerId
} }
if (isActionUp) { if (isActionUp) {
@ -84,6 +87,7 @@ class InputOverlayDrawableDpad(
downButtonState = false downButtonState = false
leftButtonState = false leftButtonState = false
rightButtonState = false rightButtonState = false
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
return true return true
} }
if (trackId == -1) { if (trackId == -1) {
@ -114,7 +118,15 @@ class InputOverlayDrawableDpad(
downButtonState = yAxis > VIRT_AXIS_DEADZONE downButtonState = yAxis > VIRT_AXIS_DEADZONE
leftButtonState = xAxis < -VIRT_AXIS_DEADZONE leftButtonState = xAxis < -VIRT_AXIS_DEADZONE
rightButtonState = xAxis > VIRT_AXIS_DEADZONE rightButtonState = xAxis > VIRT_AXIS_DEADZONE
return upState != upButtonState || downState != downButtonState || leftState != leftButtonState || rightState != rightButtonState
val stateChanged = upState != upButtonState || downState != downButtonState || leftState != leftButtonState || rightState != rightButtonState
if(stateChanged)
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
else if(isDown)
overlay.hapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
return stateChanged
} }
return false return false
} }

View file

@ -9,6 +9,7 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import io.github.lime3ds.android.NativeLibrary import io.github.lime3ds.android.NativeLibrary
import io.github.lime3ds.android.utils.EmulationMenuSettings import io.github.lime3ds.android.utils.EmulationMenuSettings
@ -41,6 +42,8 @@ class InputOverlayDrawableJoystick(
var trackId = -1 var trackId = -1
var xAxis = 0f var xAxis = 0f
var yAxis = 0f var yAxis = 0f
var angle = 0f
var radius = 0f
private var controlPositionX = 0 private var controlPositionX = 0
private var controlPositionY = 0 private var controlPositionY = 0
private var previousTouchX = 0 private var previousTouchX = 0
@ -84,7 +87,7 @@ class InputOverlayDrawableJoystick(
boundsBoxBitmap.draw(canvas) boundsBoxBitmap.draw(canvas)
} }
fun updateStatus(event: MotionEvent): Boolean { fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
val pointerIndex = event.actionIndex val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt() val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt()
@ -109,6 +112,7 @@ class InputOverlayDrawableJoystick(
} }
boundsBoxBitmap.bounds = virtBounds boundsBoxBitmap.bounds = virtBounds
trackId = pointerId trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
} }
if (isActionUp) { if (isActionUp) {
if (trackId != pointerId) { if (trackId != pointerId) {
@ -117,12 +121,15 @@ class InputOverlayDrawableJoystick(
pressedState = false pressedState = false
xAxis = 0.0f xAxis = 0.0f
yAxis = 0.0f yAxis = 0.0f
angle = 0.0f
radius = 0.0f
outerBitmap.alpha = 255 outerBitmap.alpha = 255
boundsBoxBitmap.alpha = 0 boundsBoxBitmap.alpha = 0
virtBounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) virtBounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
bounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom) bounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
setInnerBounds() setInnerBounds()
trackId = -1 trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
return true return true
} }
if (trackId == -1) return false if (trackId == -1) return false
@ -142,6 +149,8 @@ class InputOverlayDrawableJoystick(
val yAxis = touchY / maxY val yAxis = touchY / maxY
val oldXAxis = this.xAxis val oldXAxis = this.xAxis
val oldYAxis = this.yAxis val oldYAxis = this.yAxis
val oldAngle = this.angle
val oldRadius = this.radius
// Clamp the circle pad input to a circle // Clamp the circle pad input to a circle
val angle = atan2(yAxis.toDouble(), xAxis.toDouble()).toFloat() val angle = atan2(yAxis.toDouble(), xAxis.toDouble()).toFloat()
@ -152,6 +161,15 @@ class InputOverlayDrawableJoystick(
this.xAxis = cos(angle.toDouble()).toFloat() * radius this.xAxis = cos(angle.toDouble()).toFloat() * radius
this.yAxis = sin(angle.toDouble()).toFloat() * radius this.yAxis = sin(angle.toDouble()).toFloat() * radius
setInnerBounds() setInnerBounds()
if (kotlin.math.abs(oldRadius - radius) > .34f
|| radius > .5f && kotlin.math.abs(oldAngle - angle) > kotlin.math.PI / 8) {
this.radius = radius
this.angle = angle
overlay.hapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
}
return oldXAxis != this.xAxis && oldYAxis != this.yAxis return oldXAxis != this.xAxis && oldYAxis != this.yAxis
} }
return false return false

View file

@ -44,6 +44,13 @@ object EmulationMenuSettings {
.putBoolean("EmulationMenuSettings_ShowFps", value) .putBoolean("EmulationMenuSettings_ShowFps", value)
.apply() .apply()
} }
var hapticFeedback: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_HapticFeedback", true)
set(value) {
preferences.edit()
.putBoolean("EmulationMenuSettings_HapticFeedback", value)
.apply()
}
var swapScreens: Boolean var swapScreens: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_SwapScreens", false) get() = preferences.getBoolean("EmulationMenuSettings_SwapScreens", false)
set(value) { set(value) {

View file

@ -11,6 +11,11 @@
android:title="@string/emulation_show_fps" android:title="@string/emulation_show_fps"
android:checkable="true" /> android:checkable="true" />
<item
android:id="@+id/menu_haptic_feedback"
android:title="@string/emulation_haptic_feedback"
android:checkable="true" />
<item <item
android:id="@+id/menu_emulation_edit_layout" android:id="@+id/menu_emulation_edit_layout"
android:title="@string/emulation_edit_layout" /> android:title="@string/emulation_edit_layout" />

View file

@ -324,6 +324,7 @@
<string name="emulation_empty_state_slot">Slot %1$d</string> <string name="emulation_empty_state_slot">Slot %1$d</string>
<string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</string> <string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</string>
<string name="emulation_show_fps">Show FPS</string> <string name="emulation_show_fps">Show FPS</string>
<string name="emulation_haptic_feedback">Haptic Feedback</string>
<string name="emulation_overlay_options">Overlay Options</string> <string name="emulation_overlay_options">Overlay Options</string>
<string name="emulation_configure_controls">Configure Controls</string> <string name="emulation_configure_controls">Configure Controls</string>
<string name="emulation_edit_layout">Edit Layout</string> <string name="emulation_edit_layout">Edit Layout</string>