mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-20 03:52:18 +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.core.Subtype
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.Key
|
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
|
* 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.
|
* 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.
|
* Change the word data of the gesture classifier.
|
||||||
@ -38,5 +39,4 @@ interface GlideTypingClassifier {
|
|||||||
fun getSuggestions(maxSuggestionCount: Int, gestureCompleted: Boolean): List<String>
|
fun getSuggestions(maxSuggestionCount: Int, gestureCompleted: Boolean): List<String>
|
||||||
|
|
||||||
fun clear()
|
fun clear()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
import dev.patrickgold.florisboard.ime.text.key.KeyCode
|
||||||
|
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ class GlideTypingGesture {
|
|||||||
* Method which evaluates if a given [event] is a gesture.
|
* Method which evaluates if a given [event] is a gesture.
|
||||||
* @return whether or not the event was interpreted as part of 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) {
|
when (event.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN,
|
MotionEvent.ACTION_DOWN,
|
||||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
@ -69,12 +70,12 @@ class GlideTypingGesture {
|
|||||||
// evaluate whether is actually a gesture
|
// evaluate whether is actually a gesture
|
||||||
val dist = pointerData.positions[0].dist(pos)
|
val dist = pointerData.positions[0].dist(pos)
|
||||||
val time = (System.currentTimeMillis() - pointerData.startTime) + 1
|
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
|
pointerData.isActuallyGesture = true
|
||||||
// Let listener know all those points need to be added.
|
// Let listener know all those points need to be added.
|
||||||
pointerData.positions.take(pointerData.positions.size - 1).forEach { point ->
|
pointerData.positions.take(pointerData.positions.size - 1).forEach { point ->
|
||||||
listeners.forEach {
|
listeners.forEach {
|
||||||
it.onGestureAdd(point)
|
it.onGlideAddPoint(point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (time > MAX_DETECT_TIME) {
|
} else if (time > MAX_DETECT_TIME) {
|
||||||
@ -84,7 +85,7 @@ class GlideTypingGesture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pointerData.isActuallyGesture == true)
|
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
|
return pointerData.isActuallyGesture ?: false
|
||||||
}
|
}
|
||||||
@ -95,14 +96,14 @@ class GlideTypingGesture {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (pointerData.isActuallyGesture == true) {
|
if (pointerData.isActuallyGesture == true) {
|
||||||
listeners.forEach { listener -> listener.onGestureComplete(pointerData) }
|
listeners.forEach { listener -> listener.onGlideComplete(pointerData) }
|
||||||
}
|
}
|
||||||
resetState()
|
resetState()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
if (pointerData.isActuallyGesture == true) {
|
if (pointerData.isActuallyGesture == true) {
|
||||||
listeners.forEach { it.onGestureCancelled() }
|
listeners.forEach { it.onGlideCancelled() }
|
||||||
}
|
}
|
||||||
resetState()
|
resetState()
|
||||||
}
|
}
|
||||||
@ -112,7 +113,11 @@ class GlideTypingGesture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun registerListener(listener: Listener) {
|
fun registerListener(listener: Listener) {
|
||||||
this.listeners.add(listener)
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterListener(listener: Listener) {
|
||||||
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetState() {
|
private fun resetState() {
|
||||||
@ -145,18 +150,17 @@ class GlideTypingGesture {
|
|||||||
/**
|
/**
|
||||||
* Called when a gesture is complete.
|
* 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.
|
* Called when a point is added to a gesture.
|
||||||
* Will not be called before a series of events is detected as 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
|
* 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.AssetManager
|
||||||
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
import dev.patrickgold.florisboard.ime.extension.AssetRef
|
||||||
import dev.patrickgold.florisboard.ime.extension.AssetSource
|
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.TextInputManager
|
||||||
|
import dev.patrickgold.florisboard.ime.text.keyboard.TextKey
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ import org.json.JSONObject
|
|||||||
class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainScope() {
|
class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainScope() {
|
||||||
|
|
||||||
private var glideTypingClassifier = StatisticalGlideTypingClassifier()
|
private var glideTypingClassifier = StatisticalGlideTypingClassifier()
|
||||||
private val initialDimensions: HashMap<Subtype, Dimensions> = hashMapOf()
|
|
||||||
private var currentDimensions: Dimensions = Dimensions(0f, 0f)
|
private var currentDimensions: Dimensions = Dimensions(0f, 0f)
|
||||||
private lateinit var prefHelper: PrefHelper
|
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) {
|
updateSuggestionsAsync(MAX_SUGGESTION_COUNT, true) {
|
||||||
glideTypingClassifier.clear()
|
glideTypingClassifier.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGestureCancelled() {
|
override fun onGlideCancelled() {
|
||||||
glideTypingClassifier.clear()
|
glideTypingClassifier.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastTime = System.currentTimeMillis()
|
private var lastTime = System.currentTimeMillis()
|
||||||
override fun onGestureAdd(point: GlideTypingGesture.Detector.Position) {
|
override fun onGlideAddPoint(point: GlideTypingGesture.Detector.Position) {
|
||||||
val normalized = GlideTypingGesture.Detector.Position(normalizeX(point.x), normalizeY(point.y))
|
val normalized = GlideTypingGesture.Detector.Position(point.x, point.y)
|
||||||
|
|
||||||
this.glideTypingClassifier.addGesturePoint(normalized)
|
this.glideTypingClassifier.addGesturePoint(normalized)
|
||||||
|
|
||||||
@ -61,11 +60,8 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
|
|||||||
/**
|
/**
|
||||||
* Change the layout of the internal gesture classifier
|
* 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)
|
glideTypingClassifier.setLayout(keys, FlorisBoard.getInstance().activeSubtype)
|
||||||
initialDimensions.getOrPut(FlorisBoard.getInstance().activeSubtype, {
|
|
||||||
dimensions
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val wordDataCache = hashMapOf<String, Int>()
|
private val wordDataCache = hashMapOf<String, Int>()
|
||||||
@ -85,46 +81,11 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDimensions(dimensions: Dimensions) {
|
fun updateDimensions(newWidth: Float, newHeight: Float) {
|
||||||
this.currentDimensions = dimensions
|
currentDimensions.apply {
|
||||||
}
|
width = newWidth
|
||||||
|
height = newHeight
|
||||||
/**
|
}
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,6 +125,6 @@ class GlideTypingManager : GlideTypingGesture.Listener, CoroutineScope by MainSc
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Dimensions(
|
data class Dimensions(
|
||||||
val width: Float,
|
var width: Float,
|
||||||
val height: Float
|
var height: Float
|
||||||
)
|
)
|
||||||
|
@ -2,8 +2,11 @@ package dev.patrickgold.florisboard.ime.text.gestures
|
|||||||
|
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
|
import androidx.core.util.set
|
||||||
import dev.patrickgold.florisboard.ime.core.Subtype
|
import dev.patrickgold.florisboard.ime.core.Subtype
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.Key
|
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.text.Normalizer
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
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
|
* Check out Étienne Desticourt's excellent write up at https://github.com/AnySoftKeyboard/AnySoftKeyboard/pull/1870
|
||||||
*/
|
*/
|
||||||
class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
||||||
|
|
||||||
private val gesture = Gesture()
|
private val gesture = Gesture()
|
||||||
private var keysByCharacter: SparseArray<Key> = SparseArray()
|
private var keysByCharacter: SparseArray<TextKey> = SparseArray()
|
||||||
private var words: Set<String> = setOf()
|
private var words: Set<String> = setOf()
|
||||||
private var wordFrequencies: Map<String, Int> = hashMapOf()
|
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 lateinit var pruner: Pruner
|
||||||
private var wordDataSubtype: Subtype? = null
|
private var wordDataSubtype: Subtype? = null
|
||||||
private var layoutSubtype: Subtype? = null
|
private var layoutSubtype: Subtype? = null
|
||||||
@ -33,7 +35,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
*/
|
*/
|
||||||
private var distanceThresholdSquared = 0
|
private var distanceThresholdSquared = 0
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Describes the allowed length variance in a gesture. If a gesture is too long or too short, it is immediately
|
* 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
|
// stop duplicate calls
|
||||||
if (this.layoutSubtype == subtype) {
|
if (layoutSubtype == subtype) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keysByCharacter.clear()
|
keysByCharacter.clear()
|
||||||
keys.clear()
|
keys.clear()
|
||||||
/*keyViews.forEach {
|
keyViews.forEach {
|
||||||
keysByCharacter[it.data.code] = it
|
keysByCharacter[it.computedData.code] = it
|
||||||
this.keys.add(it)
|
keys.add(it)
|
||||||
}
|
}
|
||||||
layoutSubtype = subtype
|
layoutSubtype = subtype
|
||||||
distanceThresholdSquared = (keyViews.first().width / 4)*/
|
distanceThresholdSquared = (keyViews.first().visibleBounds.width() / 4)
|
||||||
distanceThresholdSquared *= distanceThresholdSquared
|
distanceThresholdSquared *= distanceThresholdSquared
|
||||||
initializePruner()
|
initializePruner()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
|
override fun setWordData(words: HashMap<String, Int>, subtype: Subtype) {
|
||||||
// stop duplicate calls..
|
// stop duplicate calls..
|
||||||
if (this.wordDataSubtype == subtype) {
|
if (wordDataSubtype == subtype) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +165,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
val candidates = arrayListOf<String>()
|
val candidates = arrayListOf<String>()
|
||||||
val candidateWeights = arrayListOf<Float>()
|
val candidateWeights = arrayListOf<Float>()
|
||||||
val key = keys.firstOrNull() ?: return listOf()
|
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)
|
var remainingWords = pruner.pruneByExtremities(gesture, this.keys)
|
||||||
val userGesture = gesture.resample(SAMPLING_POINTS)
|
val userGesture = gesture.resample(SAMPLING_POINTS)
|
||||||
val normalizedUserGesture: Gesture = userGesture.normalizeByBoxSide()
|
val normalizedUserGesture: Gesture = userGesture.normalizeByBoxSide()
|
||||||
@ -213,7 +214,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
this.gesture.clear()
|
gesture.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcLocationDistance(gesture1: Gesture, gesture2: Gesture): Float {
|
private fun calcLocationDistance(gesture1: Gesture, gesture2: Gesture): Float {
|
||||||
@ -227,7 +228,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
totalDistance += distance
|
totalDistance += distance
|
||||||
}
|
}
|
||||||
return totalDistance / SAMPLING_POINTS / 2
|
return totalDistance / SAMPLING_POINTS / 2
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcGaussianProbability(value: Float, mean: Float, standardDeviation: Float): Float {
|
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
|
* The length difference between a user gesture and a word gesture above which a word will
|
||||||
* be pruned.
|
* 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. */
|
/** A tree that provides fast access to words based on their first and last letter. */
|
||||||
@ -303,13 +303,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
fun pruneByLength(
|
fun pruneByLength(
|
||||||
userGesture: Gesture,
|
userGesture: Gesture,
|
||||||
words: ArrayList<String>,
|
words: ArrayList<String>,
|
||||||
keysByCharacter: SparseArray<Key>,
|
keysByCharacter: SparseArray<TextKey>,
|
||||||
keys: List<Key>
|
keys: List<TextKey>
|
||||||
): ArrayList<String> {
|
): ArrayList<String> {
|
||||||
val remainingWords = ArrayList<String>()
|
val remainingWords = ArrayList<String>()
|
||||||
|
|
||||||
val key = keys.firstOrNull() ?: return arrayListOf()
|
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()
|
val userLength = userGesture.getLength()
|
||||||
for (word in words) {
|
for (word in words) {
|
||||||
val idealGestures = Gesture.generateIdealGestures(word, keysByCharacter)
|
val idealGestures = Gesture.generateIdealGestures(word, keysByCharacter)
|
||||||
@ -325,21 +325,20 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun getFirstKeyLastKey(
|
private fun getFirstKeyLastKey(
|
||||||
word: String, keysByCharacter: SparseArray<Key>
|
word: String, keysByCharacter: SparseArray<TextKey>
|
||||||
): Pair<Int, Int>? {
|
): Pair<Int, Int>? {
|
||||||
val firstLetter = word[0]
|
val firstLetter = word[0]
|
||||||
val lastLetter = word[word.length - 1]
|
val lastLetter = word[word.length - 1]
|
||||||
val firstBaseChar = Normalizer.normalize(firstLetter.toString(), Normalizer.Form.NFD)[0]
|
val firstBaseChar = Normalizer.normalize(firstLetter.toString(), Normalizer.Form.NFD)[0]
|
||||||
val lastBaseChar = Normalizer.normalize(lastLetter.toString(), Normalizer.Form.NFD)[0]
|
val lastBaseChar = Normalizer.normalize(lastLetter.toString(), Normalizer.Form.NFD)[0]
|
||||||
return when {
|
return when {
|
||||||
keysByCharacter.indexOfKey(firstBaseChar.toInt()) < 0 || keysByCharacter.indexOfKey(lastBaseChar.toInt()) < 0 -> {
|
keysByCharacter.indexOfKey(firstBaseChar.code) < 0 || keysByCharacter.indexOfKey(lastBaseChar.code) < 0 -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val firstKey = keysByCharacter[firstBaseChar.toInt()]
|
val firstKey = keysByCharacter[firstBaseChar.code]
|
||||||
val lastKey = keysByCharacter[lastBaseChar.toInt()]
|
val lastKey = keysByCharacter[lastBaseChar.code]
|
||||||
//Pair(firstKey.data.code, lastKey.data.code)
|
Pair(firstKey.computedData.code, lastKey.computedData.code)
|
||||||
Pair(0, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -357,14 +356,13 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
x: Float, y: Float, n: Int, keys: Iterable<Key>
|
x: Float, y: Float, n: Int, keys: Iterable<Key>
|
||||||
): Iterable<Int> {
|
): Iterable<Int> {
|
||||||
val keyDistances = HashMap<Key, Float>()
|
val keyDistances = HashMap<Key, Float>()
|
||||||
/*for (key in keys) {
|
for (key in keys) {
|
||||||
val distance = Gesture.distance(key.centerX, key.centerY, x, y)
|
val distance = Gesture.distance(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat(), x, y)
|
||||||
keyDistances[key] = distance
|
keyDistances[key] = distance
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyDistances.entries.sortedWith { c1, c2 -> c1.value.compareTo(c2.value) }.take(n)
|
return keyDistances.entries.sortedWith { c1, c2 -> c1.value.compareTo(c2.value) }.take(n)
|
||||||
.map { it.key.data.code }*/
|
.map { (it.key as? TextKey)?.computedData?.code ?: KeyCode.UNSPECIFIED }
|
||||||
return listOf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,7 +385,7 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val MAX_SIZE = 300
|
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 idealGesture = Gesture()
|
||||||
val idealGestureWithLoops = Gesture()
|
val idealGestureWithLoops = Gesture()
|
||||||
var previousLetter = '\u0000'
|
var previousLetter = '\u0000'
|
||||||
@ -396,11 +394,11 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
// Add points for each key
|
// Add points for each key
|
||||||
for (c in word) {
|
for (c in word) {
|
||||||
val lc = Character.toLowerCase(c)
|
val lc = Character.toLowerCase(c)
|
||||||
var key = keysByCharacter[lc.toInt()]
|
var key = keysByCharacter[lc.code]
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
// Try finding the base character instead, e.g., the "e" key instead of "é"
|
// Try finding the base character instead, e.g., the "e" key instead of "é"
|
||||||
val baseCharacter: Char = Normalizer.normalize(lc.toString(), Normalizer.Form.NFD)[0]
|
val baseCharacter: Char = Normalizer.normalize(lc.toString(), Normalizer.Form.NFD)[0]
|
||||||
key = keysByCharacter[baseCharacter.toInt()]
|
key = keysByCharacter[baseCharacter.code]
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -408,30 +406,30 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
|
|
||||||
// We adda little loop on the key for duplicate letters
|
// We adda little loop on the key for duplicate letters
|
||||||
// so that we can differentiate words like pool and poll, lull and lul, etc...
|
// so that we can differentiate words like pool and poll, lull and lul, etc...
|
||||||
/*if (previousLetter == lc) {
|
if (previousLetter == lc) {
|
||||||
// bottom right
|
// bottom right
|
||||||
idealGestureWithLoops.addPoint(
|
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
|
// top right
|
||||||
idealGestureWithLoops.addPoint(
|
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
|
// top left
|
||||||
idealGestureWithLoops.addPoint(
|
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
|
// bottom left
|
||||||
idealGestureWithLoops.addPoint(
|
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
|
hasLoops = true
|
||||||
|
|
||||||
idealGesture.addPoint(key.centerX, key.centerY)
|
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
|
||||||
} else {
|
} else {
|
||||||
idealGesture.addPoint(key.centerX, key.centerY)
|
idealGesture.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
|
||||||
idealGestureWithLoops.addPoint(key.centerX, key.centerY)
|
idealGestureWithLoops.addPoint(key.visibleBounds.centerX().toFloat(), key.visibleBounds.centerY().toFloat())
|
||||||
}*/
|
}
|
||||||
previousLetter = lc
|
previousLetter = lc
|
||||||
}
|
}
|
||||||
return when (hasLoops) {
|
return when (hasLoops) {
|
||||||
@ -443,7 +441,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
|
fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
|
||||||
return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2))
|
return sqrt((x1 - x2).pow(2) + (y1 - y2).pow(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addPoint(x: Float, y: Float) {
|
fun addPoint(x: Float, y: Float) {
|
||||||
@ -455,7 +452,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
size += 1
|
size += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resamples the gesture into a new gesture with the chosen number of points by oversampling
|
* Resamples the gesture into a new gesture with the chosen number of points by oversampling
|
||||||
* it.
|
* it.
|
||||||
@ -481,7 +477,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (i in 0 until size - 1) {
|
for (i in 0 until size - 1) {
|
||||||
// We calculate the unit vector from the two points we're between in the actual
|
// We calculate the unit vector from the two points we're between in the actual
|
||||||
// gesture
|
// gesture
|
||||||
@ -515,7 +510,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun normalizeByBoxSide(): Gesture {
|
fun normalizeByBoxSide(): Gesture {
|
||||||
|
|
||||||
val normalizedGesture = Gesture()
|
val normalizedGesture = Gesture()
|
||||||
|
|
||||||
var maxX = -1.0f
|
var maxX = -1.0f
|
||||||
@ -575,7 +569,6 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
return Gesture(xs.clone(), ys.clone(), size)
|
return Gesture(xs.clone(), ys.clone(), size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@ -598,6 +591,4 @@ class StatisticalGlideTypingClassifier : GlideTypingClassifier {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package dev.patrickgold.florisboard.ime.text.keyboard
|
package dev.patrickgold.florisboard.ime.text.keyboard
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
@ -23,11 +24,14 @@ import android.graphics.drawable.PaintDrawable
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
import dev.patrickgold.florisboard.R
|
import dev.patrickgold.florisboard.R
|
||||||
import dev.patrickgold.florisboard.debug.*
|
import dev.patrickgold.florisboard.debug.*
|
||||||
import dev.patrickgold.florisboard.ime.core.*
|
import dev.patrickgold.florisboard.ime.core.*
|
||||||
import dev.patrickgold.florisboard.ime.keyboard.KeyboardView
|
import dev.patrickgold.florisboard.ime.keyboard.KeyboardView
|
||||||
import dev.patrickgold.florisboard.ime.popup.PopupManager
|
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.SwipeAction
|
||||||
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
import dev.patrickgold.florisboard.ime.text.gestures.SwipeGesture
|
||||||
import dev.patrickgold.florisboard.ime.text.key.*
|
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.cancelAll
|
||||||
import dev.patrickgold.florisboard.util.postDelayed
|
import dev.patrickgold.florisboard.util.postDelayed
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
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 computedKeyboard: TextKeyboard? = null
|
||||||
private var iconSet: TextKeyboardIconSet? = null
|
private var iconSet: TextKeyboardIconSet? = null
|
||||||
|
|
||||||
@ -107,10 +113,18 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
|
|
||||||
private var initSelectionStart: Int = 0
|
private var initSelectionStart: Int = 0
|
||||||
private var initSelectionEnd: Int = 0
|
private var initSelectionEnd: Int = 0
|
||||||
|
private var isGliding: Boolean = false
|
||||||
private var hasTriggeredGestureMove: Boolean = false
|
private var hasTriggeredGestureMove: Boolean = false
|
||||||
private var shouldBlockNextUp: Boolean = false
|
private var shouldBlockNextUp: Boolean = false
|
||||||
private val swipeGestureDetector = SwipeGesture.Detector(context, this)
|
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)
|
val desiredKey: TextKey = TextKey(data = TextKeyData.UNSPECIFIED)
|
||||||
|
|
||||||
private var keyBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
private var keyBackgroundDrawable: PaintDrawable = PaintDrawable().apply {
|
||||||
@ -118,12 +132,13 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
private var backgroundDrawable: PaintDrawable = PaintDrawable()
|
||||||
|
private val baselineTextSize = resources.getDimension(R.dimen.key_textSize)
|
||||||
var fontSizeMultiplier: Double = 1.0
|
var fontSizeMultiplier: Double = 1.0
|
||||||
private set
|
private set
|
||||||
|
private val glideTrailPaint: Paint = Paint()
|
||||||
private var labelPaintTextSize: Float = resources.getDimension(R.dimen.key_textSize)
|
private var labelPaintTextSize: Float = resources.getDimension(R.dimen.key_textSize)
|
||||||
private var labelPaintSpaceTextSize: Float = resources.getDimension(R.dimen.key_textSize)
|
private var labelPaintSpaceTextSize: Float = resources.getDimension(R.dimen.key_textSize)
|
||||||
private var labelPaint: Paint = Paint().apply {
|
private val labelPaint: Paint = Paint().apply {
|
||||||
color = 0
|
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
isFakeBoldText = false
|
isFakeBoldText = false
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
@ -131,8 +146,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
}
|
}
|
||||||
private var hintedLabelPaintTextSize: Float = resources.getDimension(R.dimen.key_textHintSize)
|
private var hintedLabelPaintTextSize: Float = resources.getDimension(R.dimen.key_textHintSize)
|
||||||
private var hintedLabelPaint: Paint = Paint().apply {
|
private val hintedLabelPaint: Paint = Paint().apply {
|
||||||
color = 0
|
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
isFakeBoldText = false
|
isFakeBoldText = false
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
@ -168,6 +182,7 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
fun setComputedKeyboard(keyboard: TextKeyboard) {
|
fun setComputedKeyboard(keyboard: TextKeyboard) {
|
||||||
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW) { keyboard.toString() }
|
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW) { keyboard.toString() }
|
||||||
computedKeyboard = keyboard
|
computedKeyboard = keyboard
|
||||||
|
initGlideClassifier(keyboard)
|
||||||
notifyStateChanged()
|
notifyStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,18 +194,49 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
fun notifyStateChanged() {
|
fun notifyStateChanged() {
|
||||||
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW)
|
flogInfo(LogTopic.TEXT_KEYBOARD_VIEW)
|
||||||
isRecomputingRequested = true
|
isRecomputingRequested = true
|
||||||
|
swipeGestureDetector.apply {
|
||||||
|
distanceThreshold = prefs.gestures.swipeDistanceThreshold
|
||||||
|
velocityThreshold = prefs.gestures.swipeVelocityThreshold
|
||||||
|
}
|
||||||
if (isMeasured) {
|
if (isMeasured) {
|
||||||
onLayoutInternal()
|
onLayoutInternal()
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
glideTypingDetector.let {
|
||||||
|
it.registerListener(this)
|
||||||
|
it.registerListener(glideTypingManager)
|
||||||
|
it.velocityThreshold = prefs.gestures.swipeVelocityThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
cachedTheme = null
|
cachedTheme = null
|
||||||
|
glideTypingDetector.let {
|
||||||
|
it.unregisterListener(this)
|
||||||
|
it.unregisterListener(glideTypingManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTouchEventInternal(event: MotionEvent) {
|
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) {
|
when (event.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN,
|
MotionEvent.ACTION_DOWN,
|
||||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
@ -705,9 +751,10 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
fontSizeMultiplier
|
fontSizeMultiplier
|
||||||
)
|
)
|
||||||
hintedLabelPaintTextSize = hintedLabelPaint.textSize
|
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
|
* Automatically sets the text size of [boxPaint] for given [text] so it fits within the given
|
||||||
* bounds.
|
* bounds.
|
||||||
@ -762,6 +809,12 @@ class TextKeyboardView : KeyboardView, SwipeGesture.Listener {
|
|||||||
paint.color = theme.getAttr(Theme.Attr.KEYBOARD_BACKGROUND).toSolidColor().color
|
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()
|
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