diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt new file mode 100644 index 0000000..5767532 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt @@ -0,0 +1,136 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.metallic.chiaki.R +import kotlin.math.abs + +class AnalogStickView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) +{ + val radius: Float + private val handleRadius: Float + private val drawableBase: Drawable? + private val drawableHandle: Drawable? + + var state = Vector(0f, 0f) + private set(value) + { + field = value + stateChangedCallback?.let { it(field) } + } + + var stateChangedCallback: ((Vector) -> Unit)? = null + + private val touchTracker = TouchTracker().also { + it.positionChangedCallback = this::updateState + } + + private var center: Vector? = null + + /** + * Same as state, but scaled to the circle + */ + private var handlePosition: Vector = Vector(0f, 0f) + + private val clipBoundsTmp = Rect() + + init + { + context.theme.obtainStyledAttributes(attrs, R.styleable.AnalogStickView, 0, 0).apply { + radius = getDimension(R.styleable.AnalogStickView_radius, 0f) + handleRadius = getDimension(R.styleable.AnalogStickView_handleRadius, 0f) + drawableBase = getDrawable(R.styleable.AnalogStickView_drawableBase) + drawableHandle = getDrawable(R.styleable.AnalogStickView_drawableHandle) + recycle() + } + } + + override fun onDraw(canvas: Canvas) + { + super.onDraw(canvas) + + val center = center + if(center != null) + { + drawableBase?.setBounds((center.x - radius).toInt(), (center.y - radius).toInt(), (center.x + radius).toInt(), (center.y + radius).toInt()) + drawableBase?.draw(canvas) + + val handleX = center.x + handlePosition.x * radius + val handleY = center.y + handlePosition.y * radius + drawableHandle?.setBounds((handleX - handleRadius).toInt(), (handleY - handleRadius).toInt(), (handleX + handleRadius).toInt(),(handleY + handleRadius).toInt()) + drawableHandle?.draw(canvas) + } + } + + private fun updateState(position: Vector?) + { + if(radius <= 0f) + return + + if(position == null) + { + center = null + state = Vector(0f, 0f) + handlePosition = Vector(0f, 0f) + invalidate() + return + } + + val center: Vector = this.center ?: position + this.center = center + + val dir = position - center + val length = dir.length + if(length > 0) + { + val strength = if(length > radius) 1.0f else length / radius + val dirNormalized = dir / length + handlePosition = dirNormalized * strength + val dirBoxNormalized = + if(abs(dirNormalized.x) > abs(dirNormalized.y)) + dirNormalized / abs(dirNormalized.x) + else + dirNormalized / abs(dirNormalized.y) + state = dirBoxNormalized * strength + } + else + { + handlePosition = Vector(0f, 0f) + state = Vector(0f, 0f) + } + + invalidate() + } + + override fun onTouchEvent(event: MotionEvent): Boolean + { + touchTracker.touchEvent(event) + return true + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index a997b79..d02b5c7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -20,13 +20,11 @@ package com.metallic.chiaki.touchcontrols import android.content.Context import android.graphics.Canvas import android.util.AttributeSet -import android.util.Log import android.view.MotionEvent import android.view.View import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import com.metallic.chiaki.R import kotlin.math.abs -import kotlin.math.sqrt class DPadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -48,7 +46,9 @@ class DPadView @JvmOverloads constructor( private val dpadIdleDrawable = VectorDrawableCompat.create(resources, R.drawable.control_dpad_idle, null) private val dpadLeftDrawable = VectorDrawableCompat.create(resources, R.drawable.control_dpad_left, null) - private var pointerId: Int? = null + private val touchTracker = TouchTracker().also { + it.positionChangedCallback = this::updateState + } override fun onDraw(canvas: Canvas) { @@ -75,10 +75,10 @@ class DPadView @JvmOverloads constructor( drawable?.draw(canvas) } - private fun directionForPosition(x: Float, y: Float): Direction + private fun directionForPosition(position: Vector): Direction { - val dx = x - width * 0.5f - val dy = y - height * 0.5f + val dx = position.x - width * 0.5f + val dy = position.y - height * 0.5f return when { dx > abs(dy) -> Direction.RIGHT @@ -88,20 +88,20 @@ class DPadView @JvmOverloads constructor( } } - private fun updateState(x: Float?, y: Float?) + private fun updateState(position: Vector?) { val newState = - if(x == null || y == null) + if(position == null) null else { - val xFrac = 2.0f * (x / width.toFloat() - 0.5f) - val yFrac = 2.0f * (y / height.toFloat() - 0.5f) + val xFrac = 2.0f * (position.x / width.toFloat() - 0.5f) + val yFrac = 2.0f * (position.y / height.toFloat() - 0.5f) val radiusSq = xFrac * xFrac + yFrac * yFrac if(radiusSq < deadzoneRadius * deadzoneRadius && state != null) state else - directionForPosition(x, y) + directionForPosition(position) } if(state != newState) @@ -114,37 +114,7 @@ class DPadView @JvmOverloads constructor( override fun onTouchEvent(event: MotionEvent): Boolean { - when(event.actionMasked) - { - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> - { - if(pointerId == null) - { - pointerId = event.getPointerId(event.actionIndex) - updateState(event.x, event.y) - } - } - - MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> - { - if(event.getPointerId(event.actionIndex) == pointerId) - { - pointerId = null - updateState(null, null) - } - } - - MotionEvent.ACTION_MOVE -> - { - val pointerId = pointerId - if(pointerId != null) - { - val pointerIndex = event.findPointerIndex(pointerId) - if(pointerIndex >= 0) - updateState(event.getX(pointerIndex), event.getY(pointerIndex)) - } - } - } + touchTracker.touchEvent(event) return true } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 5704658..a4794d3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -18,6 +18,7 @@ package com.metallic.chiaki.touchcontrols import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -61,30 +62,18 @@ class TouchControlsFragment : Fragment() l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } - leftDpadView.stateChangeCallback = { controllerState = controllerState.copy().apply { - val pos: Pair = when(it) - { - DPadView.Direction.UP -> Pair(0, Short.MIN_VALUE) - DPadView.Direction.DOWN -> Pair(0, Short.MAX_VALUE) - DPadView.Direction.LEFT -> Pair(Short.MIN_VALUE, 0) - DPadView.Direction.RIGHT -> Pair(Short.MAX_VALUE, 0) - null -> Pair(0, 0) - } - leftX = pos.first - leftY = pos.second + val quantizeStick = { f: Float -> + (Short.MAX_VALUE * f).toShort() + } + + leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + leftX = quantizeStick(it.x) + leftY = quantizeStick(it.y) }} - rightDpadView.stateChangeCallback = { controllerState = controllerState.copy().apply { - val pos: Pair = when(it) - { - DPadView.Direction.UP -> Pair(0, Short.MIN_VALUE) - DPadView.Direction.DOWN -> Pair(0, Short.MAX_VALUE) - DPadView.Direction.LEFT -> Pair(Short.MIN_VALUE, 0) - DPadView.Direction.RIGHT -> Pair(Short.MAX_VALUE, 0) - null -> Pair(0, 0) - } - rightX = pos.first - rightY = pos.second + rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + rightX = quantizeStick(it.x) + rightY = quantizeStick(it.y) }} } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt new file mode 100644 index 0000000..0d82e06 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -0,0 +1,69 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +package com.metallic.chiaki.touchcontrols + +import android.view.MotionEvent + +class TouchTracker +{ + var currentPosition: Vector? = null + private set(value) + { + field = value + positionChangedCallback?.let { it(field) } + } + + var positionChangedCallback: ((Vector?) -> Unit)? = null + + private var pointerId: Int? = null + + fun touchEvent(event: MotionEvent) + { + when(event.actionMasked) + { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> + { + if(pointerId == null) + { + pointerId = event.getPointerId(event.actionIndex) + currentPosition = Vector(event.x, event.y) + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> + { + if(event.getPointerId(event.actionIndex) == pointerId) + { + pointerId = null + currentPosition = null + } + } + + MotionEvent.ACTION_MOVE -> + { + val pointerId = pointerId + if(pointerId != null) + { + val pointerIndex = event.findPointerIndex(pointerId) + if(pointerIndex >= 0) + currentPosition = Vector(event.getX(pointerIndex), event.getY(pointerIndex)) + } + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt new file mode 100644 index 0000000..96c236b --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -0,0 +1,31 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +package com.metallic.chiaki.touchcontrols + +import kotlin.math.sqrt + +data class Vector(val x: Float, val y: Float) +{ + operator fun plus(o: Vector) = Vector(x + o.x, y + o.y) + operator fun minus(o: Vector) = Vector(x - o.x, y - o.y) + operator fun times(s: Float) = Vector(x * s, y * s) + operator fun div(s: Float) = this * (1f / s) + + val lengthSq get() = x*x + y*y + val length get() = sqrt(lengthSq) +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/control_analog_stick_base.xml b/android/app/src/main/res/drawable/control_analog_stick_base.xml new file mode 100644 index 0000000..bbe2070 --- /dev/null +++ b/android/app/src/main/res/drawable/control_analog_stick_base.xml @@ -0,0 +1,15 @@ + + + diff --git a/android/app/src/main/res/drawable/control_analog_stick_handle.xml b/android/app/src/main/res/drawable/control_analog_stick_handle.xml new file mode 100644 index 0000000..109df6a --- /dev/null +++ b/android/app/src/main/res/drawable/control_analog_stick_handle.xml @@ -0,0 +1,15 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index ff89894..7cf6d9c 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -2,12 +2,48 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:clipChildren="false"> + + + + + + - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml index 8ef2b4d..b0a1170 100644 --- a/android/app/src/main/res/values/attrs.xml +++ b/android/app/src/main/res/values/attrs.xml @@ -4,4 +4,11 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml index 27bd845..3573e86 100644 --- a/android/app/src/main/res/values/dimens.xml +++ b/android/app/src/main/res/values/dimens.xml @@ -1,4 +1,6 @@ 48dp + 64dp + 16dp \ No newline at end of file diff --git a/assets/controls/stick.svg b/assets/controls/stick.svg new file mode 100644 index 0000000..96bbe17 --- /dev/null +++ b/assets/controls/stick.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + +