diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml
index bf4f4e4..9edc514 100644
--- a/app/src/debug/res/values/donottranslate.xml
+++ b/app/src/debug/res/values/donottranslate.xml
@@ -1,3 +1,3 @@
- μLauncher [debug]
+ μLauncher [gesture detection debug]
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
index 973e0ca..331123d 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt
@@ -16,6 +16,7 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.actions.LauncherAction
+import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
@@ -62,16 +63,19 @@ class HomeActivity : UIObject, AppCompatActivity() {
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
+ // Initialise layout
+ binding = HomeBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+
touchGestureDetector = TouchGestureDetector(
this,
width,
height,
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
- )
-
- // Initialise layout
- binding = HomeBinding.inflate(layoutInflater)
- setContentView(binding.root)
+ ) { s ->
+ binding.logOutput.text = s
+ }
// Handle back key / gesture on Android 13+, cf. onKeyDown()
@@ -86,6 +90,12 @@ class HomeActivity : UIObject, AppCompatActivity() {
LauncherAction.SETTINGS.invoke(this)
}
+ binding.buttonCopyLog.setOnClickListener {
+ copyToClipboard(this,
+ touchGestureDetector.log_output)
+
+ }
+
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
index 00629a5..c35fc22 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/TouchGestureDetector.kt
@@ -1,20 +1,26 @@
package de.jrpie.android.launcher.ui
import android.content.Context
+import android.util.Log
import android.view.MotionEvent
import android.view.ViewConfiguration
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.preferences.LauncherPreferences
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
+import kotlin.math.sqrt
import kotlin.math.tan
class TouchGestureDetector(
private val context: Context,
val width: Int,
val height: Int,
- var edgeWidth: Float
+ var edgeWidth: Float,
+ val onLog: (String) -> Unit
) {
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
private val TOUCH_SLOP: Int
@@ -27,26 +33,51 @@ class TouchGestureDetector(
private val MIN_TRIANGLE_HEIGHT = 250
+ var log_output = ""
+ private var log_output_ui = ""
+ private fun log(s: String, ui:Boolean = true) {
+ Log.i("Gesture Detection", s)
+ log_output += "$s"
+ if (ui) {
+ var lines = 50
+ log_output_ui = (log_output_ui + "$s").takeLastWhile {
+ if (it == '\n') {
+ lines--
+ }
+ lines > 0
+ }
+ onLog(log_output_ui)
+ }
+ }
+
+
+
+ @Serializable
data class Vector(val x: Float, val y: Float) {
fun absSquared(): Float {
return this.x * this.x + this.y * this.y
}
+
fun plus(vector: Vector): Vector {
return Vector(this.x + vector.x, this.y + vector.y)
}
+
fun max(other: Vector): Vector {
return Vector(max(this.x, other.x), max(this.y, other.y))
}
+
fun min(other: Vector): Vector {
return Vector(min(this.x, other.x), min(this.y, other.y))
}
+
operator fun minus(vector: Vector): Vector {
return Vector(this.x - vector.x, this.y - vector.y)
}
}
+ @Serializable
class PointerPath(
val number: Int,
val start: Vector,
@@ -57,15 +88,18 @@ class TouchGestureDetector(
fun sizeSquared(): Float {
return (max - min).absSquared()
}
+
fun getDirection(): Vector {
return last - start
}
+
fun update(vector: Vector) {
min = min.min(vector)
max = max.max(vector)
last = vector
}
}
+
private fun PointerPath.isTap(): Boolean {
return sizeSquared() < TOUCH_SLOP_SQUARE
}
@@ -80,6 +114,14 @@ class TouchGestureDetector(
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
+
+ log(
+ "TOUCH_SLOP: $TOUCH_SLOP\n" +
+ "DOUBLE_TAP_SLOP: $DOUBLE_TAP_SLOP\n" +
+ "LONG_PRESS_TIMEOUT: $LONG_PRESS_TIMEOUT\n" +
+ "TAP_TIMEOUT: $TAP_TIMEOUT\n" +
+ "DOUBLE_TAP_TIMEOUT: $DOUBLE_TAP_TIMEOUT\n======================\n"
+ )
}
private var paths = HashMap()
@@ -92,12 +134,13 @@ class TouchGestureDetector(
(0.., timeStart: Long, timeEnd: Long) {
+ log(" - gesture complete")
+ log("\npaths = ${paths.entries.map { (x,y) -> "$x: ${Json.encodeToString(y)}"}}", false)
val duration = timeEnd - timeStart
val pointerCount = paths.entries.size
+ log("\nDuration: $duration, pointers: $pointerCount")
if (paths.entries.isEmpty()) {
return
}
@@ -155,41 +201,63 @@ class TouchGestureDetector(
// Ignore swipes at the very top, since this interferes with the status bar.
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
if (paths.entries.any { it.value.start.y < 100 }) {
+ log("\nToo close to the top")
return
}
if (pointerCount == 1 && mainPointerPath.isTap()) {
// detect taps
+ log("\nSlop: ${sqrt(mainPointerPath.sizeSquared())} - click")
+ log("\nTime since last click: ${timeStart - lastTappedTime}")
+ log(
+ "\nDistance to last click: ${
+ sqrt(
+ (mainPointerPath.last - (lastTappedLocation ?: Vector(
+ Float.NEGATIVE_INFINITY,
+ Float.NEGATIVE_INFINITY
+ ))).absSquared()
+ .toDouble()
+ )
+ }"
+ )
if (duration in 0..TAP_TIMEOUT) {
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
lastTappedLocation?.let {
- (mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE} == true
+ (mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE
+ } == true
) {
+ log("\nDouble click detected")
Gesture.DOUBLE_CLICK.invoke(context)
} else {
+ log("\nNot a double click: last tap too long ago")
lastTappedTime = timeEnd
lastTappedLocation = mainPointerPath.last
}
} else if (duration > LONG_PRESS_TIMEOUT) {
// TODO: Don't wait until the finger is lifted.
// Instead set a timer to start long click as soon as LONG_PRESS_TIMEOUT is reached
+ log("\nLong click detected")
Gesture.LONG_CLICK.invoke(context)
}
} else {
+ log("\nSlop: ${sqrt(mainPointerPath.sizeSquared())} - not a click")
// detect swipes
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
var gesture = getGestureForDirection(mainPointerPath.getDirection())
+ log("\nbase gesture: ${gesture}")
if (doubleActions && pointerCount > 1) {
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
// the directions of the pointers don't match
+ log("\ndirections of the pointers don't match")
return
}
gesture = gesture?.let(Gesture::getDoubleVariant)
+ log(", double variant: ${gesture}")
}
// detect triangles
@@ -197,35 +265,40 @@ class TouchGestureDetector(
val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
when (gesture) {
Gesture.SWIPE_DOWN -> {
- if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
+ if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER
}
}
+
Gesture.SWIPE_UP -> {
- if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
+ if (startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
gesture = Gesture.SWIPE_LARGER_REVERSE
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
gesture = Gesture.SWIPE_SMALLER_REVERSE
}
}
+
Gesture.SWIPE_RIGHT -> {
- if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
+ if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA
}
}
+
Gesture.SWIPE_LEFT -> {
- if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
+ if (startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
gesture = Gesture.SWIPE_V_REVERSE
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
gesture = Gesture.SWIPE_LAMBDA_REVERSE
}
}
- else -> { }
+
+ else -> {}
}
+ log(", v-variant: $gesture")
if (edgeActions) {
if (mainPointerPath.max.x < edgeWidth * width) {
@@ -240,10 +313,12 @@ class TouchGestureDetector(
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
}
}
+ log(", edge-variant: $gesture")
if (timeStart - lastTappedTime < 2 * DOUBLE_TAP_TIMEOUT) {
gesture = gesture?.getTapComboVariant()
}
+ log(", tap-combo-variant: $gesture")
gesture?.invoke(context)
}
}
diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml
index ecefdea..4c8a262 100644
--- a/app/src/main/res/layout/home.xml
+++ b/app/src/main/res/layout/home.xml
@@ -48,4 +48,22 @@
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/baseline_settings_24"/>
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index 6ebdf63..2b278a6 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -1,6 +1,6 @@
- μLauncher
+ μLauncher [gesture detection]