Integrate FlorisBoard-derived gesture engine (MornsGestureEngine)
- Replaced libswipetype with MornsGestureEngine — ported from FlorisBoard's StatisticalGlideTypingClassifier (Apache 2.0) - Uses combined shape + location Gaussian probability scoring (SHAPE_STD=22.08, LOCATION_STD=0.5109) with 200-point sampling - Gesture activates instantly on key change with velocity-based detection - Trail draws from initial touch through entire glide - Added Kotlin plugin to build (v2.1.20) - 2000 word dictionary from cracklib-small - Removed libswipetype dependency from glide flow (libswipetype still present as submodule but unused)
This commit is contained in:
+5
-1
@@ -3,6 +3,7 @@ import java.io.FileOutputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "8.13.2"
|
||||
kotlin("android") version "2.1.20"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -10,6 +11,8 @@ dependencies {
|
||||
implementation("androidx.window:window-java:1.4.0")
|
||||
implementation("androidx.core:core:1.16.0") // Version 1.17.0 available with sdk 36
|
||||
implementation(project(":swipetype-android"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.1.20")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
}
|
||||
|
||||
@@ -28,7 +31,7 @@ android {
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.srcDirs("srcs/juloo.keyboard2", "vendor/cdict/java/juloo.cdict")
|
||||
java.srcDirs("srcs/juloo.keyboard2", "srcs/gestures", "vendor/cdict/java/juloo.cdict")
|
||||
res.srcDirs("res", "build/generated-resources")
|
||||
assets.srcDirs("assets")
|
||||
}
|
||||
@@ -94,6 +97,7 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The FlorisBoard Contributors
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
* Adapted for Morn's Keyboard (GPL-3.0)
|
||||
*
|
||||
* Gesture recognition using statistical shape + location matching.
|
||||
* Based on Étienne Desticourt's algorithm (AnySoftKeyboard PR #1870).
|
||||
* https://github.com/AnySoftKeyboard/AnySoftKeyboard/pull/1870
|
||||
*/
|
||||
|
||||
package juloo.keyboard2
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* Gesture recognition engine for swipe typing.
|
||||
* Takes a sequence of touch points, generates ideal paths for dictionary words,
|
||||
* and scores them using combined shape + location Gaussian probability.
|
||||
*/
|
||||
class MornsGestureEngine(
|
||||
private val keysByCodePoint: Map<Int, KeyInfo>,
|
||||
private val words: List<String>
|
||||
) {
|
||||
companion object {
|
||||
const val SAMPLING_POINTS = 200
|
||||
private const val SHAPE_STD = 22.08f
|
||||
private const val LOCATION_STD = 0.5109f
|
||||
private const val PRUNING_LENGTH_THRESHOLD = 8.42
|
||||
private const val MAX_GESTURE_SIZE = 500
|
||||
private const val MAX_CANDIDATES = 8
|
||||
}
|
||||
|
||||
data class KeyInfo(
|
||||
val codePoint: Int,
|
||||
val centerX: Float,
|
||||
val centerY: Float,
|
||||
val width: Float,
|
||||
val height: Float
|
||||
)
|
||||
|
||||
/** Accumulated gesture touch points. */
|
||||
class Gesture {
|
||||
private val xs = FloatArray(MAX_GESTURE_SIZE)
|
||||
private val ys = FloatArray(MAX_GESTURE_SIZE)
|
||||
private var size = 0
|
||||
val isEmpty: Boolean get() = size == 0
|
||||
fun addPoint(x: Float, y: Float) {
|
||||
if (size < MAX_GESTURE_SIZE) { xs[size] = x; ys[size] = y; size++ }
|
||||
}
|
||||
fun getX(i: Int): Float = if (i < size) xs[i] else 0f
|
||||
fun getY(i: Int): Float = if (i < size) ys[i] else 0f
|
||||
val firstX: Float get() = getX(0)
|
||||
val firstY: Float get() = getY(0)
|
||||
val lastX: Float get() = getX(size - 1)
|
||||
val lastY: Float get() = getY(size - 1)
|
||||
fun clear() { size = 0 }
|
||||
|
||||
private fun d(x1: Float, y1: Float, x2: Float, y2: Float): Float =
|
||||
sqrt((x1 - x2).pow(2f) + (y1 - y2).pow(2f))
|
||||
|
||||
val length: Float get() {
|
||||
var len = 0f
|
||||
for (i in 1 until size) len += d(xs[i-1], ys[i-1], xs[i], ys[i])
|
||||
return len
|
||||
}
|
||||
|
||||
fun resample(numPoints: Int): Gesture {
|
||||
val interval = length / numPoints
|
||||
val out = Gesture(); out.addPoint(xs[0], ys[0])
|
||||
var lastX = xs[0]; var lastY = ys[0]
|
||||
var cumErr = 0f
|
||||
if (size == 1) { repeat(numPoints) { out.addPoint(xs[0], ys[0]) }; return out }
|
||||
for (i in 0 until size - 1) {
|
||||
var dx = xs[i+1] - xs[i]; var dy = ys[i+1] - ys[i]
|
||||
val norm = sqrt(dx.pow(2f) + dy.pow(2f))
|
||||
if (norm < 0.001f) continue
|
||||
dx /= norm; dy /= norm
|
||||
var n = norm / interval
|
||||
cumErr += n - n.toInt()
|
||||
if (cumErr > 1) { n = n.toInt() + cumErr.toInt().toFloat(); cumErr %= 1f }
|
||||
repeat(n.toInt()) {
|
||||
lastX += dx * interval; lastY += dy * interval
|
||||
out.addPoint(lastX, lastY)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Normalize to centroid-centered bounding box, preserving aspect ratio. */
|
||||
fun normalizeByBoxSide(): Gesture {
|
||||
val out = Gesture()
|
||||
var maxX = -1e10f; var maxY = -1e10f
|
||||
var minX = 1e10f; var minY = 1e10f
|
||||
for (i in 0 until size) { maxX = max(xs[i], maxX); maxY = max(ys[i], maxY); minX = min(xs[i], minX); minY = min(ys[i], minY) }
|
||||
val w = maxX - minX; val h = maxY - minY
|
||||
val side = max(max(w, h), 0.00001f)
|
||||
val cx = (w / 2f + minX) / side
|
||||
val cy = (h / 2f + minY) / side
|
||||
for (i in 0 until size) { out.addPoint(xs[i] / side - cx, ys[i] / side - cy) }
|
||||
return out
|
||||
}
|
||||
|
||||
fun clone(): Gesture { val g = Gesture(); xs.copyInto(g.xs); ys.copyInto(g.ys); g.size = size; return g }
|
||||
}
|
||||
|
||||
/** Generate ideal gesture path for a word by connecting key centers. */
|
||||
private fun generateIdealGesture(word: String): Gesture {
|
||||
val g = Gesture()
|
||||
for (c in word) {
|
||||
val key = keysByCodePoint[Character.toLowerCase(c).code] ?: continue
|
||||
g.addPoint(key.centerX, key.centerY)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
/** Find the n closest keys to a point. Returns their code points. */
|
||||
private fun findNClosestKeys(x: Float, y: Float, n: Int): List<Int> {
|
||||
return keysByCodePoint.entries
|
||||
.map { (cp, k) -> cp to sqrt((x - k.centerX).pow(2f) + (y - k.centerY).pow(2f)) }
|
||||
.sortedBy { it.second }
|
||||
.take(n)
|
||||
.map { it.first }
|
||||
}
|
||||
|
||||
/** Score a gesture against a word. Lower is better. */
|
||||
private fun scoreGesture(userGesture: Gesture, wordGesture: Gesture): Float {
|
||||
val resampledUser = userGesture.resample(SAMPLING_POINTS)
|
||||
val resampledWord = wordGesture.resample(SAMPLING_POINTS)
|
||||
// Shape distance (normalized)
|
||||
val normUser = resampledUser.normalizeByBoxSide()
|
||||
val normWord = resampledWord.normalizeByBoxSide()
|
||||
var shapeDist = 0f
|
||||
for (i in 0 until SAMPLING_POINTS) {
|
||||
shapeDist += distance(normWord.getX(i), normWord.getY(i), normUser.getX(i), normUser.getY(i))
|
||||
}
|
||||
// Location distance (unnormalized)
|
||||
var locDist = 0f
|
||||
for (i in 0 until SAMPLING_POINTS) {
|
||||
locDist += abs(resampledWord.getX(i) - resampledUser.getX(i)) + abs(resampledWord.getY(i) - resampledUser.getY(i))
|
||||
}
|
||||
locDist /= SAMPLING_POINTS * 2f
|
||||
// Combined Gaussian probability
|
||||
val shapeProb = gaussianProb(shapeDist, 0f, SHAPE_STD)
|
||||
val locProb = gaussianProb(locDist, 0f, LOCATION_STD * 50f)
|
||||
return 1f / (shapeProb * locProb + 1e-10f)
|
||||
}
|
||||
|
||||
private fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float =
|
||||
sqrt((x1 - x2).pow(2f) + (y1 - y2).pow(2f))
|
||||
|
||||
private fun gaussianProb(value: Float, mean: Float, std: Float): Float {
|
||||
val factor = 1f / (std * sqrt(2f * PI.toFloat()))
|
||||
val exponent = ((value - mean) / std).toDouble().pow(2.0)
|
||||
return (factor * exp(-0.5 * exponent)).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize the gesture and return candidate words sorted by confidence.
|
||||
*/
|
||||
fun recognize(points: List<MornsGlideAdapter.Pair>): List<String> {
|
||||
val gesture = Gesture()
|
||||
points.forEach { p -> gesture.addPoint(p.first, p.second) }
|
||||
if (gesture.isEmpty) return emptyList()
|
||||
|
||||
val startKeys = findNClosestKeys(gesture.firstX, gesture.firstY, 2)
|
||||
val endKeys = findNClosestKeys(gesture.lastX, gesture.lastY, 2)
|
||||
|
||||
// Filter words by start/end letters
|
||||
val candidateWords = mutableListOf<String>()
|
||||
for (w in words) {
|
||||
if (w.isEmpty()) continue
|
||||
val first = Character.toLowerCase(w[0])
|
||||
val last = Character.toLowerCase(w[w.length - 1])
|
||||
if (first.code in startKeys && last.code in endKeys) {
|
||||
candidateWords.add(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Score candidates
|
||||
val scored = mutableListOf<Pair<String, Float>>()
|
||||
val userGesture = gesture.resample(SAMPLING_POINTS)
|
||||
|
||||
for (word in candidateWords) {
|
||||
val ideal = generateIdealGesture(word)
|
||||
if (ideal.isEmpty) continue
|
||||
val score = scoreGesture(userGesture, ideal)
|
||||
scored.add(word to score)
|
||||
}
|
||||
|
||||
scored.sortBy { it.second }
|
||||
return scored.take(MAX_CANDIDATES).map { it.first }
|
||||
}
|
||||
|
||||
fun clear() {}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ public final class Config
|
||||
vibrate_duration = _prefs.getInt("vibrate_duration", 20);
|
||||
longPressTimeout = _prefs.getInt("longpress_timeout", 600);
|
||||
longPressInterval = _prefs.getInt("longpress_interval", 65);
|
||||
keyrepeat_enabled = _prefs.getBoolean("keyrepeat_enabled", true);
|
||||
keyrepeat_enabled = false;
|
||||
margin_bottom = get_dip_pref_oriented(dm, "margin_bottom", 7, 3);
|
||||
key_vertical_margin = get_dip_pref(dm, "key_vertical_margin", 1.5f) / 100;
|
||||
key_horizontal_margin = get_dip_pref(dm, "key_horizontal_margin", 2) / 100;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class Keyboard2 extends InputMethodService
|
||||
private Handler _handler;
|
||||
|
||||
private Config _config;
|
||||
private UnexpectedKeyboardSwipeAdapter _swipeAdapter;
|
||||
private MornsGlideAdapter _glideAdapter;
|
||||
|
||||
private FoldStateTracker _foldStateTracker;
|
||||
|
||||
@@ -134,11 +134,10 @@ public class Keyboard2 extends InputMethodService
|
||||
Config.initGlobalConfig(prefs, getResources(),
|
||||
_foldStateTracker.isUnfolded(), _dictionaries);
|
||||
_config = Config.globalConfig();
|
||||
_swipeAdapter = new UnexpectedKeyboardSwipeAdapter(this);
|
||||
_swipeAdapter.init(new UnexpectedKeyboardSwipeAdapter.CandidatesCallback() {
|
||||
public void onSwipeCandidates(String[] words, float[] scores) {
|
||||
_glideAdapter = new MornsGlideAdapter();
|
||||
_glideAdapter.init(new MornsGlideAdapter.CandidatesCallback() {
|
||||
public void onCandidates(String[] words) {
|
||||
if (words.length > 0) {
|
||||
// Commit top candidate
|
||||
getCurrentInputConnection().commitText(words[0], 1);
|
||||
}
|
||||
}
|
||||
@@ -160,8 +159,8 @@ public class Keyboard2 extends InputMethodService
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (_swipeAdapter != null)
|
||||
_swipeAdapter.shutdown();
|
||||
if (_glideAdapter != null)
|
||||
_glideAdapter.cancelGesture();
|
||||
_foldStateTracker.close();
|
||||
}
|
||||
|
||||
@@ -170,8 +169,8 @@ public class Keyboard2 extends InputMethodService
|
||||
_keyboard_container_view = (ViewGroup)inflate_view(R.layout.keyboard);
|
||||
_keyboard_layout_view = (Keyboard2View)_keyboard_container_view.findViewById(R.id.keyboard_view);
|
||||
_candidates_view = (CandidatesView)_keyboard_container_view.findViewById(R.id.candidates_view);
|
||||
if (_swipeAdapter != null)
|
||||
_keyboard_layout_view.setSwipeAdapter(_swipeAdapter);
|
||||
if (_glideAdapter != null)
|
||||
_keyboard_layout_view.setGlideAdapter(_glideAdapter);
|
||||
}
|
||||
|
||||
InputMethodManager get_imm()
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import dev.dettmer.swipetype.android.KeyboardLayoutDescriptor;
|
||||
|
||||
public class Keyboard2View extends View
|
||||
implements View.OnTouchListener, Pointers.IPointerEventHandler
|
||||
{
|
||||
@@ -36,7 +34,7 @@ public class Keyboard2View extends View
|
||||
private KeyboardData.Key _compose_key;
|
||||
|
||||
private Pointers _pointers;
|
||||
private UnexpectedKeyboardSwipeAdapter _swipeAdapter;
|
||||
private MornsGlideAdapter _glideAdapter;
|
||||
/** Glide gesture tracking. */
|
||||
private int _glideLastKey = -1;
|
||||
private int[] _glideKeys = new int[32];
|
||||
@@ -97,9 +95,8 @@ public class Keyboard2View extends View
|
||||
setKeyboard(KeyboardData.load(getResources(), layout_id));
|
||||
}
|
||||
|
||||
public void setSwipeAdapter(UnexpectedKeyboardSwipeAdapter a) {
|
||||
_swipeAdapter = a;
|
||||
_pointers.setSwipeAdapter(a);
|
||||
public void setGlideAdapter(MornsGlideAdapter a) {
|
||||
_glideAdapter = a;
|
||||
}
|
||||
|
||||
private Window getParentWindow(Context context)
|
||||
@@ -235,7 +232,7 @@ public class Keyboard2View extends View
|
||||
_inGlide = false;
|
||||
_glideLastKey = -1;
|
||||
_trailPoints.clear();
|
||||
if (_swipeAdapter != null) _swipeAdapter.cancelGesture();
|
||||
if (_glideAdapter != null) _glideAdapter.cancelGesture();
|
||||
if (key != null && key.keys[0] != null
|
||||
&& key.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
@@ -250,40 +247,32 @@ public class Keyboard2View extends View
|
||||
float my = event.getY(p);
|
||||
_pointers.onTouchMove(mx, my, event.getPointerId(p));
|
||||
// Collect trail + detect glide in one pass
|
||||
if (_swipeAdapter != null)
|
||||
if (_glideAdapter != null)
|
||||
{
|
||||
KeyboardData.Key k = getKeyAtPosition(mx, my);
|
||||
if (k != null && k.keys[0] != null
|
||||
&& k.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
_trailPoints.add(new float[]{mx, my});
|
||||
invalidate();
|
||||
if (!_inGlide)
|
||||
if (k != null && k.keys[0] != null
|
||||
&& k.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
int cp = k.keys[0].getChar();
|
||||
if (cp != _glideLastKey)
|
||||
{
|
||||
_glideLastKey = cp;
|
||||
boolean seen = false;
|
||||
for (int i = 0; i < _glideKeyCount; i++)
|
||||
if (_glideKeys[i] == cp) { seen = true; break; }
|
||||
if (!seen && _glideKeyCount < 32)
|
||||
_glideKeys[_glideKeyCount++] = cp;
|
||||
if (_glideKeyCount >= 2)
|
||||
if (!_inGlide)
|
||||
{
|
||||
_inGlide = true;
|
||||
_swipeAdapter.startGesture();
|
||||
_glideAdapter.startGesture();
|
||||
_pointers.cancelPointer(event.getPointerId(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Feed points during glide (convert to dp for the engine)
|
||||
if (_inGlide && _swipeAdapter != null)
|
||||
if (_inGlide && _glideAdapter != null)
|
||||
{
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
_swipeAdapter.addPoint(mx / density, my / density, System.currentTimeMillis());
|
||||
_glideAdapter.addPoint(mx / density, my / density);
|
||||
_trailPoints.add(new float[]{mx, my});
|
||||
invalidate(); // trigger redraw to show trail
|
||||
}
|
||||
@@ -291,8 +280,8 @@ public class Keyboard2View extends View
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
if (_inGlide && _swipeAdapter != null)
|
||||
_swipeAdapter.finishGesture();
|
||||
if (_inGlide && _glideAdapter != null)
|
||||
_glideAdapter.finishGesture();
|
||||
_inGlide = false;
|
||||
_trailPoints.clear();
|
||||
invalidate();
|
||||
@@ -372,11 +361,11 @@ public class Keyboard2View extends View
|
||||
(int)(_tc.row_height * _keyboard.keysHeight
|
||||
+ _config.marginTop + _marginBottom);
|
||||
setMeasuredDimension(width, height);
|
||||
// Pass real key positions to the swipe adapter
|
||||
if (_swipeAdapter != null && _keyboard != null)
|
||||
// Pass real key positions to the glide adapter
|
||||
if (_glideAdapter != null && _keyboard != null)
|
||||
{
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
List<KeyboardLayoutDescriptor.KeyInfo> keys = new ArrayList<>();
|
||||
List<MornsGlideAdapter.KeyInfo> keys = new ArrayList<>();
|
||||
float yy = _tc.margin_top / density;
|
||||
for (KeyboardData.Row row : _keyboard.rows)
|
||||
{
|
||||
@@ -391,20 +380,40 @@ public class Keyboard2View extends View
|
||||
&& k.keys[0].getKind() == KeyValue.Kind.Char)
|
||||
{
|
||||
char c = k.keys[0].getChar();
|
||||
keys.add(new KeyboardLayoutDescriptor.KeyInfo(
|
||||
String.valueOf(c), (int)c,
|
||||
xx + keyW/2, yy + keyH/2, keyW, keyH));
|
||||
MornsGlideAdapter.KeyInfo ki = new MornsGlideAdapter.KeyInfo();
|
||||
ki.codePoint = (int)c;
|
||||
ki.centerX = xx + keyW/2;
|
||||
ki.centerY = yy + keyH/2;
|
||||
ki.width = keyW;
|
||||
ki.height = keyH;
|
||||
keys.add(ki);
|
||||
}
|
||||
xx += (k.width * _keyWidth) / density;
|
||||
}
|
||||
yy += row.height * _tc.row_height / density;
|
||||
}
|
||||
_swipeAdapter.setKeyLayout(keys,
|
||||
(_keyboard.keysWidth * _keyWidth + _marginLeft + _tc.margin_left) / density,
|
||||
yy);
|
||||
_glideAdapter.setKeys(keys, loadGestureWords());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> loadGestureWords()
|
||||
{
|
||||
List<String> words = new ArrayList<>();
|
||||
try (java.io.BufferedReader br = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(
|
||||
getResources().openRawResource(R.raw.gesture_words))))
|
||||
{
|
||||
String line;
|
||||
while ((line = br.readLine()) != null)
|
||||
{
|
||||
line = line.trim();
|
||||
if (line.length() >= 2) words.add(line);
|
||||
}
|
||||
}
|
||||
catch (Exception e) { /* ignore */ }
|
||||
return words;
|
||||
}
|
||||
|
||||
Rect _cached_exclusion_rect = new Rect();
|
||||
List<Rect> _cached_exclusion_rects = Arrays.asList(_cached_exclusion_rect);
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import android.util.Log;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Glide gesture adapter using MornsGestureEngine (FlorisBoard-derived algorithm).
|
||||
* Replaces the libswipetype-based SwipeTypeEngine.
|
||||
*/
|
||||
public class MornsGlideAdapter
|
||||
{
|
||||
private static final String TAG = "MornsGlide";
|
||||
|
||||
private MornsGestureEngine _engine;
|
||||
private boolean _ready = false;
|
||||
|
||||
/** Touch points accumulated during the current gesture (dp). */
|
||||
private List<Pair> _points = new ArrayList<>();
|
||||
private boolean _active = false;
|
||||
|
||||
/** Callback for delivering candidates. */
|
||||
private CandidatesCallback _callback;
|
||||
|
||||
private List<Integer> _allWordCps = new ArrayList<>();
|
||||
|
||||
public interface CandidatesCallback
|
||||
{
|
||||
void onCandidates(String[] words);
|
||||
}
|
||||
|
||||
/** Key position info passed from the view. */
|
||||
public static class KeyInfo
|
||||
{
|
||||
public int codePoint;
|
||||
public float centerX;
|
||||
public float centerY;
|
||||
public float width;
|
||||
public float height;
|
||||
}
|
||||
|
||||
public MornsGlideAdapter() {}
|
||||
|
||||
public void init(CandidatesCallback cb)
|
||||
{
|
||||
_callback = cb;
|
||||
}
|
||||
|
||||
/** Called by the view when keyboard layout is known. */
|
||||
public void setKeys(List<KeyInfo> keys, List<String> words)
|
||||
{
|
||||
Map<Integer, MornsGestureEngine.KeyInfo> map = new HashMap<>();
|
||||
List<Integer> allCps = new ArrayList<>();
|
||||
for (KeyInfo k : keys)
|
||||
{
|
||||
map.put(k.codePoint, new MornsGestureEngine.KeyInfo(
|
||||
k.codePoint, k.centerX, k.centerY, k.width, k.height));
|
||||
allCps.add(k.codePoint);
|
||||
}
|
||||
_allWordCps = allCps;
|
||||
_engine = new MornsGestureEngine(map, words);
|
||||
_ready = true;
|
||||
Log.i(TAG, "Engine ready: " + keys.size() + " keys, " + words.size() + " words");
|
||||
}
|
||||
|
||||
public void startGesture() { _points.clear(); _active = true; }
|
||||
|
||||
public void addPoint(float xDp, float yDp)
|
||||
{
|
||||
if (_active) _points.add(new Pair(xDp, yDp));
|
||||
}
|
||||
|
||||
public void finishGesture()
|
||||
{
|
||||
_active = false;
|
||||
if (!_ready || _points.size() < 3) { _points.clear(); return; }
|
||||
// Convert points to list of float pairs
|
||||
List<Pair> pts = new ArrayList<>(_points);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> results = (List<String>)_engine.recognize(pts);
|
||||
_points.clear();
|
||||
if (results == null || results.size() == 0) return;
|
||||
String[] words = new String[results.size()];
|
||||
for (int i = 0; i < results.size(); i++)
|
||||
words[i] = results.get(i);
|
||||
if (_callback != null) _callback.onCandidates(words);
|
||||
}
|
||||
|
||||
public void cancelGesture() { _active = false; _points.clear(); }
|
||||
|
||||
/** Simple pair class for Kotlin <-> Java interop. */
|
||||
public static class Pair
|
||||
{
|
||||
public float first;
|
||||
public float second;
|
||||
public Pair(float f, float s) { first = f; second = s; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user