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