From 1e4906c48d565beb913f5fdc5f5fb184d0428234 Mon Sep 17 00:00:00 2001 From: tibbi Date: Sat, 8 Jan 2022 19:13:09 +0100 Subject: [PATCH] adding an initial implementation of MyKeyboard --- .../keyboard/helpers/MyKeyboard.kt | 849 ++++++++++++++++++ .../keyboard/views/MyKeyboardView.kt | 1 + 2 files changed, 850 insertions(+) create mode 100644 app/src/main/kotlin/com/simplemobiletools/keyboard/helpers/MyKeyboard.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/helpers/MyKeyboard.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/helpers/MyKeyboard.kt new file mode 100644 index 0000000..7b9ab61 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/helpers/MyKeyboard.kt @@ -0,0 +1,849 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.simplemobiletools.keyboard.helpers + +import android.content.Context +import android.content.res.Resources +import android.content.res.TypedArray +import android.content.res.XmlResourceParser +import android.graphics.drawable.Drawable +import android.inputmethodservice.Keyboard +import android.text.TextUtils +import android.util.TypedValue +import android.util.Xml +import androidx.annotation.XmlRes +import com.simplemobiletools.keyboard.R +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.util.* + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + * + * The layout file for a keyboard contains XML that looks like the following snippet: + *
+ * <Keyboard
+ * android:keyWidth="%10p"
+ * android:keyHeight="50px"
+ * android:horizontalGap="2px"
+ * android:verticalGap="2px" >
+ * <Row android:keyWidth="32px" >
+ * <Key android:keyLabel="A" />
+ * ...
+ * </Row>
+ * ...
+ * </Keyboard>
+
* + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + */ +class MyKeyboard { + /** Keyboard label */ + private val mLabel: CharSequence? = null + + /** Horizontal gap default for all rows */ + protected var mDefaultHorizontalGap = 0 + + /** Default key width */ + protected var mDefaultWidth = 0 + + /** Default key height */ + protected var mDefaultHeight = 0 + + /** Default gap between rows */ + protected var mDefaultVerticalGap = 0 + + /** Is the keyboard in the shifted state */ + var isShifted = false + private set + + /** Key instance for the shift key, if present */ + private val mShiftKeys = arrayOf(null, null) + /** + * @hide + */ + /** Key index for the shift key, if present */ + val shiftKeyIndices = intArrayOf(-1, -1) + + /** Current key width, while loading the keyboard */ + private val mKeyWidth = 0 + + /** Current key height, while loading the keyboard */ + private val mKeyHeight = 0 + /** + * Returns the total height of the keyboard + * @return the total height of the keyboard + */ + /** Total height of the keyboard, including the padding and keys */ + var height = 0 + private set + + /** + * Total width of the keyboard, including left side gaps and keys, but not any gaps on the + * right side. + */ + var minWidth = 0 + private set + + /** List of keys in this keyboard */ + private var mKeys: MutableList? = null + + /** List of modifier keys such as Shift & Alt, if any */ + private var mModifierKeys: MutableList? = null + + /** Width of the screen available to fit the keyboard */ + private var mDisplayWidth = 0 + + /** Height of the screen */ + private var mDisplayHeight = 0 + + /** Keyboard mode, or zero, if none. */ + private var mKeyboardMode = 0 + private var mCellWidth = 0 + private var mCellHeight = 0 + private var mGridNeighbors = ArrayList() + private var mProximityThreshold = 0 + private val rows = ArrayList() + + companion object { + const val TAG = "Keyboard" + + // Keyboard XML Tags + private const val TAG_KEYBOARD = "Keyboard" + private const val TAG_ROW = "Row" + private const val TAG_KEY = "Key" + const val EDGE_LEFT = 0x01 + const val EDGE_RIGHT = 0x02 + const val EDGE_TOP = 0x04 + const val EDGE_BOTTOM = 0x08 + const val KEYCODE_SHIFT = -1 + const val KEYCODE_MODE_CHANGE = -2 + const val KEYCODE_CANCEL = -3 + const val KEYCODE_DONE = -4 + const val KEYCODE_DELETE = -5 + const val KEYCODE_ALT = -6 + + // Variables for pre-computing nearest keys. + private const val GRID_WIDTH = 10 + private const val GRID_HEIGHT = 5 + private val GRID_SIZE: Int = GRID_WIDTH * GRID_HEIGHT + + /** Number of key widths from current touch point to search for nearest keys. */ + private const val SEARCH_DISTANCE = 1.8f + fun getDimensionOrFraction(a: TypedArray, index: Int, base: Int, defValue: Int): Int { + val value = a.peekValue(index) ?: return defValue + if (value.type == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelOffset(index, defValue) + } else if (value.type == TypedValue.TYPE_FRACTION) { + // Round it to avoid values like 47.9999 from getting truncated + return Math.round(a.getFraction(index, base, base, defValue.toFloat())) + } + return defValue + } + } + + /** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the [Keyboard] + * defines. + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_verticalGap + * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags + * @attr ref android.R.styleable#Keyboard_Row_keyboardMode + */ + class Row { + /** Default width of a key in this row. */ + var defaultWidth = 0 + + /** Default height of a key in this row. */ + var defaultHeight = 0 + + /** Default horizontal gap between keys in this row. */ + var defaultHorizontalGap = 0 + + /** Vertical gap following this row. */ + var verticalGap = 0 + var mKeys = ArrayList() + + /** + * Edge flags for this row of keys. Possible values that can be assigned are + * [EDGE_TOP][Keyboard.EDGE_TOP] and [EDGE_BOTTOM][Keyboard.EDGE_BOTTOM] + */ + var rowEdgeFlags = 0 + + /** The keyboard mode for this row */ + var mode = 0 + var parent: MyKeyboard + + constructor(parent: MyKeyboard) { + this.parent = parent + } + + constructor(res: Resources, parent: MyKeyboard, parser: XmlResourceParser?) { + this.parent = parent + var a = res.obtainAttributes( + Xml.asAttributeSet(parser), + R.styleable.Keyboard + ) + + defaultWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth, parent.mDisplayWidth, parent.mDefaultWidth) + defaultHeight = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight, parent.mDisplayHeight, parent.mDefaultHeight) + defaultHorizontalGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap, parent.mDisplayWidth, parent.mDefaultHorizontalGap) + verticalGap = getDimensionOrFraction(a, R.styleable.Keyboard_verticalGap, parent.mDisplayHeight, parent.mDefaultVerticalGap) + + a.recycle() + a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Row) + rowEdgeFlags = a.getInt(R.styleable.Keyboard_Row_rowEdgeFlags, 0) + mode = a.getResourceId(R.styleable.Keyboard_Row_keyboardMode, 0) + } + } + + /** + * Class for describing the position and characteristics of a single key in the keyboard. + * + * @attr ref android.R.styleable#Keyboard_keyWidth + * @attr ref android.R.styleable#Keyboard_keyHeight + * @attr ref android.R.styleable#Keyboard_horizontalGap + * @attr ref android.R.styleable#Keyboard_Key_codes + * @attr ref android.R.styleable#Keyboard_Key_keyIcon + * @attr ref android.R.styleable#Keyboard_Key_keyLabel + * @attr ref android.R.styleable#Keyboard_Key_iconPreview + * @attr ref android.R.styleable#Keyboard_Key_isSticky + * @attr ref android.R.styleable#Keyboard_Key_isRepeatable + * @attr ref android.R.styleable#Keyboard_Key_isModifier + * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard + * @attr ref android.R.styleable#Keyboard_Key_popupCharacters + * @attr ref android.R.styleable#Keyboard_Key_keyOutputText + * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags + */ + class Key(parent: Row) { + /** + * All the key codes (unicode or custom code) that this key could generate, zero'th + * being the most important. + */ + var codes = ArrayList() + + /** Label to display */ + var label: CharSequence? = null + + /** Icon to display instead of a label. Icon takes precedence over a label */ + var icon: Drawable? = null + + /** Preview version of the icon, for the preview popup */ + var iconPreview: Drawable? = null + + /** Width of the key, not including the gap */ + var width: Int + + /** Height of the key, not including the gap */ + var height: Int + + /** The horizontal gap before this key */ + var gap: Int + + /** Whether this key is sticky, i.e., a toggle key */ + var sticky = false + + /** X coordinate of the key in the keyboard layout */ + var x = 0 + + /** Y coordinate of the key in the keyboard layout */ + var y = 0 + + /** The current pressed state of this key */ + var pressed = false + + /** If this is a sticky key, is it on? */ + var on = false + + /** Text to output when pressed. This can be multiple characters, like ".com" */ + var text: CharSequence? = null + + /** Popup characters */ + var popupCharacters: CharSequence? = null + + /** + * Flags that specify the anchoring to edges of the keyboard for detecting touch events + * that are just out of the boundary of the key. This is a bit mask of + * [Keyboard.EDGE_LEFT], [Keyboard.EDGE_RIGHT], [Keyboard.EDGE_TOP] and + * [Keyboard.EDGE_BOTTOM]. + */ + var edgeFlags: Int + + /** Whether this is a modifier key, such as Shift or Alt */ + var modifier = false + + /** The keyboard that this key belongs to */ + private val keyboard: MyKeyboard + + /** + * If this key pops up a mini keyboard, this is the resource id for the XML layout for that + * keyboard. + */ + var popupResId = 0 + + /** Whether this key repeats itself when held down */ + var repeatable = false + + /** Create a key with the given top-left coordinate and extract its attributes from + * the XML parser. + * @param res resources associated with the caller's context + * @param parent the row that this key belongs to. The row must already be attached to + * a [Keyboard]. + * @param x the x coordinate of the top-left + * @param y the y coordinate of the top-left + * @param parser the XML parser containing the attributes for this key + */ + constructor(res: Resources, parent: Row, x: Int, y: Int, parser: XmlResourceParser?) : this(parent) { + this.x = x + this.y = y + var a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard) + width = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth, keyboard.mDisplayWidth, parent.defaultWidth) + height = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight, keyboard.mDisplayHeight, parent.defaultHeight) + gap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap, keyboard.mDisplayWidth, parent.defaultHorizontalGap) + a.recycle() + a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key) + this.x += gap + val codesValue = TypedValue() + + a.getValue(R.styleable.Keyboard_Key_codes, codesValue) + if (codesValue.type == TypedValue.TYPE_INT_DEC || codesValue.type == TypedValue.TYPE_INT_HEX) { + codes = arrayListOf(codesValue.data) + } else if (codesValue.type == TypedValue.TYPE_STRING) { + codes = parseCSV(codesValue.string.toString()) + } + + iconPreview = a.getDrawable(R.styleable.Keyboard_Key_iconPreview) + if (iconPreview != null) { + iconPreview!!.setBounds( + 0, 0, iconPreview!!.intrinsicWidth, + iconPreview!!.intrinsicHeight + ) + } + popupCharacters = a.getText( + R.styleable.Keyboard_Key_popupCharacters + ) + popupResId = a.getResourceId( + R.styleable.Keyboard_Key_popupKeyboard, 0 + ) + repeatable = a.getBoolean( + R.styleable.Keyboard_Key_isRepeatable, false + ) + modifier = a.getBoolean( + R.styleable.Keyboard_Key_isModifier, false + ) + sticky = a.getBoolean( + R.styleable.Keyboard_Key_isSticky, false + ) + edgeFlags = a.getInt(R.styleable.Keyboard_Key_keyEdgeFlags, 0) + edgeFlags = edgeFlags or parent.rowEdgeFlags + icon = a.getDrawable( + R.styleable.Keyboard_Key_keyIcon + ) + if (icon != null) { + icon!!.setBounds(0, 0, icon!!.intrinsicWidth, icon!!.intrinsicHeight) + } + label = a.getText(R.styleable.Keyboard_Key_keyLabel) + text = a.getText(R.styleable.Keyboard_Key_keyOutputText) + if (!TextUtils.isEmpty(label)) { + codes = arrayListOf(label!![0].toInt()) + } + a.recycle() + } + + /** Create an empty key with no attributes. */ + init { + keyboard = parent.parent + height = parent.defaultHeight + width = parent.defaultWidth + gap = parent.defaultHorizontalGap + edgeFlags = parent.rowEdgeFlags + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see .onReleased + */ + fun onPressed() { + pressed = !pressed + } + + /** + * Changes the pressed state of the key. + * + * + * Toggled state of the key will be flipped when all the following conditions are + * fulfilled: + * + * + * * This is a sticky key, that is, [.sticky] is `true`. + * * The parameter `inside` is `true`. + * * [android.os.Build.VERSION.SDK_INT] is greater than + * [android.os.Build.VERSION_CODES.LOLLIPOP_MR1]. + * + * + * @param inside whether the finger was released inside the key. Works only on Android M and + * later. See the method document for details. + * @see .onPressed + */ + fun onReleased(inside: Boolean) { + pressed = !pressed + if (sticky && inside) { + on = !on + } + } + + fun parseCSV(value: String): ArrayList { + var count = 0 + var lastIndex = 0 + if (value.length > 0) { + count++ + while (value.indexOf(",", lastIndex + 1).also { lastIndex = it } > 0) { + count++ + } + } + + val values = ArrayList(count) + count = 0 + val st = StringTokenizer(value, ",") + while (st.hasMoreTokens()) { + try { + values[count++] = st.nextToken().toInt() + } catch (nfe: NumberFormatException) { + } + } + return values + } + + /** + * Detects if a point falls inside this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls inside the key. If the key is attached to an edge, + * it will assume that all points between the key and the edge are considered to be inside + * the key. + */ + fun isInside(x: Int, y: Int): Boolean { + val leftEdge = edgeFlags and EDGE_LEFT > 0 + val rightEdge = edgeFlags and EDGE_RIGHT > 0 + val topEdge = edgeFlags and EDGE_TOP > 0 + val bottomEdge = edgeFlags and EDGE_BOTTOM > 0 + return ((x >= this.x || leftEdge && x <= this.x + width) + && (x < this.x + width || rightEdge && x >= this.x) + && (y >= this.y || topEdge && y <= this.y + height) + && (y < this.y + height || bottomEdge && y >= this.y)) + } + + /** + * Returns the square of the distance between the center of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the center of the key + */ + fun squaredDistanceFrom(x: Int, y: Int): Int { + val xDist = this.x + width / 2 - x + val yDist = this.y + height / 2 - y + return xDist * xDist + yDist * yDist + } + + /** + * Returns the drawable state for the key, based on the current state and type of the key. + * @return the drawable state of the key. + * @see android.graphics.drawable.StateListDrawable.setState + */ + val currentDrawableState: IntArray + get() { + var states: IntArray = KEY_STATE_NORMAL + if (on) { + states = if (pressed) { + KEY_STATE_PRESSED_ON + } else { + KEY_STATE_NORMAL_ON + } + } else { + if (sticky) { + states = if (pressed) { + KEY_STATE_PRESSED_OFF + } else { + KEY_STATE_NORMAL_OFF + } + } else { + if (pressed) { + states = KEY_STATE_PRESSED + } + } + } + return states + } + + companion object { + private val KEY_STATE_NORMAL_ON = intArrayOf( + android.R.attr.state_checkable, + android.R.attr.state_checked + ) + private val KEY_STATE_PRESSED_ON = intArrayOf( + android.R.attr.state_pressed, + android.R.attr.state_checkable, + android.R.attr.state_checked + ) + private val KEY_STATE_NORMAL_OFF = intArrayOf( + android.R.attr.state_checkable + ) + private val KEY_STATE_PRESSED_OFF = intArrayOf( + android.R.attr.state_pressed, + android.R.attr.state_checkable + ) + private val KEY_STATE_NORMAL = intArrayOf() + private val KEY_STATE_PRESSED = intArrayOf( + android.R.attr.state_pressed + ) + } + } + + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + * @param width sets width of keyboard + * @param height sets height of keyboard + */ + constructor( + context: Context, @XmlRes xmlLayoutResId: Int, modeId: Int, width: Int, + height: Int + ) { + mDisplayWidth = width + mDisplayHeight = height + mDefaultHorizontalGap = 0 + mDefaultWidth = mDisplayWidth / 10 + mDefaultVerticalGap = 0 + mDefaultHeight = mDefaultWidth + mKeys = ArrayList() + mModifierKeys = ArrayList() + mKeyboardMode = modeId + loadKeyboard(context, context.resources.getXml(xmlLayoutResId)) + } + /** + * Creates a keyboard from the given xml key layout file. Weeds out rows + * that have a keyboard mode defined but don't match the specified mode. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param modeId keyboard mode identifier + */ + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + */ + @JvmOverloads + constructor(context: Context, @XmlRes xmlLayoutResId: Int, modeId: Int = 0) { + val dm = context.resources.displayMetrics + mDisplayWidth = dm.widthPixels + mDisplayHeight = dm.heightPixels + //Log.v(TAG, "keyboard's display metrics:" + dm); + mDefaultHorizontalGap = 0 + mDefaultWidth = mDisplayWidth / 10 + mDefaultVerticalGap = 0 + mDefaultHeight = mDefaultWidth + mKeys = ArrayList() + mModifierKeys = ArrayList() + mKeyboardMode = modeId + loadKeyboard(context, context.resources.getXml(xmlLayoutResId)) + } + + /** + * + * Creates a blank keyboard from the given resource file and populates it with the specified + * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. + * + * + * If the specified number of columns is -1, then the keyboard will fit as many keys as + * possible in each row. + * @param context the application or service context + * @param layoutTemplateResId the layout template file, containing no keys. + * @param characters the list of characters to display on the keyboard. One key will be created + * for each character. + * @param columns the number of columns of keys to display. If this number is greater than the + * number of keys that can fit in a row, it will be ignored. If this number is -1, the + * keyboard will fit as many keys as possible in each row. + */ + constructor( + context: Context, layoutTemplateResId: Int, + characters: CharSequence, columns: Int, horizontalPadding: Int + ) : this(context, layoutTemplateResId) { + var x = 0 + var y = 0 + var column = 0 + minWidth = 0 + val row = Row(this) + row.defaultHeight = mDefaultHeight + row.defaultWidth = mDefaultWidth + row.defaultHorizontalGap = mDefaultHorizontalGap + row.verticalGap = mDefaultVerticalGap + row.rowEdgeFlags = EDGE_TOP or EDGE_BOTTOM + val maxColumns = if (columns == -1) Int.MAX_VALUE else columns + for (i in 0 until characters.length) { + val c = characters[i] + if (column >= maxColumns + || x + mDefaultWidth + horizontalPadding > mDisplayWidth + ) { + x = 0 + y += mDefaultVerticalGap + mDefaultHeight + column = 0 + } + val key = Key(row) + key.x = x + key.y = y + key.label = c.toString() + key.codes = arrayListOf(c.toInt()) + column++ + x += key.width + key.gap + mKeys!!.add(key) + row.mKeys.add(key) + if (x > minWidth) { + minWidth = x + } + } + height = y + mDefaultHeight + rows.add(row) + } + + fun resize(newWidth: Int, newHeight: Int) { + val numRows = rows.size + for (rowIndex in 0 until numRows) { + val row = rows[rowIndex] ?: continue + val numKeys: Int = row.mKeys.size + var totalGap = 0 + var totalWidth = 0 + for (keyIndex in 0 until numKeys) { + val key: Key = row.mKeys.get(keyIndex) + if (keyIndex > 0) { + totalGap += key.gap + } + totalWidth += key.width + } + + if (totalGap + totalWidth > newWidth) { + var x = 0 + val scaleFactor = (newWidth - totalGap).toFloat() / totalWidth + for (keyIndex in 0 until numKeys) { + val key = row.mKeys[keyIndex] + key.width *= scaleFactor.toInt() + key.x = x + x += key.width + key.gap + } + } + } + minWidth = newWidth + // TODO: This does not adjust the vertical placement according to the new size. + // The main problem in the previous code was horizontal placement/size, but we should + // also recalculate the vertical sizes/positions when we get this resize call. + } + + val keys: List? + get() = mKeys + val modifierKeys: List? + get() = mModifierKeys + + fun setShifted(shiftState: Boolean): Boolean { + for (shiftKey in mShiftKeys) { + if (shiftKey != null) { + shiftKey.on = shiftState + } + } + if (isShifted != shiftState) { + isShifted = shiftState + return true + } + return false + } + + val shiftKeyIndex: Int + get() = shiftKeyIndices[0] + + private fun computeNearestNeighbors() { + // Round-up so we don't have any pixels outside the grid + mCellWidth = (minWidth + GRID_WIDTH - 1) / GRID_WIDTH + mCellHeight = (height + GRID_HEIGHT - 1) / GRID_HEIGHT + mGridNeighbors = ArrayList() + val indices = IntArray(mKeys!!.size) + val gridWidth: Int = GRID_WIDTH * mCellWidth + val gridHeight: Int = GRID_HEIGHT * mCellHeight + var x = 0 + while (x < gridWidth) { + var y = 0 + while (y < gridHeight) { + var count = 0 + for (i in mKeys!!.indices) { + val key = mKeys!![i] + if (key!!.squaredDistanceFrom(x, y) < mProximityThreshold || key.squaredDistanceFrom( + x + mCellWidth - 1, y + ) < mProximityThreshold || (key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) + < mProximityThreshold) || key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold + ) { + indices[count++] = i + } + } + val cell = IntArray(count) + System.arraycopy(indices, 0, cell, 0, count) + mGridNeighbors[y / mCellHeight * GRID_WIDTH + x / mCellWidth] = cell + y += mCellHeight + } + x += mCellWidth + } + } + + /** + * Returns the indices of the keys that are closest to the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the array of integer indices for the nearest keys to the given point. If the given + * point is out of range, then an array of size zero is returned. + */ + fun getNearestKeys(x: Int, y: Int): IntArray? { + if (x >= 0 && x < minWidth && y >= 0 && y < height) { + val index: Int = y / mCellHeight * GRID_WIDTH + x / mCellWidth + if (index < GRID_SIZE) { + return mGridNeighbors[index] + } + } + return IntArray(0) + } + + protected fun createRowFromXml(res: Resources, parser: XmlResourceParser?): Row { + return Row(res, this, parser) + } + + protected fun createKeyFromXml(res: Resources, parent: Row, x: Int, y: Int, parser: XmlResourceParser?): Key { + return Key(res, parent, x, y, parser) + } + + private fun loadKeyboard(context: Context, parser: XmlResourceParser) { + var inKey = false + var inRow = false + val leftMostKey = false + var row = 0 + var x = 0 + var y = 0 + var key: Key? = null + var currentRow: Row? = null + val res = context.resources + var skipRow = false + try { + var event: Int + while (parser.next().also { event = it } != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + val tag = parser.name + if (TAG_ROW == tag) { + inRow = true + x = 0 + currentRow = createRowFromXml(res, parser) + rows.add(currentRow) + skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode + if (skipRow) { + skipToEndOfRow(parser) + inRow = false + } + } else if (TAG_KEY == tag) { + inKey = true + key = createKeyFromXml(res, currentRow!!, x, y, parser) + mKeys!!.add(key) + if (key.codes[0] == KEYCODE_SHIFT) { + // Find available shift key slot and put this shift key in it + for (i in mShiftKeys.indices) { + if (mShiftKeys[i] == null) { + mShiftKeys[i] = key + shiftKeyIndices[i] = mKeys!!.size - 1 + break + } + } + mModifierKeys!!.add(key) + } else if (key.codes[0] == KEYCODE_ALT) { + mModifierKeys!!.add(key) + } + currentRow.mKeys.add(key) + } else if (TAG_KEYBOARD == tag) { + parseKeyboardAttributes(res, parser) + } + } else if (event == XmlResourceParser.END_TAG) { + if (inKey) { + inKey = false + x += key!!.gap + key.width + if (x > minWidth) { + minWidth = x + } + } else if (inRow) { + inRow = false + y += currentRow!!.verticalGap + y += currentRow.defaultHeight + row++ + } + } + } + } catch (e: Exception) { + } + height = y - mDefaultVerticalGap + } + + @Throws(XmlPullParserException::class, IOException::class) + private fun skipToEndOfRow(parser: XmlResourceParser) { + var event: Int + while (parser.next().also { event = it } != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.END_TAG + && parser.name == TAG_ROW + ) { + break + } + } + } + + private fun parseKeyboardAttributes(res: Resources, parser: XmlResourceParser) { + val a = res.obtainAttributes( + Xml.asAttributeSet(parser), + R.styleable.Keyboard + ) + mDefaultWidth = getDimensionOrFraction( + a, + R.styleable.Keyboard_keyWidth, + mDisplayWidth, mDisplayWidth / 10 + ) + mDefaultHeight = getDimensionOrFraction( + a, + R.styleable.Keyboard_keyHeight, + mDisplayHeight, 50 + ) + mDefaultHorizontalGap = getDimensionOrFraction( + a, + R.styleable.Keyboard_horizontalGap, + mDisplayWidth, 0 + ) + mDefaultVerticalGap = getDimensionOrFraction( + a, + R.styleable.Keyboard_verticalGap, + mDisplayHeight, 0 + ) + mProximityThreshold = (mDefaultWidth * SEARCH_DISTANCE) as Int + mProximityThreshold = mProximityThreshold * mProximityThreshold // Square it for comparison + a.recycle() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt index 550c9aa..5419a60 100644 --- a/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt +++ b/app/src/main/kotlin/com/simplemobiletools/keyboard/views/MyKeyboardView.kt @@ -763,6 +763,7 @@ class MyKeyboardView @JvmOverloads constructor(context: Context, attrs: Attribut AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, keyCode ) } + if (mCurrentKeyIndex != NOT_A_KEY && keys.size > mCurrentKeyIndex) { val newKey = keys[mCurrentKeyIndex] newKey.onPressed()