pineapple-src/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
2024-02-19 23:28:58 +01:00

1051 lines
41 KiB
Kotlin
Executable file

// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.overlay
import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View
import android.view.View.OnTouchListener
import android.view.WindowInsets
import androidx.core.content.ContextCompat
import androidx.window.layout.WindowMetricsCalculator
import kotlin.math.max
import kotlin.math.min
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
import org.yuzu.yuzu_emu.utils.NativeConfig
/**
* Draws the interactive input overlay on top of the
* [SurfaceView] that is rendering emulation.
*/
class InputOverlay(context: Context, attrs: AttributeSet?) :
SurfaceView(context, attrs),
OnTouchListener {
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
private var inEditMode = false
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
private lateinit var windowInsets: WindowInsets
var layout = OverlayLayout.Landscape
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
windowInsets = rootWindowInsets
val overlayControlData = NativeConfig.getOverlayControlData()
if (overlayControlData.isEmpty()) {
populateDefaultConfig()
} else {
checkForNewControls(overlayControlData)
}
// Load the controls.
refreshControls()
// Set the on touch listener.
setOnTouchListener(this)
// Force draw
setWillNotDraw(false)
// Request focus for the overlay so it has priority on presses.
requestFocus()
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
for (button in overlayButtons) {
button.draw(canvas)
}
for (dpad in overlayDpads) {
dpad.draw(canvas)
}
for (joystick in overlayJoysticks) {
joystick.draw(canvas)
}
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (inEditMode) {
return onTouchWhileEditing(event)
}
var shouldUpdateView = false
val playerIndex =
if (NativeInput.isHandheldOnly()) {
NativeInput.ConsoleDevice
} else {
NativeInput.Player1Device
}
for (button in overlayButtons) {
if (!button.updateStatus(event)) {
continue
}
NativeInput.onOverlayButtonEvent(
playerIndex,
button.button,
button.status
)
playHaptics(event)
shouldUpdateView = true
}
for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {
continue
}
NativeInput.onOverlayButtonEvent(
playerIndex,
dpad.up,
dpad.upStatus
)
NativeInput.onOverlayButtonEvent(
playerIndex,
dpad.down,
dpad.downStatus
)
NativeInput.onOverlayButtonEvent(
playerIndex,
dpad.left,
dpad.leftStatus
)
NativeInput.onOverlayButtonEvent(
playerIndex,
dpad.right,
dpad.rightStatus
)
playHaptics(event)
shouldUpdateView = true
}
for (joystick in overlayJoysticks) {
if (!joystick.updateStatus(event)) {
continue
}
NativeInput.onOverlayJoystickEvent(
playerIndex,
joystick.joystick,
joystick.xAxis,
joystick.realYAxis
)
NativeInput.onOverlayButtonEvent(
playerIndex,
joystick.button,
joystick.buttonStatus
)
playHaptics(event)
shouldUpdateView = true
}
if (shouldUpdateView) {
invalidate()
}
if (!BooleanSetting.TOUCHSCREEN.getBoolean()) {
return true
}
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
val isActionUp =
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown && !isTouchInputConsumed(pointerId)) {
NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
}
if (isActionMove) {
for (i in 0 until event.pointerCount) {
val fingerId = event.getPointerId(i)
if (isTouchInputConsumed(fingerId)) {
continue
}
NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i))
}
}
if (isActionUp && !isTouchInputConsumed(pointerId)) {
NativeInput.onTouchReleased(pointerId)
}
return true
}
private fun playHaptics(event: MotionEvent) {
if (BooleanSetting.HAPTIC_FEEDBACK.getBoolean()) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP ->
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
}
}
}
private fun isTouchInputConsumed(track_id: Int): Boolean {
for (button in overlayButtons) {
if (button.trackId == track_id) {
return true
}
}
for (dpad in overlayDpads) {
if (dpad.trackId == track_id) {
return true
}
}
for (joystick in overlayJoysticks) {
if (joystick.trackId == track_id) {
return true
}
}
return false
}
private fun onTouchWhileEditing(event: MotionEvent): Boolean {
val pointerIndex = event.actionIndex
val fingerPositionX = event.getX(pointerIndex).toInt()
val fingerPositionY = event.getY(pointerIndex).toInt()
for (button in overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
// If no button is being moved now, remember the currently touched button to move.
if (buttonBeingConfigured == null &&
button.bounds.contains(fingerPositionX, fingerPositionY)
) {
buttonBeingConfigured = button
buttonBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
buttonBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
// Persist button position by saving new place.
saveControlPosition(
buttonBeingConfigured!!.overlayControlData.id,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
layout
)
buttonBeingConfigured = null
}
}
}
for (dpad in overlayDpads) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
// If no button is being moved now, remember the currently touched button to move.
if (buttonBeingConfigured == null &&
dpad.bounds.contains(fingerPositionX, fingerPositionY)
) {
dpadBeingConfigured = dpad
dpadBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
dpadBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
// Persist button position by saving new place.
saveControlPosition(
OverlayControl.COMBINED_DPAD.id,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
layout
)
dpadBeingConfigured = null
}
}
}
for (joystick in overlayJoysticks) {
when (event.action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
joystick.bounds.contains(fingerPositionX, fingerPositionY)
) {
joystickBeingConfigured = joystick
joystickBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
joystickBeingConfigured!!.onConfigureTouch(event)
invalidate()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
saveControlPosition(
joystickBeingConfigured!!.prefId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
layout
)
joystickBeingConfigured = null
}
}
}
return true
}
private fun addOverlayControls(layout: OverlayLayout) {
val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
val overlayControlData = NativeConfig.getOverlayControlData()
for (data in overlayControlData) {
if (!data.enabled) {
continue
}
val position = data.positionFromLayout(layout)
when (data.id) {
OverlayControl.BUTTON_A.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_a,
R.drawable.facebutton_a_depressed,
NativeButton.A,
data,
position
)
)
}
OverlayControl.BUTTON_B.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_b,
R.drawable.facebutton_b_depressed,
NativeButton.B,
data,
position
)
)
}
OverlayControl.BUTTON_X.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_x,
R.drawable.facebutton_x_depressed,
NativeButton.X,
data,
position
)
)
}
OverlayControl.BUTTON_Y.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_y,
R.drawable.facebutton_y_depressed,
NativeButton.Y,
data,
position
)
)
}
OverlayControl.BUTTON_PLUS.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_plus,
R.drawable.facebutton_plus_depressed,
NativeButton.Plus,
data,
position
)
)
}
OverlayControl.BUTTON_MINUS.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_minus,
R.drawable.facebutton_minus_depressed,
NativeButton.Minus,
data,
position
)
)
}
OverlayControl.BUTTON_HOME.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_home,
R.drawable.facebutton_home_depressed,
NativeButton.Home,
data,
position
)
)
}
OverlayControl.BUTTON_CAPTURE.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.facebutton_screenshot,
R.drawable.facebutton_screenshot_depressed,
NativeButton.Capture,
data,
position
)
)
}
OverlayControl.BUTTON_L.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.l_shoulder,
R.drawable.l_shoulder_depressed,
NativeButton.L,
data,
position
)
)
}
OverlayControl.BUTTON_R.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.r_shoulder,
R.drawable.r_shoulder_depressed,
NativeButton.R,
data,
position
)
)
}
OverlayControl.BUTTON_ZL.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.zl_trigger,
R.drawable.zl_trigger_depressed,
NativeButton.ZL,
data,
position
)
)
}
OverlayControl.BUTTON_ZR.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.zr_trigger,
R.drawable.zr_trigger_depressed,
NativeButton.ZR,
data,
position
)
)
}
OverlayControl.BUTTON_STICK_L.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.button_l3,
R.drawable.button_l3_depressed,
NativeButton.LStick,
data,
position
)
)
}
OverlayControl.BUTTON_STICK_R.id -> {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
R.drawable.button_r3,
R.drawable.button_r3_depressed,
NativeButton.RStick,
data,
position
)
)
}
OverlayControl.STICK_L.id -> {
overlayJoysticks.add(
initializeOverlayJoystick(
context,
windowSize,
R.drawable.joystick_range,
R.drawable.joystick,
R.drawable.joystick_depressed,
NativeAnalog.LStick,
NativeButton.LStick,
data,
position
)
)
}
OverlayControl.STICK_R.id -> {
overlayJoysticks.add(
initializeOverlayJoystick(
context,
windowSize,
R.drawable.joystick_range,
R.drawable.joystick,
R.drawable.joystick_depressed,
NativeAnalog.RStick,
NativeButton.RStick,
data,
position
)
)
}
OverlayControl.COMBINED_DPAD.id -> {
overlayDpads.add(
initializeOverlayDpad(
context,
windowSize,
R.drawable.dpad_standard,
R.drawable.dpad_standard_cardinal_depressed,
R.drawable.dpad_standard_diagonal_depressed,
position
)
)
}
}
}
}
fun refreshControls() {
// Remove all the overlay buttons from the HashSet.
overlayButtons.clear()
overlayDpads.clear()
overlayJoysticks.clear()
// Add all the enabled overlay items back to the HashSet.
if (BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) {
addOverlayControls(layout)
}
invalidate()
}
private fun saveControlPosition(id: String, x: Int, y: Int, layout: OverlayLayout) {
val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
val min = windowSize.first
val max = windowSize.second
val overlayControlData = NativeConfig.getOverlayControlData()
val data = overlayControlData.firstOrNull { it.id == id }
val newPosition = Pair((x - min.x).toDouble() / max.x, (y - min.y).toDouble() / max.y)
when (layout) {
OverlayLayout.Landscape -> data?.landscapePosition = newPosition
OverlayLayout.Portrait -> data?.portraitPosition = newPosition
OverlayLayout.Foldable -> data?.foldablePosition = newPosition
}
NativeConfig.setOverlayControlData(overlayControlData)
}
fun setIsInEditMode(editMode: Boolean) {
inEditMode = editMode
}
/**
* Applies and saves all default values for the overlay
*/
private fun populateDefaultConfig() {
val newConfig = OverlayControl.entries.map { it.toOverlayControlData() }
NativeConfig.setOverlayControlData(newConfig.toTypedArray())
NativeConfig.saveGlobalConfig()
}
/**
* Checks if any new controls were added to OverlayControl that do not exist within deserialized
* config and adds / saves them if necessary
*
* @param overlayControlData Overlay control data from [NativeConfig.getOverlayControlData]
*/
private fun checkForNewControls(overlayControlData: Array<OverlayControlData>) {
val missingControls = mutableListOf<OverlayControlData>()
OverlayControl.entries.forEach { defaultControl ->
val controlData = overlayControlData.firstOrNull { it.id == defaultControl.id }
if (controlData == null) {
missingControls.add(defaultControl.toOverlayControlData())
}
}
if (missingControls.isNotEmpty()) {
NativeConfig.setOverlayControlData(
arrayOf(*overlayControlData, *(missingControls.toTypedArray()))
)
NativeConfig.saveGlobalConfig()
}
}
fun resetLayoutVisibilityAndPlacement() {
defaultOverlayPositionByLayout(layout)
val overlayControlData = NativeConfig.getOverlayControlData()
overlayControlData.forEach {
it.enabled = OverlayControl.from(it.id)?.defaultVisibility == true
}
NativeConfig.setOverlayControlData(overlayControlData)
refreshControls()
}
private fun defaultOverlayPositionByLayout(layout: OverlayLayout) {
val overlayControlData = NativeConfig.getOverlayControlData()
for (data in overlayControlData) {
val defaultControlData = OverlayControl.from(data.id) ?: continue
val position = defaultControlData.getDefaultPositionForLayout(layout)
when (layout) {
OverlayLayout.Landscape -> data.landscapePosition = position
OverlayLayout.Portrait -> data.portraitPosition = position
OverlayLayout.Foldable -> data.foldablePosition = position
}
}
NativeConfig.setOverlayControlData(overlayControlData)
}
override fun isInEditMode(): Boolean {
return inEditMode
}
companion object {
// Increase this number every time there is a breaking change to every overlay layout
const val OVERLAY_VERSION = 1
// Increase the corresponding layout version number whenever that layout has a breaking change
private const val LANDSCAPE_OVERLAY_VERSION = 1
private const val PORTRAIT_OVERLAY_VERSION = 1
private const val FOLDABLE_OVERLAY_VERSION = 1
val overlayLayoutVersions = listOf(
LANDSCAPE_OVERLAY_VERSION,
PORTRAIT_OVERLAY_VERSION,
FOLDABLE_OVERLAY_VERSION
)
/**
* Resizes a [Bitmap] by a given scale factor
*
* @param context Context for getting the vector drawable
* @param drawableId The ID of the drawable to scale.
* @param scale The scale factor for the bitmap.
* @return The scaled [Bitmap]
*/
private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
val bitmap = Bitmap.createBitmap(
(vectorDrawable.intrinsicWidth * scale).toInt(),
(vectorDrawable.intrinsicHeight * scale).toInt(),
Bitmap.Config.ARGB_8888
)
val dm = context.resources.displayMetrics
val minScreenDimension = min(dm.widthPixels, dm.heightPixels)
val maxBitmapDimension = max(bitmap.width, bitmap.height)
val bitmapScale = scale * minScreenDimension / maxBitmapDimension
val scaledBitmap = Bitmap.createScaledBitmap(
bitmap,
(bitmap.width * bitmapScale).toInt(),
(bitmap.height * bitmapScale).toInt(),
true
)
val canvas = Canvas(scaledBitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
vectorDrawable.draw(canvas)
return scaledBitmap
}
/**
* Gets the safe screen size for drawing the overlay
*
* @param context Context for getting the window metrics
* @return A pair of points, the first being the top left corner of the safe area,
* the second being the bottom right corner of the safe area
*/
private fun getSafeScreenSize(
context: Context,
screenSize: Pair<Int, Int>
): Pair<Point, Point> {
// Get screen size
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity)
var maxX = screenSize.first.toFloat()
var maxY = screenSize.second.toFloat()
var minX = 0
var minY = 0
// If we have API access, calculate the safe area to draw the overlay
var cutoutLeft = 0
var cutoutBottom = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null) {
if (insets.boundingRectTop.bottom != 0 &&
insets.boundingRectTop.bottom > maxY / 2
) {
maxY = insets.boundingRectTop.bottom.toFloat()
}
if (insets.boundingRectRight.left != 0 &&
insets.boundingRectRight.left > maxX / 2
) {
maxX = insets.boundingRectRight.left.toFloat()
}
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
}
}
// This makes sure that if we have an inset on one side of the screen, we mirror it on
// the other side. Since removing space from one of the max values messes with the scale,
// we also have to account for it using our min values.
if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
maxX -= (minX * 2)
} else if (minX > 0) {
maxX -= minX
}
if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
maxY -= (minY * 2)
} else if (minY > 0) {
maxY -= minY
}
return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
}
/**
* Initializes an InputOverlayDrawableButton, given by resId, with all of the
* parameters set for it to be properly shown on the InputOverlay.
*
*
* This works due to the way the X and Y coordinates are stored within
* the [SharedPreferences].
*
*
* In the input overlay configuration menu,
* once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
* the X and Y coordinates of the button at the END of its touch event
* (when you remove your finger/stylus from the touchscreen) are then stored in a native .
*
* Technically no modifications should need to be performed on the returned
* InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
* for Android to call the onDraw method.
*
* @param context The current [Context].
* @param windowSize The size of the window to draw the overlay on.
* @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
* @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
* @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
* @param overlayControlData Identifier for determining where a button appears on screen.
* @param position The position on screen as represented by an x and y value between 0 and 1.
* @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
*/
private fun initializeOverlayButton(
context: Context,
windowSize: Pair<Point, Point>,
defaultResId: Int,
pressedResId: Int,
button: NativeButton,
overlayControlData: OverlayControlData,
position: Pair<Double, Double>
): InputOverlayDrawableButton {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
// Decide scale based on button preference ID and user preference
var scale: Float = when (overlayControlData.id) {
OverlayControl.BUTTON_HOME.id,
OverlayControl.BUTTON_CAPTURE.id,
OverlayControl.BUTTON_PLUS.id,
OverlayControl.BUTTON_MINUS.id -> 0.07f
OverlayControl.BUTTON_L.id,
OverlayControl.BUTTON_R.id,
OverlayControl.BUTTON_ZL.id,
OverlayControl.BUTTON_ZR.id -> 0.26f
OverlayControl.BUTTON_STICK_L.id,
OverlayControl.BUTTON_STICK_R.id -> 0.155f
else -> 0.11f
}
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableButton.
val defaultStateBitmap = getBitmap(context, defaultResId, scale)
val pressedStateBitmap = getBitmap(context, pressedResId, scale)
val overlayDrawable = InputOverlayDrawableButton(
res,
defaultStateBitmap,
pressedStateBitmap,
button,
overlayControlData
)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableX = (position.first * max.x + min.x).toInt()
val drawableY = (position.second * max.y + min.y).toInt()
val width = overlayDrawable.width
val height = overlayDrawable.height
// Now set the bounds for the InputOverlayDrawableButton.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
overlayDrawable.setBounds(
drawableX - (width / 2),
drawableY - (height / 2),
drawableX + (width / 2),
drawableY + (height / 2)
)
// Need to set the image's position
overlayDrawable.setPosition(
drawableX - (width / 2),
drawableY - (height / 2)
)
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
return overlayDrawable
}
/**
* Initializes an [InputOverlayDrawableDpad]
*
* @param context The current [Context].
* @param windowSize The size of the window to draw the overlay on.
* @param defaultResId The [Bitmap] resource ID of the default state.
* @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
* @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
* @param position The position on screen as represented by an x and y value between 0 and 1.
* @return The initialized [InputOverlayDrawableDpad]
*/
private fun initializeOverlayDpad(
context: Context,
windowSize: Pair<Point, Point>,
defaultResId: Int,
pressedOneDirectionResId: Int,
pressedTwoDirectionsResId: Int,
position: Pair<Double, Double>
): InputOverlayDrawableDpad {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
// Decide scale based on button ID and user preference
var scale = 0.25f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableDpad.
val defaultStateBitmap =
getBitmap(context, defaultResId, scale)
val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
val pressedTwoDirectionsStateBitmap =
getBitmap(context, pressedTwoDirectionsResId, scale)
val overlayDrawable = InputOverlayDrawableDpad(
res,
defaultStateBitmap,
pressedOneDirectionStateBitmap,
pressedTwoDirectionsStateBitmap
)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableX = (position.first * max.x + min.x).toInt()
val drawableY = (position.second * max.y + min.y).toInt()
val width = overlayDrawable.width
val height = overlayDrawable.height
// Now set the bounds for the InputOverlayDrawableDpad.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
overlayDrawable.setBounds(
drawableX - (width / 2),
drawableY - (height / 2),
drawableX + (width / 2),
drawableY + (height / 2)
)
// Need to set the image's position
overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
return overlayDrawable
}
/**
* Initializes an [InputOverlayDrawableJoystick]
*
* @param context The current [Context]
* @param windowSize The size of the window to draw the overlay on.
* @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
* @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
* @param pressedResInner Resource ID for the pressed inner image of the joystick.
* @param joystick Identifier for which joystick this is.
* @param buttonId Identifier for which joystick button this is.
* @param overlayControlData Identifier for determining where a button appears on screen.
* @param position The position on screen as represented by an x and y value between 0 and 1.
* @return The initialized [InputOverlayDrawableJoystick].
*/
private fun initializeOverlayJoystick(
context: Context,
windowSize: Pair<Point, Point>,
resOuter: Int,
defaultResInner: Int,
pressedResInner: Int,
joystick: NativeAnalog,
button: NativeButton,
overlayControlData: OverlayControlData,
position: Pair<Double, Double>
): InputOverlayDrawableJoystick {
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
// Decide scale based on user preference
var scale = 0.3f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableJoystick.
val bitmapOuter = getBitmap(context, resOuter, scale)
val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableX = (position.first * max.x + min.x).toInt()
val drawableY = (position.second * max.y + min.y).toInt()
val outerScale = 1.66f
// Now set the bounds for the InputOverlayDrawableJoystick.
// This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
val outerSize = bitmapOuter.width
val outerRect = Rect(
drawableX - (outerSize / 2),
drawableY - (outerSize / 2),
drawableX + (outerSize / 2),
drawableY + (outerSize / 2)
)
val innerRect =
Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
// Send the drawableId to the joystick so it can be referenced when saving control position.
val overlayDrawable = InputOverlayDrawableJoystick(
res,
bitmapOuter,
bitmapInnerDefault,
bitmapInnerPressed,
outerRect,
innerRect,
joystick,
button,
overlayControlData.id
)
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY)
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
return overlayDrawable
}
}
}