diff --git a/app/src/main/java/com/urik/keyboard/settings/KeyboardSettings.kt b/app/src/main/java/com/urik/keyboard/settings/KeyboardSettings.kt
index 65a7c90e..521a3e04 100644
--- a/app/src/main/java/com/urik/keyboard/settings/KeyboardSettings.kt
+++ b/app/src/main/java/com/urik/keyboard/settings/KeyboardSettings.kt
@@ -64,6 +64,7 @@ enum class CursorSpeed(
SLOW(R.string.cursor_speed_slow, 40f),
MEDIUM(R.string.cursor_speed_medium, 25f),
FAST(R.string.cursor_speed_fast, 15f),
+ VERY_FAST(R.string.cursor_speed_very_fast, 8f),
}
/**
diff --git a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/HapticSignature.kt b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/HapticSignature.kt
index ff112297..1496b1ba 100644
--- a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/HapticSignature.kt
+++ b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/HapticSignature.kt
@@ -3,80 +3,118 @@ package com.urik.keyboard.ui.keyboard.components
import android.os.VibrationEffect
sealed class HapticSignature {
+ abstract val durationMs: Long
+
abstract fun createEffect(baseAmplitude: Int): VibrationEffect
+ protected fun createAmplitudeEffect(
+ timings: LongArray,
+ amplitudes: IntArray,
+ totalDurationMs: Long,
+ baseAmplitude: Int,
+ ): VibrationEffect =
+ if (baseAmplitude == VibrationEffect.DEFAULT_AMPLITUDE) {
+ VibrationEffect.createOneShot(totalDurationMs, VibrationEffect.DEFAULT_AMPLITUDE)
+ } else {
+ VibrationEffect.createWaveform(timings, amplitudes, -1)
+ }
+
data object LetterClick : HapticSignature() {
+ override val durationMs = 25L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val amplitude = baseAmplitude.coerceIn(1, 255)
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 25),
intArrayOf(0, amplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object SpaceThump : HapticSignature() {
+ override val durationMs = 35L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val peakAmplitude = baseAmplitude.coerceIn(1, 255)
val startAmplitude = (peakAmplitude * 0.5).toInt()
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 15, 20),
intArrayOf(0, startAmplitude, peakAmplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object BackspaceChirp : HapticSignature() {
+ override val durationMs = 28L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val startAmplitude = baseAmplitude.coerceIn(1, 255)
val endAmplitude = (startAmplitude * 0.4).toInt()
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 14, 14),
intArrayOf(0, startAmplitude, endAmplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object ShiftPulse : HapticSignature() {
+ override val durationMs = 42L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val amplitude = (baseAmplitude * 0.9).toInt().coerceIn(1, 255)
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 15, 12, 15),
intArrayOf(0, amplitude, 0, amplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object EnterCompletion : HapticSignature() {
+ override val durationMs = 51L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val peakAmplitude = baseAmplitude.coerceIn(1, 255)
val midAmplitude = (peakAmplitude * 0.7).toInt()
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 12, 25, 14),
intArrayOf(0, midAmplitude, peakAmplitude, midAmplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object PunctuationTick : HapticSignature() {
+ override val durationMs = 18L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
val amplitude = (baseAmplitude * 0.7).toInt().coerceIn(1, 255)
- return VibrationEffect.createWaveform(
+ return createAmplitudeEffect(
longArrayOf(0, 18),
intArrayOf(0, amplitude),
- -1,
+ durationMs,
+ baseAmplitude,
)
}
}
data object NumberClick : HapticSignature() {
+ override val durationMs = 25L
+
override fun createEffect(baseAmplitude: Int): VibrationEffect {
- val amplitude = (baseAmplitude * 0.85).toInt().coerceIn(1, 255)
+ val amplitude = if (baseAmplitude == VibrationEffect.DEFAULT_AMPLITUDE) {
+ VibrationEffect.DEFAULT_AMPLITUDE
+ } else {
+ (baseAmplitude * 0.85).toInt().coerceIn(1, 255)
+ }
return VibrationEffect.createOneShot(25L, amplitude)
}
}
diff --git a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/KeyboardLayoutManager.kt b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/KeyboardLayoutManager.kt
index 9e9c36e3..6fdc13c3 100644
--- a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/KeyboardLayoutManager.kt
+++ b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/KeyboardLayoutManager.kt
@@ -95,6 +95,13 @@ class KeyboardLayoutManager(
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as? android.os.Vibrator
}
+ private val supportsAmplitudeControl = vibrator?.hasAmplitudeControl() == true
+ private val vibrationAttributes =
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ android.os.VibrationAttributes.createForUsage(android.os.VibrationAttributes.USAGE_TOUCH)
+ } else {
+ null
+ }
private var hapticEnabled = true
private var hapticAmplitude = 170
private var shiftLongPressFired = false
@@ -629,11 +636,9 @@ class KeyboardLayoutManager(
fun triggerBackspaceHaptic() {
if (!hapticEnabled || hapticAmplitude == 0) return
- try {
- val effect = HapticSignature.BackspaceChirp.createEffect(hapticAmplitude)
- vibrator?.vibrate(effect)
- } catch (_: Exception) {
- }
+ val amplitude = if (supportsAmplitudeControl) hapticAmplitude else android.os.VibrationEffect.DEFAULT_AMPLITUDE
+ val effect = HapticSignature.BackspaceChirp.createEffect(amplitude)
+ vibrateEffect(effect)
}
fun forceStopAcceleratedBackspace() {
@@ -718,6 +723,18 @@ class KeyboardLayoutManager(
customKeyMappings = mappings
}
+ private fun vibrateEffect(effect: android.os.VibrationEffect) {
+ val v = vibrator ?: return
+ try {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && vibrationAttributes != null) {
+ v.vibrate(effect, vibrationAttributes)
+ } else {
+ v.vibrate(effect)
+ }
+ } catch (_: Exception) {
+ }
+ }
+
private fun performContextualHaptic(key: KeyboardKey?) {
if (!hapticEnabled || hapticAmplitude == 0) return
@@ -752,8 +769,9 @@ class KeyboardLayoutManager(
}
}
- val effect = signature.createEffect(hapticAmplitude)
- vibrator?.vibrate(effect)
+ val amplitude = if (supportsAmplitudeControl) hapticAmplitude else android.os.VibrationEffect.DEFAULT_AMPLITUDE
+ val effect = signature.createEffect(amplitude)
+ vibrateEffect(effect)
} catch (_: Exception) {
}
}
@@ -1811,10 +1829,14 @@ class KeyboardLayoutManager(
val phaseProgress = elapsed / 500f
val intervalMs = (80 - phaseProgress * 20).toLong().coerceAtLeast(60)
val intensity = 0.4f + phaseProgress * 0.3f
- val amplitude = (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ val amplitude = if (supportsAmplitudeControl) {
+ (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ } else {
+ android.os.VibrationEffect.DEFAULT_AMPLITUDE
+ }
withContext(Dispatchers.Main) {
- vibrator?.vibrate(
+ vibrateEffect(
android.os.VibrationEffect.createOneShot(intervalMs / 2, amplitude),
)
}
@@ -1832,10 +1854,14 @@ class KeyboardLayoutManager(
val phaseProgress = (elapsed - 500) / 1000f
val intervalMs = (60 - phaseProgress * 30).toLong().coerceAtLeast(30)
val intensity = 0.7f + phaseProgress * 0.3f
- val amplitude = (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ val amplitude = if (supportsAmplitudeControl) {
+ (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ } else {
+ android.os.VibrationEffect.DEFAULT_AMPLITUDE
+ }
withContext(Dispatchers.Main) {
- vibrator?.vibrate(
+ vibrateEffect(
android.os.VibrationEffect.createOneShot(intervalMs / 2, amplitude),
)
}
@@ -1873,21 +1899,22 @@ class KeyboardLayoutManager(
}
val intensity = 0.4f + progress * 0.7f
- (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ if (supportsAmplitudeControl) {
+ (hapticAmplitude * intensity).toInt().coerceIn(1, 255)
+ } else {
+ android.os.VibrationEffect.DEFAULT_AMPLITUDE
+ }
}
backgroundScope.launch {
withContext(Dispatchers.Main) {
- try {
- vibrator?.vibrate(
- android.os.VibrationEffect.createWaveform(
- timings,
- amplitudes,
- rampSteps + 2,
- ),
- )
- } catch (_: Exception) {
- }
+ vibrateEffect(
+ android.os.VibrationEffect.createWaveform(
+ timings,
+ amplitudes,
+ rampSteps + 2,
+ ),
+ )
}
}
}
diff --git a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/SwipeKeyboardView.kt b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/SwipeKeyboardView.kt
index ed5be000..768d3782 100644
--- a/app/src/main/java/com/urik/keyboard/ui/keyboard/components/SwipeKeyboardView.kt
+++ b/app/src/main/java/com/urik/keyboard/ui/keyboard/components/SwipeKeyboardView.kt
@@ -102,6 +102,8 @@ class SwipeKeyboardView
private var gestureStartY = 0f
private var gestureLastProcessedX = 0f
private var gestureDensity = 1f
+ private var gesturePrevX = 0f
+ private var gesturePrevTime = 0L
private var currentCursorSpeed: com.urik.keyboard.settings.CursorSpeed = com.urik.keyboard.settings.CursorSpeed.MEDIUM
private var confirmationOverlay: FrameLayout? = null
@@ -1672,9 +1674,24 @@ class SwipeKeyboardView
when (key.action) {
KeyboardKey.ActionType.SPACE -> {
- val totalDx = x - gestureStartX
- val sensitivity = currentCursorSpeed.sensitivityDp * gestureDensity
+ val now = System.currentTimeMillis()
+ val dt = (now - gesturePrevTime).coerceAtLeast(1)
+ val velocityPxPerMs = kotlin.math.abs(x - gesturePrevX) / dt.toFloat()
+ gesturePrevX = x
+ gesturePrevTime = now
+
+ val velocityDpPerMs = velocityPxPerMs / gestureDensity
+ val accelerationMultiplier = when {
+ velocityDpPerMs > 1.5f -> 3.0f
+ velocityDpPerMs > 0.8f -> 2.0f
+ velocityDpPerMs > 0.4f -> 1.4f
+ else -> 1.0f
+ }
+ val baseSensitivity = currentCursorSpeed.sensitivityDp * gestureDensity
+ val sensitivity = baseSensitivity / accelerationMultiplier
+
+ val totalDx = x - gestureStartX
val positionsToMove = (totalDx / sensitivity).toInt()
val lastPositionsMoved = ((gestureLastProcessedX - gestureStartX) / sensitivity).toInt()
val deltaPositions = positionsToMove - lastPositionsMoved
@@ -1749,6 +1766,8 @@ class SwipeKeyboardView
gestureStartX = ev.x
gestureStartY = ev.y
gestureLastProcessedX = ev.x
+ gesturePrevX = ev.x
+ gesturePrevTime = System.currentTimeMillis()
}
swipeDetector?.handleTouchEvent(ev) { x, y -> findKeyAt(x, y) }
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 83a57c6a..8d3daaf4 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -54,6 +54,7 @@
بطيء
متوسط
سريع
+ سريع جداً
نحيف
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index b9ef8a9f..12a3fcb6 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -51,6 +51,7 @@
Lenta
Mitjana
Ràpida
+ Molt ràpid
Compacta
Estàndard
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 4cd25864..ebaaf8a3 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -53,6 +53,7 @@
Pomalá
Střední
Rychlá
+ Velmi rychlá
Kompaktní
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index d069a397..aa6e22eb 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -52,6 +52,7 @@
Langsam
Mittel
Schnell
+ Sehr schnell
Kompakt
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 9e7cd379..80233d85 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -51,6 +51,7 @@
Αργή
Κανονική
Γρήγορη
+ Πολύ γρήγορο
Στενό
Κανονικό
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 5ab94dcb..4c3b3d55 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -53,6 +53,7 @@
Lenta
Media
Rápida
+ Muy rápido
Compacta
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 18b909e7..8c60e8d9 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -53,6 +53,7 @@
کند
متوسط
سریع
+ خیلی سریع
جمعوجور
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index e57d2c67..1a329c62 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -54,6 +54,7 @@
Lent
Moyen
Rapide
+ Très rapide
Compacte
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9610ed68..a3fc686e 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -53,6 +53,7 @@
Lenta
Media
Veloce
+ Molto veloce
Compatta
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index d8e8a0fc..007adf4d 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -53,6 +53,7 @@
Traag
Gemiddeld
Snel
+ Zeer snel
Compact
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8c317f5a..c9afb048 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -53,6 +53,7 @@
Wolna
Średnia
Szybka
+ Bardzo szybko
Kompaktowy
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 3edb572b..9362d1aa 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -53,6 +53,7 @@
Lenta
Média
Rápida
+ Muito rápido
Compacta
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index e36960b2..582b12d5 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -53,6 +53,7 @@
Медленная
Средняя
Быстрая
+ Очень быстро
Компактный
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index bc0ee246..d7b9b7c3 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -188,6 +188,7 @@
Långsam
Medium
Snabb
+ Mycket snabb
Kompakt
Standard
Bred
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 25a20dc4..31907991 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -53,6 +53,7 @@
Повільна
Середня
Швидка
+ Дуже швидко
Компактний
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d04bdd93..900dae34 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -55,6 +55,7 @@
Slow
Medium
Fast
+ Very Fast
Compact