From 5914ceec77aa545e4995377f30d4c80d0cef3260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 11:34:10 +0100 Subject: [PATCH] Add TouchpadView to Android --- .../java/com/metallic/chiaki/lib/Chiaki.kt | 38 +++++++- .../chiaki/touchcontrols/TouchTracker.kt | 2 +- .../chiaki/touchcontrols/TouchpadView.kt | 86 +++++++++++++++++++ .../main/res/drawable/control_touchpad.xml | 12 +++ .../src/main/res/layout/fragment_controls.xml | 10 +++ android/app/src/main/res/values/attrs.xml | 4 + assets/controls/touchpad_surface.svg | 65 ++++++++++++++ 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt create mode 100644 android/app/src/main/res/drawable/control_touchpad.xml create mode 100644 assets/controls/touchpad_surface.svg diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 9533c7c..9c4ea34 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -152,9 +152,9 @@ private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a e private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX data class ControllerTouch( - val x: UShort = 0U, - val y: UShort = 0U, - val id: Byte = -1 // -1 = up + var x: UShort = 0U, + var y: UShort = 0U, + var id: Byte = -1 // -1 = up ) data class ControllerState constructor( @@ -165,7 +165,7 @@ data class ControllerState constructor( var leftY: Short = 0, var rightX: Short = 0, var rightY: Short = 0, - private var touchIdNext: UByte = 0U, + private var touchIdNext: UByte = 100U, var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), var gyroX: Float = 0.0f, var gyroY: Float = 0.0f, @@ -196,6 +196,8 @@ data class ControllerState constructor( val BUTTON_SHARE = (1 shl 13).toUInt() val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() + val TOUCHPAD_WIDTH: UShort = 1920U + val TOUCHPAD_HEIGHT: UShort = 942U } infix fun or(o: ControllerState) = ControllerState( @@ -272,6 +274,34 @@ data class ControllerState constructor( result = 31 * result + orientW.hashCode() return result } + + fun startTouch(x: UShort, y: UShort): UByte? = + touches + .find { it.id < 0 } + ?.also { + it.id = touchIdNext.toByte() + Log.d("TouchId", "touch id next: $touchIdNext") + touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() + }?.id?.toUByte() + + fun stopTouch(id: UByte) + { + touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + it.id = -1 + } + } + + fun setTouchPos(id: UByte, x: UShort, y: UShort): Boolean + = touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + val r = it.x != x || it.y != y + it.x = x + it.y = y + r + } ?: false } class QuitReason(val value: Int) 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 index 01d6869..a1ead71 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -26,7 +26,7 @@ class TouchTracker if(pointerId == null) { pointerId = event.getPointerId(event.actionIndex) - currentPosition = Vector(event.x, event.y) + currentPosition = Vector(event.getX(event.actionIndex), event.getY(event.actionIndex)) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt new file mode 100644 index 0000000..292351b --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.metallic.chiaki.R +import com.metallic.chiaki.lib.ControllerState + +class TouchpadView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) +{ + val state: ControllerState = ControllerState() + private val pointerTouchIds = mutableMapOf() + + var stateChangeCallback: ((ControllerState) -> Unit)? = null + + private val drawable: Drawable? + + init + { + context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { + drawable = getDrawable(R.styleable.TouchpadView_drawable) + recycle() + } + isClickable = true + } + + override fun onDraw(canvas: Canvas) + { + super.onDraw(canvas) + if(state.touches.find { it.id >= 0 } == null) + return + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) + drawable?.draw(canvas) + } + + private fun touchX(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_WIDTH - 1u).toUShort(), + (ControllerState.TOUCHPAD_WIDTH.toFloat() * event.getX(index) / width.toFloat()).toUInt().toUShort())) + + private fun touchY(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_HEIGHT - 1u).toUShort(), + (ControllerState.TOUCHPAD_HEIGHT.toFloat() * event.getY(index) / height.toFloat()).toUInt().toUShort())) + + override fun onTouchEvent(event: MotionEvent): Boolean + { + when(event.actionMasked) + { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + pointerTouchIds[event.getPointerId(event.actionIndex)] = it + triggerStateChanged() + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + pointerTouchIds.remove(event.getPointerId(event.actionIndex))?.let { + state.stopTouch(it) + triggerStateChanged() + } + } + MotionEvent.ACTION_MOVE -> { + val changed = pointerTouchIds.entries.fold(false) { acc, it -> + val index = event.findPointerIndex(it.key) + if(index < 0) + acc + else + acc || state.setTouchPos(it.value, touchX(event, index), touchY(event, index)) + } + if(changed) + triggerStateChanged() + } + } + return true + } + + private fun triggerStateChanged() + { + stateChangeCallback?.let { it(state) } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/control_touchpad.xml b/android/app/src/main/res/drawable/control_touchpad.xml new file mode 100644 index 0000000..509b8a3 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 94c2c52..1f33197 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -47,6 +47,16 @@ app:drawableHandle="@drawable/control_analog_stick_handle" /> + + + + + + \ No newline at end of file diff --git a/assets/controls/touchpad_surface.svg b/assets/controls/touchpad_surface.svg new file mode 100644 index 0000000..7476128 --- /dev/null +++ b/assets/controls/touchpad_surface.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + +