mirror of
https://git.sr.ht/~thestr4ng3r/chiaki
synced 2025-08-14 10:46:51 -07:00
Add Analog Sticks to Android
This commit is contained in:
parent
8f2275c6ab
commit
37e5de2b0f
11 changed files with 400 additions and 83 deletions
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Short, Short> = 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<Short, Short> = 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)
|
||||
}}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="135.46666"
|
||||
android:viewportHeight="135.46667">
|
||||
<path
|
||||
android:pathData="M67.733,67.733m-67.733,0a67.733,67.733 0,1 1,135.467 0a67.733,67.733 0,1 1,-135.467 0"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4.56389332"
|
||||
android:fillColor="@color/control_primary"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="135.46666"
|
||||
android:viewportHeight="135.46667">
|
||||
<path
|
||||
android:pathData="M67.733,67.733m-67.733,0a67.733,67.733 0,1 1,135.467 0a67.733,67.733 0,1 1,-135.467 0"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4.56389332"
|
||||
android:fillColor="@color/control_pressed"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
|
@ -2,12 +2,48 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:clipChildren="false">
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.ControlsBackgroundView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/centerGuideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintGuide_percent="0.5"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.AnalogStickView
|
||||
android:id="@+id/leftAnalogStickView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/centerGuideline"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/dpadView"
|
||||
app:radius="@dimen/control_analog_stick_radius"
|
||||
app:handleRadius="@dimen/control_analog_stick_handle_radius"
|
||||
app:drawableBase="@drawable/control_analog_stick_base"
|
||||
app:drawableHandle="@drawable/control_analog_stick_handle"
|
||||
/>
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.AnalogStickView
|
||||
android:id="@+id/rightAnalogStickView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintLeft_toRightOf="@id/centerGuideline"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/faceButtonsLayout"
|
||||
app:radius="@dimen/control_analog_stick_radius"
|
||||
app:handleRadius="@dimen/control_analog_stick_handle_radius"
|
||||
app:drawableBase="@drawable/control_analog_stick_base"
|
||||
app:drawableHandle="@drawable/control_analog_stick_handle"
|
||||
/>
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.DPadView
|
||||
android:id="@+id/dpadView"
|
||||
android:layout_width="128dp"
|
||||
|
@ -19,6 +55,7 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/faceButtonsLayout"
|
||||
android:layout_width="144dp"
|
||||
android:layout_height="144dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
@ -167,22 +204,4 @@
|
|||
app:layout_constraintRight_toLeftOf="@id/r2ButtonView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.DPadView
|
||||
android:id="@+id/leftDpadView"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<com.metallic.chiaki.touchcontrols.DPadView
|
||||
android:id="@+id/rightDpadView"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_marginLeft="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,4 +4,11 @@
|
|||
<attr name="drawableIdle" format="reference" />
|
||||
<attr name="drawablePressed" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AnalogStickView">
|
||||
<attr name="radius" format="dimension" />
|
||||
<attr name="handleRadius" format="dimension" />
|
||||
<attr name="drawableBase" format="reference" />
|
||||
<attr name="drawableHandle" format="reference" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="control_face_button_size">48dp</dimen>
|
||||
<dimen name="control_analog_stick_radius">64dp</dimen>
|
||||
<dimen name="control_analog_stick_handle_radius">16dp</dimen>
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue