Add Analog Sticks to Android

This commit is contained in:
Florian Märkl 2019-09-29 17:08:16 +02:00
commit 37e5de2b0f
No known key found for this signature in database
GPG key ID: 125BC8A5A6A1E857
11 changed files with 400 additions and 83 deletions

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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)
}}
}

View file

@ -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))
}
}
}
}
}

View file

@ -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)
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>