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:
parent
d3e8d35e5d
commit
535b48e5b4
@ -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()
|
||||
|
||||
}
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user