0
0
mirror of https://github.com/florisboard/florisboard.git synced 2024-09-19 19:42:20 +02:00

Re-implement glide typing for new keyboard view

This commit is contained in:
Patrick Goldinger 2021-05-08 19:59:50 +02:00
parent d3e8d35e5d
commit 535b48e5b4
5 changed files with 219 additions and 118 deletions

View File

@ -2,6 +2,7 @@ package dev.patrickgold.florisboard.ime.text.gestures
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
/**
* Inherit this to be able to handle gesture typing. Takes in raw pointer data, and
@ -17,7 +18,7 @@ interface GlideTypingClassifier {
/**
* Change the layout of the gesture classifier.
*/
fun setLayout(keyViews: Sequence<Key>, subtype: Subtype)
fun setLayout(keyViews: List<TextKey>, subtype: Subtype)
/**
* Change the word data of the gesture classifier.
@ -38,5 +39,4 @@ interface GlideTypingClassifier {
fun getSuggestions(maxSuggestionCount: Int, gestureCompleted: Boolean): List<String>
fun clear()
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.view.MotionEvent
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
import kotlin.math.pow
import kotlin.math.sqrt
@ -33,7 +34,7 @@ class GlideTypingGesture {
* Method which evaluates if a given [event] is a gesture.
* @return whether or not the event was interpreted as part of a gesture.
*/
fun onTouchEvent(event: MotionEvent, initialKeyCodes: MutableMap<Int, Int>): Boolean {
fun onTouchEvent(event: MotionEvent, initialKey: TextKey?): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
@ -69,12 +70,12 @@ class GlideTypingGesture {
// evaluate whether is actually a gesture
val dist = pointerData.positions[0].dist(pos)
val time = (System.currentTimeMillis() - pointerData.startTime) + 1
if (dist > keySize && (dist / time) > VELOCITY_THRESHOLD && (initialKeyCodes[pointerId] !in SWIPE_GESTURE_KEYS)) {
if (dist > keySize && (dist / time) > VELOCITY_THRESHOLD && (initialKey?.computedData?.code !in SWIPE_GESTURE_KEYS)) {
pointerData.isActuallyGesture = true
// Let listener know all those points need to be added.
pointerData.positions.take(pointerData.positions.size - 1).forEach { point ->
listeners.forEach {
it.onGestureAdd(point)
it.onGlideAddPoint(point)
}
}
} else if (time > MAX_DETECT_TIME) {
@ -84,7 +85,7 @@ class GlideTypingGesture {
}
if (pointerData.isActuallyGesture == true)
pointerData.positions.last().let { point -> listeners.forEach { it.onGestureAdd(point) } }
pointerData.positions.last().let { point -> listeners.forEach { it.onGlideAddPoint(point) } }
}
return pointerData.isActuallyGesture ?: false
}
@ -95,14 +96,14 @@ class GlideTypingGesture {
return false
}
if (pointerData.isActuallyGesture == true) {
listeners.forEach { listener -> listener.onGestureComplete(pointerData) }
listeners.forEach { listener -> listener.onGlideComplete(pointerData) }
}
resetState()
return false
}
MotionEvent.ACTION_CANCEL -> {
if (pointerData.isActuallyGesture == true) {
listeners.forEach { it.onGestureCancelled() }
listeners.forEach { it.onGlideCancelled() }
}
resetState()
}
@ -112,7 +113,11 @@ class GlideTypingGesture {
}
fun registerListener(listener: Listener) {
this.listeners.add(listener)
listeners.add(listener)
}
fun unregisterListener(listener: Listener) {
listeners.remove(listener)
}
private fun resetState() {
@ -145,18 +150,17 @@ class GlideTypingGesture {
/**
* Called when a gesture is complete.
*/
fun onGestureComplete(data: Detector.PointerData) {}
fun onGlideComplete(data: Detector.PointerData) {}
/**
* Called when a point is added to a gesture.
* Will not be called before a series of events is detected as a gesture.
*/
fun onGestureAdd(point: Detector.Position) {}
fun onGlideAddPoint(point: Detector.Position) {}
/**
* Called to cancel a gesture
*/
fun onGestureCancelled() {}
fun onGlideCancelled() {}
}
}

View File

@ -6,8 +6,8 @@ import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.extension.AssetManager
import dev.patrickgold.florisboard.ime.extension.AssetRef
import dev.patrickgold.florisboard.ime.extension.AssetSource
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.text.TextInputManager
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
import kotlinx.coroutines.*
import org.json.JSONObject
@ -18,7 +18,6 @@ import org.json.JSONObject
class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainScope() {
private var glideTypingClassifier = StatisticalGlideTypingClassifier()
private val initialDimensions: HashMap<Subtype, Dimensions> = hashMapOf()
private var currentDimensions: Dimensions = Dimensions(0f, 0f)
private lateinit var prefHelper: PrefHelper
@ -35,19 +34,19 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
}
}
override fun onGestureComplete(data: GlideTypingGesture.Detector.PointerData) {
override fun onGlideComplete(data: GlideTypingGesture.Detector.PointerData) {
updateSuggestionsAsync(MAX_SUGGESTION_COUNT, true) {
glideTypingClassifier.clear()
}
}
override fun onGestureCancelled() {
override fun onGlideCancelled() {
glideTypingClassifier.clear()
}
private var lastTime = System.currentTimeMillis()
override fun onGestureAdd(point: GlideTypingGesture.Detector.Position) {
val normalized = GlideTypingGesture.Detector.Position(normalizeX(point.x), normalizeY(point.y))
override fun onGlideAddPoint(point: GlideTypingGesture.Detector.Position) {
val normalized = GlideTypingGesture.Detector.Position(point.x, point.y)
this.glideTypingClassifier.addGesturePoint(normalized)
@ -61,11 +60,8 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
/**
* Change the layout of the internal gesture classifier
*/
fun setLayout(keys: Sequence<Key>, dimensions: Dimensions) {
fun setLayout(keys: List<TextKey>) {
glideTypingClassifier.setLayout(keys, FlorisBoard.getInstance().activeSubtype)
initialDimensions.getOrPut(FlorisBoard.getInstance().activeSubtype, {
dimensions
})
}
private val wordDataCache = hashMapOf<String, Int>()
@ -85,46 +81,11 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
}
}
fun updateDimensions(dimensions: Dimensions) {
this.currentDimensions = dimensions
}
/**
* To avoid constantly having to regenerate Pruners every time we switch between landscape and portrait or enable/
* disable one handed mode, we just normalize the x, y coordinates to the same range as the original which were
* active when the Pruner was created.
*/
private fun normalizeX(x: Float): Float {
val initial = initialDimensions[FlorisBoard.getInstance().activeSubtype] ?: return x
return scaleRange(
x,
0f,
currentDimensions.width,
0f,
initial.width
)
}
/**
* To avoid constantly having to regenerate Pruners every time we switch between landscape and portrait or enable/
* disable one handed mode, we just normalize the x, y coordinates to the same range as the original which were
* active when the Pruner was created.
*/
private fun normalizeY(y: Float): Float {
val initial = initialDimensions[FlorisBoard.getInstance().activeSubtype] ?: return y
return scaleRange(
y,
0f,
currentDimensions.height,
0f,
initial.height
)
}
private fun scaleRange(x: Float, oldMin: Float, oldMax: Float, newMin: Float, newMax: Float): Float {
return (((x - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin
fun updateDimensions(newWidth: Float, newHeight: Float) {
currentDimensions.apply {
width = newWidth
height = newHeight
}
}
/**
@ -164,6 +125,6 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
}
data class Dimensions(
val width: Float,
val height: Float
var width: Float,
var height: Float
)

View File

@ -2,8 +2,11 @@ package dev.patrickgold.florisboard.ime.text.gestures
import android.util.SparseArray
import androidx.collection.LruCache
import androidx.core.util.set
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.keyboard.Key
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
import java.text.Normalizer
import java.util.*
import kotlin.collections.HashMap
@ -15,12 +18,11 @@ import kotlin.math.*
* Check out Étienne Desticourt's excellent write up at https://github.com/AnySoftKeyboard/AnySoftKeyboard/pull/1870
*/
class StatisticalGlideTypingClassifier : GlideTypingClassifier {
private val gesture = Gesture()
private var keysByCharacter: SparseArray<Key> = SparseArray()
private var keysByCharacter: SparseArray<TextKey> = SparseArray()
private var words: Set<String> = setOf()
private var wordFrequencies: Map<String, Int> = hashMapOf()
private var keys: ArrayList<Key> = arrayListOf()
private var keys: ArrayList<TextKey> = arrayListOf()
private lateinit var pruner: Pruner
private var wordDataSubtype: Subtype? = null
private var layoutSubtype: Subtype? = null
@ -33,7 +35,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
*/
private var distanceThresholdSquared = 0
companion object {
/**
* Describes the allowed length variance in a gesture. If a gesture is too long or too short, it is immediately
@ -85,27 +86,27 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
}
}
override fun setLayout(keyViews: Sequence<Key>, subtype: Subtype) {
override fun setLayout(keyViews: List<TextKey>, subtype: Subtype) {
// stop duplicate calls
if (this.layoutSubtype == subtype) {
if (layoutSubtype == subtype) {
return
}
keysByCharacter.clear()
keys.clear()
/*keyViews.forEach {
keysByCharacter[it.data.code] = it
this.keys.add(it)
keyViews.forEach {
keysByCharacter[it.computedData.code] = it
keys.add(it)
}
layoutSubtype = subtype
distanceThresholdSquared = (keyViews.first().width / 4)*/
distanceThresholdSquared = (keyViews.first().visibleBounds.width() / 4)
distanceThresholdSquared *= distanceThresholdSquared
initializePruner()
}
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
// stop duplicate calls..
if (this.wordDataSubtype == subtype) {
if (wordDataSubtype == subtype) {
return
}
@ -164,7 +165,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
val candidates = arrayListOf<String>()
val candidateWeights = arrayListOf<Float>()
val key = keys.firstOrNull() ?: return listOf()
val radius = 0//min(key.height, key.width)
val radius = min(key.visibleBounds.height(), key.visibleBounds.width())
var remainingWords = pruner.pruneByExtremities(gesture, this.keys)
val userGesture = gesture.resample(SAMPLING_POINTS)
val normalizedUserGesture: Gesture = userGesture.normalizeByBoxSide()
@ -213,7 +214,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
}
override fun clear() {
this.gesture.clear()
gesture.clear()
}
private fun calcLocationDistance(gesture1: Gesture, gesture2: Gesture): Float {
@ -227,7 +228,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
totalDistance += distance
}
return totalDistance / SAMPLING_POINTS / 2
}
private fun calcGaussianProbability(value: Float, mean: Float, standardDeviation: Float): Float {
@ -256,7 +256,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
* The length difference between a user gesture and a word gesture above which a word will
* be pruned.
*/
private val lengthThreshold: Double, words: Set<String>, keysByCharacter: SparseArray<Key>
private val lengthThreshold: Double, words: Set<String>, keysByCharacter: SparseArray<TextKey>
) {
/** A tree that provides fast access to words based on their first and last letter. */
@ -303,13 +303,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
fun pruneByLength(
userGesture: Gesture,
words: ArrayList<String>,
keysByCharacter: SparseArray<Key>,
keys: List<Key>
keysByCharacter: SparseArray<TextKey>,
keys: List<TextKey>
): ArrayList<String> {
val remainingWords = ArrayList<String>()
val key = keys.firstOrNull() ?: return arrayListOf()
val radius = 0//min(key.height, key.width)
val radius = min(key.visibleBounds.height(), key.visibleBounds.width())
val userLength = userGesture.getLength()
for (word in words) {
val idealGestures = Gesture.generateIdealGestures(word, keysByCharacter)
@ -325,21 +325,20 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
companion object {
private fun getFirstKeyLastKey(
word: String, keysByCharacter: SparseArray<Key>
word: String, keysByCharacter: SparseArray<TextKey>
): Pair<Int, Int>? {
val firstLetter = word[0]
val lastLetter = word[word.length - 1]
val firstBaseChar = Normalizer.normalize(firstLetter.toString(), Normalizer.Form.NFD)[0]
val lastBaseChar = Normalizer.normalize(lastLetter.toString(), Normalizer.Form.NFD)[0]
return when {
keysByCharacter.indexOfKey(firstBaseChar.toInt()) < 0 || keysByCharacter.indexOfKey(lastBaseChar.toInt()) < 0 -> {
keysByCharacter.indexOfKey(firstBaseChar.code) < 0 || keysByCharacter.indexOfKey(lastBaseChar.code) < 0 -> {
null
}
else -> {
val firstKey = keysByCharacter[firstBaseChar.toInt()]
val lastKey = keysByCharacter[lastBaseChar.toInt()]
//Pair(firstKey.data.code, lastKey.data.code)
Pair(0, 0)
val firstKey = keysByCharacter[firstBaseChar.code]
val lastKey = keysByCharacter[lastBaseChar.code]
Pair(firstKey.computedData.code, lastKey.computedData.code)
}
}
}
@ -357,14 +356,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
x: Float, y: Float, n: Int, keys: Iterable<Key>
): Iterable<Int> {
val keyDistances = HashMap<Key, Float>()
/*for (key in keys) {
val distance = Gesture.distance(key.centerX, key.centerY, x, y)
for (key in keys) {
val distance = Gesture.distance(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat(), x, y)
keyDistances[key] = distance
}
return keyDistances.entries.sortedWith { c1, c2 -> c1.value.compareTo(c2.value) }.take(n)
.map { it.key.data.code }*/
return listOf()
.map { (it.key as? TextKey)?.computedData?.code ?: KeyCode.UNSPECIFIED }
}
}
@ -387,7 +385,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
companion object {
private const val MAX_SIZE = 300
fun generateIdealGestures(word: String, keysByCharacter: SparseArray<Key>): List<Gesture> {
fun generateIdealGestures(word: String, keysByCharacter: SparseArray<TextKey>): List<Gesture> {
val idealGesture = Gesture()
val idealGestureWithLoops = Gesture()
var previousLetter = '\u0000'
@ -396,11 +394,11 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
// Add points for each key
for (c in word) {
val lc = Character.toLowerCase(c)
var key = keysByCharacter[lc.toInt()]
var key = keysByCharacter[lc.code]
if (key == null) {
// Try finding the base character instead, e.g., the "e" key instead of "é"
val baseCharacter: Char = Normalizer.normalize(lc.toString(), Normalizer.Form.NFD)[0]
key = keysByCharacter[baseCharacter.toInt()]
key = keysByCharacter[baseCharacter.code]
if (key == null) {
continue
}
@ -408,30 +406,30 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
// We adda little loop on the key for duplicate letters
// so that we can differentiate words like pool and poll, lull and lul, etc...
/*if (previousLetter == lc) {
if (previousLetter == lc) {
// bottom right
idealGestureWithLoops.addPoint(
key.centerX + key.width / 4.0f, key.centerY + key.height / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
// top right
idealGestureWithLoops.addPoint(
key.centerX + key.width / 4.0f, key.centerY - key.height / 4.0f
key.visibleBounds.centerX() + key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// top left
idealGestureWithLoops.addPoint(
key.centerX - key.width / 4.0f, key.centerY - key.height / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() - key.visibleBounds.height() / 4.0f
)
// bottom left
idealGestureWithLoops.addPoint(
key.centerX - key.width / 4.0f, key.centerY + key.height / 4.0f
key.visibleBounds.centerX() - key.visibleBounds.width() / 4.0f, key.visibleBounds.centerY() + key.visibleBounds.height() / 4.0f
)
hasLoops = true
idealGesture.addPoint(key.centerX, key.centerY)
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
} else {
idealGesture.addPoint(key.centerX, key.centerY)
idealGestureWithLoops.addPoint(key.centerX, key.centerY)
}*/
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
idealGestureWithLoops.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
}
previousLetter = lc
}
return when (hasLoops) {
@ -443,7 +441,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2))
}
}
fun addPoint(x: Float, y: Float) {
@ -455,7 +452,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
size += 1
}
/**
* Resamples the gesture into a new gesture with the chosen number of points by oversampling
* it.
@ -481,7 +477,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
}
}
for (i in 0 until size - 1) {
// We calculate the unit vector from the two points we're between in the actual
// gesture
@ -515,7 +510,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
}
fun normalizeByBoxSide(): Gesture {
val normalizedGesture = Gesture()
var maxX = -1.0f
@ -575,7 +569,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
return Gesture(xs.clone(), ys.clone(), size)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -598,6 +591,4 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
return result
}
}
}

View File

@ -16,6 +16,7 @@
package dev.patrickgold.florisboard.ime.text.keyboard
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.*
@ -23,11 +24,14 @@ import android.graphics.drawable.PaintDrawable
import android.os.Handler
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.animation.AccelerateInterpolator
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.debug.*
import dev.patrickgold.florisboard.ime.core.*
import dev.patrickgold.florisboard.ime.keyboard.KeyboardView
import dev.patrickgold.florisboard.ime.popup.PopupManager
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingGesture
import dev.patrickgold.florisboard.ime.text.gestures.GlideTypingManager
import dev.patrickgold.florisboard.ime.text.gestures.SwipeAction
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
import dev.patrickgold.florisboard.ime.text.key.*
@ -37,9 +41,11 @@ import dev.patrickgold.florisboard.util.ViewLayoutUtils
import dev.patrickgold.florisboard.util.cancelAll
import dev.patrickgold.florisboard.util.postDelayed
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt
class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
class TextKeyboardView : KeyboardView, SwipeGesture.Listener, GlideTypingGesture.Listener {
private var computedKeyboard: TextKeyboard? = null
private var iconSet: TextKeyboardIconSet? = null
@ -107,10 +113,18 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
private var initSelectionStart: Int = 0
private var initSelectionEnd: Int = 0
private var isGliding: Boolean = false
private var hasTriggeredGestureMove: Boolean = false
private var shouldBlockNextUp: Boolean = false
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
private val glideTypingDetector = GlideTypingGesture.Detector(context)
private val glideTypingManager: GlideTypingManager
get() = GlideTypingManager.getInstance()
private val glideDataForDrawing: MutableList<GlideTypingGesture.Detector.Position> = mutableListOf()
private val fadingGlide: MutableList<GlideTypingGesture.Detector.Position> = mutableListOf()
private var fadingGlideRadius: Float = 0.0f
val desiredKey: TextKey = TextKey(data = TextKeyData.UNSPECIFIED)
private var keyBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
@ -118,12 +132,13 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
private var backgroundDrawable: PaintDrawable = PaintDrawable()
private val baselineTextSize = resources.getDimension(R.dimen.key_textSize)
var fontSizeMultiplier: Double = 1.0
private set
private val glideTrailPaint: Paint = Paint()
private var labelPaintTextSize: Float = resources.getDimension(R.dimen.key_textSize)
private var labelPaintSpaceTextSize: Float = resources.getDimension(R.dimen.key_textSize)
private var labelPaint: Paint = Paint().apply {
color = 0
private val labelPaint: Paint = Paint().apply {
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
@ -131,8 +146,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
typeface = Typeface.DEFAULT
}
private var hintedLabelPaintTextSize: Float = resources.getDimension(R.dimen.key_textHintSize)
private var hintedLabelPaint: Paint = Paint().apply {
color = 0
private val hintedLabelPaint: Paint = Paint().apply {
isAntiAlias = true
isFakeBoldText = false
textAlign = Paint.Align.CENTER
@ -168,6 +182,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
fun setComputedKeyboard(keyboard: TextKeyboard) {
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW) { keyboard.toString() }
computedKeyboard = keyboard
initGlideClassifier(keyboard)
notifyStateChanged()
}
@ -179,18 +194,49 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
fun notifyStateChanged() {
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW)
isRecomputingRequested = true
swipeGestureDetector.apply {
distanceThreshold = prefs.gestures.swipeDistanceThreshold
velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
if (isMeasured) {
onLayoutInternal()
invalidate()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
glideTypingDetector.let {
it.registerListener(this)
it.registerListener(glideTypingManager)
it.velocityThreshold = prefs.gestures.swipeVelocityThreshold
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
cachedTheme = null
glideTypingDetector.let {
it.unregisterListener(this)
it.unregisterListener(glideTypingManager)
}
}
override fun onTouchEventInternal(event: MotionEvent) {
if (prefs.glide.enabled &&
computedKeyboard?.mode == KeyboardMode.CHARACTERS &&
glideTypingDetector.onTouchEvent(event, initialKey) &&
event.actionMasked != MotionEvent.ACTION_UP
) {
if (activePointerId != null) {
val pointerIndex = event.actionIndex
onTouchCancelInternal(event, pointerIndex, activePointerId!!)
}
isGliding = true
invalidate()
return
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
@ -705,9 +751,10 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
fontSizeMultiplier
)
hintedLabelPaintTextSize = hintedLabelPaint.textSize
glideTypingManager.updateDimensions(measuredWidth.toFloat(), measuredHeight.toFloat())
}
private val baselineTextSize = resources.getDimension(R.dimen.key_textSize)
/**
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
* bounds.
@ -762,6 +809,12 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
paint.color = theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color
}
}
if (theme.getAttr(Theme.Attr.GLIDE_TRAIL_COLOR).toSolidColor().color == 0) {
glideTrailPaint.color = theme.getAttr(Theme.Attr.WINDOW_COLOR_PRIMARY).toSolidColor().color
glideTrailPaint.alpha = 32
} else {
glideTrailPaint.color = theme.getAttr(Theme.Attr.GLIDE_TRAIL_COLOR).toSolidColor().color
}
invalidate()
}
@ -1059,4 +1112,96 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
}
}
}
override fun dispatchDraw(canvas: Canvas?) {
super.dispatchDraw(canvas)
if (prefs.glide.enabled && prefs.glide.showTrail && !isSmartbarKeyboardView) {
val targetDist = 5.0f
val maxPoints = prefs.glide.trailMaxLength
val radius = 20.0f
// the tip of the trail will be 1px
val radiusReductionFactor = (1.0f /radius).pow(1.0f / maxPoints)
if (fadingGlideRadius > 0) {
drawGlideTrail(fadingGlide, maxPoints, targetDist, fadingGlideRadius, canvas, radiusReductionFactor)
}
if (isGliding && glideDataForDrawing.isNotEmpty()) {
drawGlideTrail(glideDataForDrawing, maxPoints, targetDist, radius, canvas, radiusReductionFactor)
}
}
}
private fun drawGlideTrail(
gestureData: MutableList<GlideTypingGesture.Detector.Position>,
maxPoints: Int,
targetDist: Float,
initialRadius: Float,
canvas: Canvas?,
radiusReductionFactor: Float
) {
var radius = initialRadius
var drawnPoints = 0
var prevX = gestureData.lastOrNull()?.x ?: 0.0f
var prevY = gestureData.lastOrNull()?.y ?: 0.0f
outer@ for (i in gestureData.size - 1 downTo 1) {
val dx = prevX - gestureData[i - 1].x
val dy = prevY - gestureData[i - 1].y
val dist = sqrt(dx * dx + dy * dy)
val numPoints = (dist / targetDist).toInt()
for (j in 0 until numPoints) {
if (drawnPoints > maxPoints) break@outer
radius *= radiusReductionFactor
val intermediateX =
gestureData[i].x * (1 - j.toFloat() / numPoints) + gestureData[i - 1].x * (j.toFloat() / numPoints)
val intermediateY =
gestureData[i].y * (1 - j.toFloat() / numPoints) + gestureData[i - 1].y * (j.toFloat() / numPoints)
canvas?.drawCircle(intermediateX, intermediateY, radius,glideTrailPaint)
drawnPoints += 1
prevX = intermediateX
prevY = intermediateY
}
}
}
private fun initGlideClassifier(keyboard: TextKeyboard) {
if (isSmartbarKeyboardView || keyboard.mode != KeyboardMode.CHARACTERS) {
return
}
post {
val keys = keyboard.keys().asSequence().toList()
GlideTypingManager.getInstance().setLayout(keys)
}
}
override fun onGlideAddPoint(point: GlideTypingGesture.Detector.Position) {
if (prefs.glide.enabled) {
glideDataForDrawing.add(point)
}
}
override fun onGlideComplete(data: GlideTypingGesture.Detector.PointerData) {
onGlideCancelled()
}
override fun onGlideCancelled() {
if (prefs.glide.showTrail) {
fadingGlide.clear()
fadingGlide.addAll(glideDataForDrawing)
val animator = ValueAnimator.ofFloat(20.0f, 0.0f)
animator.interpolator = AccelerateInterpolator()
animator.duration = prefs.glide.trailDuration.toLong()
animator.addUpdateListener {
fadingGlideRadius = it.animatedValue as Float
invalidate()
}
animator.start()
glideDataForDrawing.clear()
isGliding = false
invalidate()
}
}
}