Refactor input overlay

This commit is contained in:
SSimco
2025-03-10 18:04:26 +02:00
parent d6e226f129
commit 0c412caa09
36 changed files with 683 additions and 818 deletions
@@ -1,41 +0,0 @@
package info.cemu.cemu.inputoverlay
import android.content.res.Resources
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import info.cemu.cemu.utils.getInvertedDrawable
class InputDrawable(
resources: Resources,
@DrawableRes inputDrawableId: Int,
) {
private val iconInactive: Drawable = ResourcesCompat.getDrawable(
resources,
inputDrawableId,
null
)!!
private val iconActive: Drawable = iconInactive.getInvertedDrawable(resources)
private var _icon = iconInactive
val icon: Drawable
get() = _icon
fun setActiveState(active: Boolean) {
_icon = if (active) iconActive else iconInactive
}
fun setAlpha(alpha: Int) {
iconActive.alpha = alpha
iconInactive.alpha = alpha
}
fun setBounds(bounds: Rect) {
iconActive.bounds = bounds
iconInactive.bounds = bounds
}
fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
setBounds(Rect(left, top, right, bottom))
}
}
@@ -13,8 +13,16 @@ import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View
import android.view.View.OnTouchListener
import androidx.annotation.DrawableRes
import info.cemu.cemu.R
import info.cemu.cemu.inputoverlay.inputs.DPadInput
import info.cemu.cemu.inputoverlay.inputs.Input
import info.cemu.cemu.inputoverlay.inputs.Joystick
import info.cemu.cemu.inputoverlay.inputs.RectangleButton
import info.cemu.cemu.inputoverlay.inputs.RoundButton
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.ButtonInnerDrawing
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.HomeButtonInnerDrawing
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.StickClickInnerDrawing
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.TextButtonInnerDrawing
import info.cemu.cemu.nativeinterface.NativeInput
import info.cemu.cemu.nativeinterface.NativeInput.getControllerType
import info.cemu.cemu.nativeinterface.NativeInput.isControllerDisabled
@@ -35,8 +43,10 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
private var currentConfiguredInput: Input? = null
private var nativeControllerType = -1
private var visible = false
var controllerIndex: Int = 0
private var controllerIndex: Int = 0
private var onJoystickChange: (OverlayInput, Float, Float, Float, Float) -> Unit =
{ _, _, _, _, _ -> }
private var overlyButtonToNativeButton: (OverlayInput) -> Int = { _ -> -1 }
private var inputs: MutableList<Pair<OverlayInput, Input>>? = null
private val settingsProvider: InputOverlaySettingsManager
private val vibrator: Vibrator?
@@ -85,7 +95,6 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
return
}
for ((overlayInput, input) in inputs!!) {
input.reset()
settingsProvider.saveRectangle(overlayInput, input.getBoundingRectangle())
}
}
@@ -188,13 +197,7 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
}
private fun onButtonStateChange(button: OverlayInput, state: Boolean) {
val nativeButtonId = when (nativeControllerType) {
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> overlayButtonToVPADButton(button)
NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> overlayButtonToClassicButton(button)
NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> overlayButtonToProButton(button)
NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> overlayButtonToWiimoteButton(button)
else -> -1
}
val nativeButtonId = overlyButtonToNativeButton(button)
if (nativeButtonId == -1) {
return
}
@@ -284,13 +287,6 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
}
private fun onJoystickStateChange(joystick: OverlayInput, x: Float, y: Float) {
val onJoystickChange = when (nativeControllerType) {
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> ::onVPADJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> ::onProJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> ::onClassicJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> ::onWiimoteJoystickStateChange
else -> return
}
val (up, down) = if (y < 0) Pair(-y, 0f) else Pair(0f, y)
val (left, right) = if (x < 0) Pair(-x, 0f) else Pair(0f, x)
onJoystickChange(joystick, up, down, left, right)
@@ -300,15 +296,13 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
return settingsProvider.getInputOverlayRectangle(input, width, height, pixelDensity)
}
private fun MutableList<Pair<OverlayInput, Input>>.addRoundButton(
button: OverlayButton,
@DrawableRes buttonDrawable: Int,
innerDrawing: ButtonInnerDrawing,
) {
add(
button to RoundButton(
resources,
buttonDrawable,
innerDrawing,
{ onButtonStateChange(button, it) },
currentAlpha,
getBoundingRectangleForInput(button),
@@ -316,16 +310,14 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
)
}
private fun MutableList<Pair<OverlayInput, Input>>.addJoystick(
joystick: OverlayJoystick,
@DrawableRes joystickInnerCircleDrawable: Int = R.drawable.stick_inner,
@DrawableRes joystickBackgroundDrawable: Int = R.drawable.stick_background,
) {
private fun MutableList<Pair<OverlayInput, Input>>.addRoundButton(
button: OverlayButton,
buttonText: String = button.name,
) = addRoundButton(button, TextButtonInnerDrawing(buttonText))
private fun MutableList<Pair<OverlayInput, Input>>.addJoystick(joystick: OverlayJoystick) {
add(
joystick to Joystick(
resources,
joystickBackgroundDrawable,
joystickInnerCircleDrawable,
{ x, y -> onJoystickStateChange(joystick, x, y) },
currentAlpha,
getBoundingRectangleForInput(joystick)
@@ -336,9 +328,6 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
private fun MutableList<Pair<OverlayInput, Input>>.addDpad() {
add(
OverlayDpad.DPAD_UP to DPadInput(
resources,
R.drawable.dpad_background,
R.drawable.dpad_button,
::onButtonStateChange,
currentAlpha,
getBoundingRectangleForInput(OverlayDpad.DPAD_UP)
@@ -348,12 +337,11 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
private fun MutableList<Pair<OverlayInput, Input>>.addRectangleButton(
button: OverlayButton,
@DrawableRes buttonDrawable: Int,
buttonText: String = button.name,
) {
add(
button to RectangleButton(
resources,
buttonDrawable,
TextButtonInnerDrawing(buttonText),
{ onButtonStateChange(button, it) },
currentAlpha,
getBoundingRectangleForInput(button)
@@ -372,85 +360,48 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
}
nativeControllerType = getControllerType(controllerIndex)
overlyButtonToNativeButton = when (nativeControllerType) {
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> ::overlayButtonToVPADButton
NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> ::overlayButtonToClassicButton
NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> ::overlayButtonToProButton
NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> ::overlayButtonToWiimoteButton
else -> { _ -> -1 }
}
onJoystickChange = when (nativeControllerType) {
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> ::onVPADJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> ::onProJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> ::onClassicJoystickStateChange
NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> ::onWiimoteJoystickStateChange
else -> { _, _, _, _, _ -> }
}
inputs = mutableListOf<Pair<OverlayInput, Input>>().apply {
addRoundButton(
OverlayButton.MINUS,
R.drawable.button_minus,
)
addRoundButton(
OverlayButton.PLUS,
R.drawable.button_plus,
)
addRoundButton(OverlayButton.MINUS, "-")
addRoundButton(OverlayButton.PLUS, "+")
addDpad()
addRoundButton(
OverlayButton.A,
R.drawable.button_a,
)
addRoundButton(
OverlayButton.B,
R.drawable.button_b,
)
addRoundButton(OverlayButton.A)
addRoundButton(OverlayButton.B)
addJoystick(OverlayJoystick.RIGHT)
if (nativeControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE) {
addRoundButton(
OverlayButton.X,
R.drawable.button_x,
)
addRoundButton(
OverlayButton.Y,
R.drawable.button_y,
)
addRectangleButton(
OverlayButton.ZL,
R.drawable.button_zl,
)
addRectangleButton(
OverlayButton.ZR,
R.drawable.button_zr,
)
addRectangleButton(
OverlayButton.L,
R.drawable.button_l,
)
addRectangleButton(
OverlayButton.R,
R.drawable.button_r,
)
addRoundButton(OverlayButton.X)
addRoundButton(OverlayButton.Y)
addRectangleButton(OverlayButton.ZL)
addRectangleButton(OverlayButton.ZR)
addRectangleButton(OverlayButton.L)
addRectangleButton(OverlayButton.R)
addJoystick(OverlayJoystick.LEFT)
}
if (nativeControllerType == NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE) {
addRoundButton(
OverlayButton.ONE,
R.drawable.button_one,
)
addRoundButton(
OverlayButton.TWO,
R.drawable.button_two,
)
addRoundButton(
OverlayButton.C,
R.drawable.button_c,
)
addRectangleButton(
OverlayButton.Z,
R.drawable.button_z,
)
addRoundButton(
OverlayButton.HOME,
R.drawable.button_home,
)
addRoundButton(OverlayButton.ONE, "1")
addRoundButton(OverlayButton.TWO, "2")
addRoundButton(OverlayButton.C)
addRectangleButton(OverlayButton.Z)
addRoundButton(OverlayButton.HOME, HomeButtonInnerDrawing())
}
if (nativeControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC
&& nativeControllerType != NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE
) {
addRoundButton(
OverlayButton.L_STICK_CLICK,
R.drawable.button_stick,
)
addRoundButton(
OverlayButton.R_STICK_CLICK,
R.drawable.button_stick,
)
addRoundButton(OverlayButton.L_STICK_CLICK, StickClickInnerDrawing())
addRoundButton(OverlayButton.R_STICK_CLICK, StickClickInnerDrawing())
}
}
}
@@ -472,90 +423,88 @@ class InputOverlaySurfaceView(context: Context, attrs: AttributeSet?) :
private fun onEditPosition(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val x = event.x.toInt()
val y = event.y.toInt()
for ((_, input) in inputs!!) {
if (input.isInside(x, y)) {
currentConfiguredInput = input
input.enableDrawingBoundingRect(
resources.getColor(
R.color.purple,
context.theme
)
)
return true
}
}
val configuredInput = currentConfiguredInput
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
if (configuredInput != null) {
return false
}
MotionEvent.ACTION_UP -> {
if (currentConfiguredInput != null) {
currentConfiguredInput!!.disableDrawingBoundingRect()
currentConfiguredInput = null
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (currentConfiguredInput != null) {
val x = event.x.toInt()
val y = event.y.toInt()
currentConfiguredInput!!.moveInput(x, y, width, height)
val x = event.x
val y = event.y
for ((_, input) in inputs!!) {
if (input.isInside(x, y)) {
currentConfiguredInput = input
input.enableDrawingBoundingRect(
resources.getColor(R.color.purple, context.theme)
)
return true
}
}
}
if (configuredInput == null) {
return false
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
configuredInput.disableDrawingBoundingRect()
currentConfiguredInput = null
return true
}
if (event.actionMasked == MotionEvent.ACTION_MOVE) {
val x = event.x.toInt()
val y = event.y.toInt()
configuredInput.moveInput(x, y, width, height)
return true
}
return false
}
private fun onEditSize(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
val x = event.x.toInt()
val y = event.y.toInt()
for ((_, input) in inputs!!) {
if (input.isInside(x, y)) {
currentConfiguredInput = input
input.enableDrawingBoundingRect(
resources.getColor(
R.color.red,
context.theme
)
)
return true
}
}
}
val configuredInput = currentConfiguredInput
MotionEvent.ACTION_UP -> {
if (currentConfiguredInput != null) {
currentConfiguredInput!!.disableDrawingBoundingRect()
currentConfiguredInput = null
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
val x = event.x
val y = event.y
for ((_, input) in inputs!!) {
if (input.isInside(x, y)) {
currentConfiguredInput = input
input.enableDrawingBoundingRect(
resources.getColor(R.color.red, context.theme)
)
return true
}
}
}
MotionEvent.ACTION_MOVE -> {
if (currentConfiguredInput != null) {
val histSize = event.historySize
if (event.historySize >= 2) {
val x1 = event.getHistoricalX(0)
val y1 = event.getHistoricalY(0)
val x2 = event.getHistoricalX(histSize - 1)
val y2 = event.getHistoricalY(histSize - 1)
currentConfiguredInput!!.resize(
(x2 - x1).toInt(),
(y2 - y1).toInt(),
width,
height,
inputsMinWidthHeight
)
}
return true
}
if (configuredInput == null) {
return false
}
if (event.actionMasked == MotionEvent.ACTION_UP) {
configuredInput.disableDrawingBoundingRect()
currentConfiguredInput = null
return true
}
if (event.actionMasked == MotionEvent.ACTION_MOVE) {
val histSize = event.historySize
if (event.historySize >= 2) {
val x1 = event.getHistoricalX(0)
val y1 = event.getHistoricalY(0)
val x2 = event.getHistoricalX(histSize - 1)
val y2 = event.getHistoricalY(histSize - 1)
configuredInput.resize(
(x2 - x1).toInt(),
(y2 - y1).toInt(),
width,
height,
inputsMinWidthHeight
)
}
return true
}
return false
}
@@ -1,144 +0,0 @@
package info.cemu.cemu.inputoverlay
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.MotionEvent
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import kotlin.math.min
import kotlin.math.sqrt
class Joystick(
resources: Resources,
@DrawableRes joystickBackgroundId: Int,
@DrawableRes innerStickId: Int,
private val onStickStateChange: (x: Float, y: Float) -> Unit,
private val alpha: Int,
boundingRectangle: Rect,
) : Input(boundingRectangle) {
private val joystickBackground: Drawable =
ResourcesCompat.getDrawable(resources, joystickBackgroundId, null)!!
private val joystickDrawable = InputDrawable(resources, innerStickId)
private var currentPointerId = -1
private var centerX = 0
private var centerY = 0
private var originalCenterX = 0
private var originalCenterY = 0
private var radius = 0
private var innerRadius = 0
init {
configure()
}
private fun updateState(pressed: Boolean, x: Float, y: Float) {
joystickDrawable.setActiveState(pressed)
onStickStateChange(x, y)
val newCentreX = (centerX + radius * x).toInt()
val newCentreY = (centerY + radius * y).toInt()
joystickDrawable.setBounds(
newCentreX - innerRadius,
newCentreY - innerRadius,
newCentreX + innerRadius,
newCentreY + innerRadius
)
}
override fun configure() {
centerX = rect.centerX()
originalCenterX = centerX
centerY = rect.centerY()
originalCenterY = centerY
radius = min(rect.width(), rect.height()) / 2
innerRadius = (radius * 0.65f).toInt()
joystickBackground.setBounds(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
joystickBackground.alpha = alpha
joystickDrawable.setAlpha(alpha)
joystickDrawable.setBounds(
centerX - innerRadius,
centerY - innerRadius,
centerX + innerRadius,
centerY + innerRadius
)
}
override fun onTouch(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
val pointerIndex = event.actionIndex
val x = event.getX(pointerIndex).toInt()
val y = event.getY(pointerIndex).toInt()
if (isInside(x, y)) {
centerX = x
centerY = y
joystickBackground.setBounds(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
currentPointerId = event.getPointerId(pointerIndex)
updateState(true, 0.0f, 0.0f)
return true
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
if (currentPointerId == event.getPointerId(event.actionIndex)) {
centerX = originalCenterX
centerY = originalCenterY
joystickBackground.setBounds(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
currentPointerId = -1
updateState(false, 0.0f, 0.0f)
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (currentPointerId == -1) {
return false
}
for (i in 0 until event.pointerCount) {
if (currentPointerId != event.getPointerId(i)) {
continue
}
var x: Float = (event.getX(i) - centerX) / radius
var y: Float = (event.getY(i) - centerY) / radius
val norm = sqrt(x * x + y * y)
if (norm > 1.0f) {
x /= norm
y /= norm
}
updateState(true, x, y)
return true
}
}
}
return false
}
override fun resetInput() {
updateState(false, 0f, 0f)
}
override fun drawInput(canvas: Canvas) {
joystickBackground.draw(canvas)
joystickDrawable.icon.draw(canvas)
}
override fun isInside(x: Int, y: Int): Boolean {
return ((x - originalCenterX) * (x - originalCenterX) + (y - originalCenterY) * (y - originalCenterY)) <= radius * radius
}
}
@@ -1,35 +0,0 @@
package info.cemu.cemu.inputoverlay
import android.content.res.Resources
import android.graphics.Rect
import androidx.annotation.DrawableRes
class RectangleButton(
resources: Resources,
@DrawableRes buttonId: Int,
onButtonStateChange: (state: Boolean) -> Unit,
private val alpha: Int,
rect: Rect,
) : Button(resources, buttonId, onButtonStateChange, rect) {
var left: Int = 0
var top: Int = 0
var right: Int = 0
var bottom: Int = 0
init {
configure()
}
override fun configure() {
this.left = rect.left
this.top = rect.top
this.right = rect.right
this.bottom = rect.bottom
inputDrawable.setAlpha(alpha)
inputDrawable.setBounds(left, top, right, bottom)
}
override fun isInside(x: Int, y: Int): Boolean {
return x in left..<right && y in top..<bottom
}
}
@@ -1,40 +0,0 @@
package info.cemu.cemu.inputoverlay
import android.content.res.Resources
import android.graphics.Rect
import androidx.annotation.DrawableRes
import kotlin.math.min
class RoundButton constructor(
resources: Resources,
@DrawableRes buttonId: Int,
onButtonStateChange: (state: Boolean) -> Unit,
private val alpha: Int,
rect: Rect,
) : Button(resources, buttonId, onButtonStateChange, rect) {
private var centreX: Int = 0
private var centreY: Int = 0
private var radius2: Int = 0
init {
configure()
}
override fun configure() {
centreX = rect.centerX()
centreY = rect.centerY()
val radius = min(rect.width(), rect.height()) / 2
inputDrawable.setBounds(
centreX - radius,
centreY - radius,
centreX + radius,
centreY + radius
)
inputDrawable.setAlpha(alpha)
radius2 = radius * radius
}
override fun isInside(x: Int, y: Int): Boolean {
return ((x - centreX) * (x - centreX) + (y - centreY) * (y - centreY)) <= radius2
}
}
@@ -1,29 +1,21 @@
package info.cemu.cemu.inputoverlay
package info.cemu.cemu.inputoverlay.inputs
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
import android.view.MotionEvent
import androidx.annotation.DrawableRes
abstract class Button(
resources: Resources,
@DrawableRes buttonId: Int,
private val onButtonStateChange: (state: Boolean) -> Unit,
boundingRect: Rect,
) : Input(boundingRect) {
protected val inputDrawable = InputDrawable(resources, buttonId)
protected var state = false
private var currentPointerId: Int = -1
override fun onTouch(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
val pointerIndex = event.actionIndex
val x = event.getX(pointerIndex).toInt()
val y = event.getY(pointerIndex).toInt()
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
if (isInside(x, y)) {
currentPointerId = event.getPointerId(pointerIndex)
updateState(true)
@@ -42,16 +34,12 @@ abstract class Button(
return false
}
override fun drawInput(canvas: Canvas) {
inputDrawable.icon.draw(canvas)
}
override fun resetInput() {
updateState(false)
}
private fun updateState(state: Boolean) {
inputDrawable.setActiveState(state)
this.state = state
onButtonStateChange(state)
}
}
@@ -1,74 +1,72 @@
package info.cemu.cemu.inputoverlay
package info.cemu.cemu.inputoverlay.inputs
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.graphics.RectF
import android.view.MotionEvent
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import info.cemu.cemu.inputoverlay.OverlayDpad
import info.cemu.cemu.utils.fillCircleWithStroke
import info.cemu.cemu.utils.fillRoundRectangleWithStroke
import kotlin.math.atan2
import kotlin.math.min
class DPadInput(
resources: Resources,
@DrawableRes backgroundId: Int,
@DrawableRes buttonId: Int,
private val onButtonStateChange: (button: OverlayDpad, state: Boolean) -> Unit,
private val alpha: Int,
rect: Rect,
) : Input(rect) {
private val dpadUpDrawable = InputDrawable(resources, buttonId)
private val dpadDownDrawable = InputDrawable(resources, buttonId)
private val dpadLeftDrawable = InputDrawable(resources, buttonId)
private val dpadRightDrawable = InputDrawable(resources, buttonId)
private val background = ResourcesCompat.getDrawable(resources, backgroundId, null)!!
private var backgroundFillColor: Int = 0
private var backgroundStrokeColor: Int = 0
private val dpadUpRect = RectF()
private val dpadDownRect = RectF()
private val dpadLeftRect = RectF()
private val dpadRightRect = RectF()
private var dpadState: Int = NONE
private var centerX: Int = 0
private var centerY: Int = 0
private var radius2: Int = 0
private var centerX: Float = 0f
private var centerY: Float = 0f
private var radius2: Float = 0f
private var radius: Float = 0f
private var currentPointerId: Int = -1
override fun configure() {
this.centerX = rect.centerX()
this.centerY = rect.centerY()
val radius = min(rect.width(), rect.height()) / 2
radius2 = radius * radius
background.alpha = alpha
background.setBounds(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
backgroundFillColor = Color.argb(alpha, 128, 128, 128)
backgroundStrokeColor = Color.argb(alpha, 200, 200, 200)
configureColors(alpha)
centerX = rect.exactCenterX()
centerY = rect.exactCenterY()
radius = min(rect.width(), rect.height()) * 0.5f
radius2 = radius * radius
val buttonSize = radius / 2
val configureButton = { circleXPos: Int, circleYPos: Int, inputDrawable: InputDrawable ->
inputDrawable.setAlpha(alpha)
val left = circleXPos - buttonSize / 2
val top = circleYPos - buttonSize / 2
inputDrawable.setBounds(left, top, left + buttonSize, top + buttonSize)
val configureButtonRect = { circleXPos: Float, circleYPos: Float, rect: RectF ->
val left = circleXPos - buttonSize * 0.5f
val top = circleYPos - buttonSize * 0.5f
rect.set(left, top, left + buttonSize, top + buttonSize)
}
configureButton(
val buttonCenterXYTranslate = 2f * radius / 3f
configureButtonRect(
centerX,
centerY - 2 * radius / 3,
dpadUpDrawable
centerY - buttonCenterXYTranslate,
dpadUpRect
)
configureButton(
configureButtonRect(
centerX,
centerY + 2 * radius / 3,
dpadDownDrawable
centerY + buttonCenterXYTranslate,
dpadDownRect
)
configureButton(
centerX - 2 * radius / 3,
configureButtonRect(
centerX - buttonCenterXYTranslate,
centerY,
dpadLeftDrawable
dpadLeftRect
)
configureButton(
centerX + 2 * radius / 3,
configureButtonRect(
centerX + buttonCenterXYTranslate,
centerY,
dpadRightDrawable
dpadRightRect
)
}
@@ -87,23 +85,19 @@ class DPadInput(
if (dpadState == NONE) return
if ((dpadState and UP) != 0) {
onButtonStateChange(OverlayDpad.DPAD_UP, pressed)
dpadUpDrawable.setActiveState(pressed)
}
if ((dpadState and DOWN) != 0) {
onButtonStateChange(OverlayDpad.DPAD_DOWN, pressed)
dpadDownDrawable.setActiveState(pressed)
}
if ((dpadState and LEFT) != 0) {
onButtonStateChange(OverlayDpad.DPAD_LEFT, pressed)
dpadLeftDrawable.setActiveState(pressed)
}
if ((dpadState and RIGHT) != 0) {
onButtonStateChange(OverlayDpad.DPAD_RIGHT, pressed)
dpadRightDrawable.setActiveState(pressed)
}
}
private fun getStateByPosition(x: Int, y: Int): Int {
private fun getStateByPosition(x: Float, y: Float): Int {
val norm2 = (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)
if (norm2 <= radius2 * 0.1f) {
return NONE
@@ -128,8 +122,8 @@ class DPadInput(
return false
}
val pointerIndex = event.actionIndex
val x = event.getX(pointerIndex).toInt()
val y = event.getY(pointerIndex).toInt()
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
val pointerId = event.getPointerId(pointerIndex)
if (isInside(x, y)) {
currentPointerId = pointerId
@@ -155,8 +149,8 @@ class DPadInput(
if (currentPointerId != event.getPointerId(i)) {
continue
}
val x = event.getX(i).toInt()
val y = event.getY(i).toInt()
val x = event.getX(i)
val y = event.getY(i)
updateState(getStateByPosition(x, y))
return true
}
@@ -169,19 +163,40 @@ class DPadInput(
updateState(NONE)
}
override fun isInside(x: Int, y: Int): Boolean {
override fun isInside(x: Float, y: Float): Boolean {
return (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY) <= radius2
}
override fun drawInput(canvas: Canvas) {
background.draw(canvas)
dpadUpDrawable.icon.draw(canvas)
dpadDownDrawable.icon.draw(canvas)
dpadLeftDrawable.icon.draw(canvas)
dpadRightDrawable.icon.draw(canvas)
canvas.fillCircleWithStroke(
centerX,
centerY,
radius,
paint,
backgroundFillColor,
backgroundStrokeColor
)
drawButton(canvas, dpadUpRect, dpadState and UP)
drawButton(canvas, dpadDownRect, dpadState and DOWN)
drawButton(canvas, dpadLeftRect, dpadState and LEFT)
drawButton(canvas, dpadRightRect, dpadState and RIGHT)
}
private fun drawButton(canvas: Canvas, rect: RectF, state: Int) {
val fillColor: Int
val strokeColor: Int
if (state != 0) {
fillColor = activeFillColor
strokeColor = activeStrokeColor
} else {
fillColor = inactiveFillColor
strokeColor = inactiveStrokeColor
}
canvas.fillRoundRectangleWithStroke(rect, BUTTON_RADIUS, paint, fillColor, strokeColor)
}
companion object {
private const val BUTTON_RADIUS = 5f
private const val NONE = 0
private const val UP = 1 shl 0
private const val DOWN = 1 shl 1
@@ -1,6 +1,7 @@
package info.cemu.cemu.inputoverlay
package info.cemu.cemu.inputoverlay.inputs
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.view.MotionEvent
@@ -13,6 +14,13 @@ abstract class Input protected constructor(
) {
private var drawBoundingRectangle = false
private val boundingRectanglePaint = Paint()
protected var activeFillColor = 0
protected var activeStrokeColor = 0
protected var inactiveFillColor = 0
protected var inactiveStrokeColor = 0
protected val paint = Paint().apply {
strokeWidth = 3f
}
fun getBoundingRectangle() = rect
@@ -65,6 +73,13 @@ abstract class Input protected constructor(
protected abstract fun configure()
protected fun configureColors(alpha: Int) {
activeFillColor = Color.argb(alpha, 255, 255, 255)
activeStrokeColor = Color.argb(alpha, 0, 0, 0)
inactiveFillColor = activeStrokeColor
inactiveStrokeColor = activeFillColor
}
protected abstract fun drawInput(canvas: Canvas)
fun draw(canvas: Canvas) {
@@ -83,5 +98,5 @@ abstract class Input protected constructor(
drawBoundingRectangle = false
}
abstract fun isInside(x: Int, y: Int): Boolean
abstract fun isInside(x: Float, y: Float): Boolean
}
@@ -0,0 +1,134 @@
package info.cemu.cemu.inputoverlay.inputs
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.view.MotionEvent
import info.cemu.cemu.utils.fillCircleWithStroke
import kotlin.math.min
import kotlin.math.sqrt
class Joystick(
private val onStickStateChange: (x: Float, y: Float) -> Unit,
private val alpha: Int,
boundingRectangle: Rect,
) : Input(boundingRectangle) {
private var backgroundFillColor: Int = 0
private var currentPointerId = -1
private var pressed = false
private var centerX: Float = 0f
private var centerY: Float = 0f
private var originalCenterX: Float = 0f
private var originalCenterY: Float = 0f
private var radius: Float = 0f
private var innerRadius: Float = 0f
private var innerCenterX: Float = 0f
private var innerCenterY: Float = 0f
init {
configure()
}
private fun updateState(pressed: Boolean, x: Float, y: Float) {
this.pressed = pressed
onStickStateChange(x, y)
innerCenterX = centerX + radius * x
innerCenterY = centerY + radius * y
}
override fun configure() {
backgroundFillColor = Color.argb(alpha, 128, 128, 128)
configureColors(alpha)
centerX = rect.exactCenterX()
originalCenterX = centerX
innerCenterX = centerX
centerY = rect.exactCenterY()
originalCenterY = centerY
innerCenterY = centerY
radius = min(rect.width(), rect.height()) * 0.5f
innerRadius = radius * 0.65f
}
override fun onTouch(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
val pointerIndex = event.actionIndex
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
if (isInside(x, y)) {
centerX = x
centerY = y
currentPointerId = event.getPointerId(pointerIndex)
updateState(true, 0.0f, 0.0f)
return true
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
if (currentPointerId == event.getPointerId(event.actionIndex)) {
centerX = originalCenterX
centerY = originalCenterY
currentPointerId = -1
updateState(false, 0.0f, 0.0f)
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (currentPointerId == -1) {
return false
}
for (i in 0 until event.pointerCount) {
if (currentPointerId != event.getPointerId(i)) {
continue
}
var x: Float = (event.getX(i) - centerX) / radius
var y: Float = (event.getY(i) - centerY) / radius
val norm = sqrt(x * x + y * y)
if (norm > 1.0f) {
x /= norm
y /= norm
}
updateState(true, x, y)
return true
}
}
}
return false
}
override fun resetInput() {
updateState(false, 0f, 0f)
}
override fun drawInput(canvas: Canvas) {
paint.color = backgroundFillColor
paint.style = Paint.Style.FILL
canvas.drawCircle(centerX, centerY, radius, paint)
val fillColor: Int
val strokeColor: Int
if (pressed) {
fillColor = activeFillColor
strokeColor = activeStrokeColor
} else {
fillColor = inactiveFillColor
strokeColor = inactiveStrokeColor
}
canvas.fillCircleWithStroke(
innerCenterX,
innerCenterY,
innerRadius,
paint,
fillColor,
strokeColor
)
}
override fun isInside(x: Float, y: Float): Boolean {
return (x - originalCenterX) * (x - originalCenterX) + (y - originalCenterY) * (y - originalCenterY) <= radius * radius
}
}
@@ -0,0 +1,62 @@
package info.cemu.cemu.inputoverlay.inputs
import android.graphics.Canvas
import android.graphics.Rect
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.ButtonInnerDrawing
import info.cemu.cemu.utils.fillRoundRectangleWithStroke
class RectangleButton(
private val innerDrawing: ButtonInnerDrawing,
onButtonStateChange: (state: Boolean) -> Unit,
private val alpha: Int,
rect: Rect,
) : Button(onButtonStateChange, rect) {
var left: Float = 0f
var top: Float = 0f
var right: Float = 0f
var bottom: Float = 0f
init {
configure()
}
override fun configure() {
this.left = rect.left.toFloat()
this.top = rect.top.toFloat()
this.right = rect.right.toFloat()
this.bottom = rect.bottom.toFloat()
configureColors(alpha)
innerDrawing.configure(rect, alpha)
}
override fun drawInput(canvas: Canvas) {
val fillColor: Int
val strokeColor: Int
if (state) {
fillColor = activeFillColor
strokeColor = activeStrokeColor
} else {
fillColor = inactiveFillColor
strokeColor = inactiveStrokeColor
}
canvas.fillRoundRectangleWithStroke(
left,
top,
right,
bottom,
RECTANGLE_RADIUS,
paint,
fillColor,
strokeColor
)
innerDrawing.draw(canvas, state)
}
override fun isInside(x: Float, y: Float): Boolean {
return x in left..<right && y in top..<bottom
}
companion object {
const val RECTANGLE_RADIUS = 5f
}
}
@@ -0,0 +1,51 @@
package info.cemu.cemu.inputoverlay.inputs
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import info.cemu.cemu.inputoverlay.inputs.innerdrawing.ButtonInnerDrawing
import info.cemu.cemu.utils.fillCircleWithStroke
import kotlin.math.min
class RoundButton(
private val innerDrawing: ButtonInnerDrawing,
onButtonStateChange: (state: Boolean) -> Unit,
private val alpha: Int,
rect: Rect,
) : Button(onButtonStateChange, rect) {
private var centerX: Float = 0f
private var centerY: Float = 0f
private var radius2: Float = 0f
private var radius: Float = 0f
init {
configure()
}
override fun configure() {
centerX = rect.centerX().toFloat()
centerY = rect.centerY().toFloat()
radius = min(rect.width().toFloat(), rect.height().toFloat()) / 2f
radius2 = radius * radius
innerDrawing.configure(rect, alpha)
configureColors(alpha)
}
override fun drawInput(canvas: Canvas) {
val fillColor: Int
val strokeColor: Int
if (state) {
fillColor = activeFillColor
strokeColor = activeStrokeColor
} else {
fillColor = inactiveFillColor
strokeColor = inactiveStrokeColor
}
canvas.fillCircleWithStroke(centerX, centerY, radius, paint, fillColor, strokeColor)
innerDrawing.draw(canvas, state)
}
override fun isInside(x: Float, y: Float): Boolean {
return (x - centerX) * (x - centerX) + (y - centerY) * (y - centerY) <= radius2
}
}
@@ -0,0 +1,9 @@
package info.cemu.cemu.inputoverlay.inputs.innerdrawing
import android.graphics.Canvas
import android.graphics.Rect
interface ButtonInnerDrawing {
fun draw(canvas: Canvas, state: Boolean)
fun configure(boundingRect: Rect, alpha: Int)
}
@@ -0,0 +1,55 @@
package info.cemu.cemu.inputoverlay.inputs.innerdrawing
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import kotlin.math.min
class HomeButtonInnerDrawing : ButtonInnerDrawing {
private var activeFillColor = 0
private var inactiveFillColor = 0
private val paint = Paint()
private var path = Path()
override fun draw(canvas: Canvas, state: Boolean) {
paint.color = if (state) activeFillColor else inactiveFillColor
canvas.drawPath(path, paint)
}
override fun configure(boundingRect: Rect, alpha: Int) {
activeFillColor = Color.argb(alpha, 0, 0, 0)
inactiveFillColor = Color.argb(alpha, 255, 255, 255)
path = Path()
val firstPoint = IconPathPoints[0]
path.moveTo(firstPoint.first, firstPoint.second)
for (i in 1..<IconPathPoints.size) {
val point = IconPathPoints[i]
path.lineTo(point.first, point.second)
}
val transformMatrix = Matrix()
val rectSize = min(boundingRect.width(), boundingRect.height()).toFloat()
val scale = rectSize / CANVAS_SIZE
transformMatrix.setScale(scale, scale)
transformMatrix.postTranslate(
boundingRect.exactCenterX() - rectSize * 0.5f,
boundingRect.exactCenterY() - rectSize * 0.5f
)
path.transform(transformMatrix)
}
companion object {
const val CANVAS_SIZE = 24f
val IconPathPoints = listOf(
6.735f to 19f,
6.735f to 11.85f,
4.675f to 11.85f,
12.175f to 5.15f,
19.675f to 11.85f,
17.615f to 11.85f,
17.615f to 19f,
)
}
}
@@ -0,0 +1,59 @@
package info.cemu.cemu.inputoverlay.inputs.innerdrawing
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import kotlin.math.min
class StickClickInnerDrawing : ButtonInnerDrawing {
private var activeFillColor = 0
private var inactiveFillColor = 0
private val paint = Paint().apply {
strokeWidth = 5f
style = Paint.Style.STROKE
}
private var path = Path()
override fun draw(canvas: Canvas, state: Boolean) {
paint.color = if (state) activeFillColor else inactiveFillColor
canvas.drawPath(path, paint)
}
override fun configure(boundingRect: Rect, alpha: Int) {
activeFillColor = Color.argb(alpha, 0, 0, 0)
inactiveFillColor = Color.argb(alpha, 255, 255, 255)
val transformMatrix = Matrix()
path = createArrowPath()
transformMatrix.setTranslate(-0.15f, 0f)
path.transform(transformMatrix)
transformMatrix.reset()
val arrowPath = createArrowPath()
transformMatrix.setRotate(180f)
transformMatrix.postTranslate(0.15f, 0f)
arrowPath.transform(transformMatrix)
path.addPath(arrowPath)
transformMatrix.reset()
val scale = min(boundingRect.width(), boundingRect.height()) * 0.5f
transformMatrix.setScale(scale, scale)
transformMatrix.postTranslate(
boundingRect.exactCenterX(),
boundingRect.exactCenterY()
)
path.transform(transformMatrix)
}
private fun createArrowPath(): Path {
val arrowPath = Path()
arrowPath.moveTo(-0.5f, 0.5f)
arrowPath.lineTo(0f, 0f)
arrowPath.lineTo(-0.5f, -0.5f)
return arrowPath
}
}
@@ -0,0 +1,41 @@
package info.cemu.cemu.inputoverlay.inputs.innerdrawing
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.text.StaticLayout
import android.text.TextPaint
import androidx.core.graphics.withTranslation
import kotlin.math.min
class TextButtonInnerDrawing(private val text: String) : ButtonInnerDrawing {
private val textPaint = TextPaint()
private var staticLayout = createStaticLayout()
private var textXCoordinate = 0f
private var textYCoordinate = 0f
private var activeFillColor = 0
private var inactiveFillColor = 0
override fun draw(canvas: Canvas, state: Boolean) {
textPaint.color = if (state) activeFillColor else inactiveFillColor
canvas.withTranslation(textXCoordinate, textYCoordinate) {
staticLayout.draw(canvas)
}
}
override fun configure(boundingRect: Rect, alpha: Int) {
textPaint.textSize = min(boundingRect.width(), boundingRect.height()) * 0.75f
activeFillColor = Color.argb(alpha, 0, 0, 0)
inactiveFillColor = Color.argb(alpha, 255, 255, 255)
staticLayout = createStaticLayout()
textXCoordinate = boundingRect.exactCenterX() - staticLayout.width * 0.5f
textYCoordinate = boundingRect.exactCenterY() - staticLayout.height * 0.5f
}
private fun createStaticLayout(): StaticLayout {
val textWidth = textPaint.measureText(text).toInt()
return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, textWidth)
.setIncludePad(false)
.build()
}
}
@@ -0,0 +1,55 @@
package info.cemu.cemu.utils
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
fun Canvas.fillCircleWithStroke(
centerX: Float,
centerY: Float,
radius: Float,
paint: Paint,
fillColor: Int,
strokeColor: Int,
) {
paint.color = fillColor
paint.style = Paint.Style.FILL
drawCircle(centerX, centerY, radius, paint)
paint.color = strokeColor
paint.style = Paint.Style.STROKE
drawCircle(centerX, centerY, radius, paint)
}
fun Canvas.fillRoundRectangleWithStroke(
left: Float,
top: Float,
right: Float,
bottom: Float,
radius: Float,
paint: Paint,
fillColor: Int,
strokeColor: Int,
) {
fillRoundRectangleWithStroke(
RectF(left, top, right, bottom),
radius,
paint,
fillColor,
strokeColor,
)
}
fun Canvas.fillRoundRectangleWithStroke(
rect: RectF,
radius: Float,
paint: Paint,
fillColor: Int,
strokeColor: Int,
) {
paint.style = Paint.Style.FILL
paint.color = fillColor
drawRoundRect(rect, radius, radius, paint)
paint.style = Paint.Style.STROKE
paint.color = strokeColor
drawRoundRect(rect, radius, radius, paint)
}
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="95.036dp"
android:height="95.036dp"
android:viewportWidth="95.036"
android:viewportHeight="95.036">
<path
android:pathData="M47.518,47.518m-47.268,0a47.268,47.268 0,1 1,94.536 0a47.268,47.268 0,1 1,-94.536 0"
android:strokeWidth="0.5"
android:fillColor="#000000"
android:strokeColor="#ffffff"
android:fillType="evenOdd" />
<path
android:pathData="m27.626,70.075l5.656,0l14.757,-41.344l-0.585,0l13.911,41.344l6.046,0l-15.862,-45.115l-7.021,0zM36.142,56.684l22.557,0l0,-4.68l-22.557,0z"
android:strokeWidth="0.5"
android:fillColor="#ffffff" />
</vector>
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="95.036dp"
android:height="95.036dp"
android:viewportWidth="95.036"
android:viewportHeight="95.036">
<path
android:pathData="M47.518,47.518m-47.268,0a47.268,47.268 0,1 1,94.536 0a47.268,47.268 0,1 1,-94.536 0"
android:strokeWidth="0.5"
android:fillColor="#000000"
android:strokeColor="#ffffff"
android:fillType="evenOdd" />
<path
android:pathData="m31.396,70.075l15.732,0c10.401,0 16.512,-4.875 16.512,-13.131 0,-7.606 -5.721,-12.611 -14.366,-12.611l-14.692,0l0,4.485l14.106,0c5.721,0 9.426,3.64 9.426,9.231 0,5.201 -3.12,7.411 -10.336,7.411l-10.986,0l0,-35.884l7.671,0c6.891,0 10.271,2.535 10.271,7.671 0,3.965 -2.21,6.241 -8.191,8.451l7.801,0c3.575,-1.56 5.916,-5.526 5.916,-9.881 0,-6.891 -5.526,-10.856 -15.147,-10.856l-13.716,0z"
android:strokeWidth="0.5"
android:fillColor="#ffffff" />
</vector>
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="95.036dp"
android:height="95.036dp"
android:viewportWidth="95.036"
android:viewportHeight="95.036">
<path
android:pathData="M47.518,47.518m-47.268,0a47.268,47.268 0,1 1,94.536 0a47.268,47.268 0,1 1,-94.536 0"
android:strokeWidth="0.5"
android:fillColor="#000000"
android:strokeColor="#ffffff"
android:fillType="evenOdd" />
<path
android:pathData="m49.875,70.693c4.355,0 8.906,-0.975 13.196,-2.86l-1.235,-3.965c-3.25,1.3 -7.281,2.08 -10.726,2.08 -10.791,0 -17.942,-7.671 -17.942,-19.177 0,-10.986 6.566,-17.682 17.422,-17.682 3.64,0 7.736,0.78 11.181,2.08l1.56,-4.03c-3.315,-1.755 -7.996,-2.795 -12.416,-2.795 -13.976,0 -23.272,9.491 -23.272,23.662 0,13.586 8.906,22.687 22.232,22.687z"
android:strokeWidth="0.5"
android:fillColor="#ffffff" />
</vector>
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="95.036dp"
android:height="95.036dp"
android:viewportWidth="95.036"
android:viewportHeight="95.036">
<path
android:pathData="M47.518,47.518m-47.268,0a47.268,47.268 0,1 1,94.536 0a47.268,47.268 0,1 1,-94.536 0"
android:strokeWidth="0.5"
android:fillColor="#000000"
android:strokeColor="#ffffff"
android:fillType="evenOdd" />
<path
android:pathData="M41.874,68.172L41.874,54.079h11.275v14.094c0,1.55 1.268,2.819 2.819,2.819h8.456c1.55,0 2.819,-1.268 2.819,-2.819v-19.731h4.792c1.297,0 1.917,-1.607 0.93,-2.452L49.4,24.764c-1.071,-0.958 -2.706,-0.958 -3.777,0L22.059,45.989c-0.958,0.846 -0.366,2.452 0.93,2.452h4.792v19.731c0,1.55 1.268,2.819 2.819,2.819h8.456c1.55,0 2.819,-1.268 2.819,-2.819z"
android:strokeWidth="2.81871"
android:fillColor="#ffffff" />
</vector>

Some files were not shown because too many files have changed in this diff Show More