ProximityInfo.java revision 9342484e8d573a40f470b6a593df31c602fa4076
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.keyboard; 18 19import android.graphics.Rect; 20import android.util.Log; 21 22import com.android.inputmethod.keyboard.internal.TouchPositionCorrection; 23import com.android.inputmethod.latin.common.Constants; 24import com.android.inputmethod.latin.utils.JniUtils; 25 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.Collections; 29import java.util.List; 30 31public class ProximityInfo { 32 private static final String TAG = ProximityInfo.class.getSimpleName(); 33 private static final boolean DEBUG = false; 34 35 // Must be equal to MAX_PROXIMITY_CHARS_SIZE in native/jni/src/defines.h 36 public static final int MAX_PROXIMITY_CHARS_SIZE = 16; 37 /** Number of key widths from current touch point to search for nearest keys. */ 38 private static final float SEARCH_DISTANCE = 1.2f; 39 private static final List<Key> EMPTY_KEY_LIST = Collections.emptyList(); 40 private static final float DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS = 0.15f; 41 42 private final int mGridWidth; 43 private final int mGridHeight; 44 private final int mGridSize; 45 private final int mCellWidth; 46 private final int mCellHeight; 47 // TODO: Find a proper name for mKeyboardMinWidth 48 private final int mKeyboardMinWidth; 49 private final int mKeyboardHeight; 50 private final int mMostCommonKeyWidth; 51 private final int mMostCommonKeyHeight; 52 private final List<Key> mSortedKeys; 53 private final List<Key>[] mGridNeighbors; 54 55 @SuppressWarnings("unchecked") 56 ProximityInfo(final int gridWidth, final int gridHeight, final int minWidth, final int height, 57 final int mostCommonKeyWidth, final int mostCommonKeyHeight, final List<Key> sortedKeys, 58 final TouchPositionCorrection touchPositionCorrection) { 59 mGridWidth = gridWidth; 60 mGridHeight = gridHeight; 61 mGridSize = mGridWidth * mGridHeight; 62 mCellWidth = (minWidth + mGridWidth - 1) / mGridWidth; 63 mCellHeight = (height + mGridHeight - 1) / mGridHeight; 64 mKeyboardMinWidth = minWidth; 65 mKeyboardHeight = height; 66 mMostCommonKeyHeight = mostCommonKeyHeight; 67 mMostCommonKeyWidth = mostCommonKeyWidth; 68 mSortedKeys = sortedKeys; 69 mGridNeighbors = new List[mGridSize]; 70 if (minWidth == 0 || height == 0) { 71 // No proximity required. Keyboard might be more keys keyboard. 72 return; 73 } 74 computeNearestNeighbors(); 75 mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection); 76 } 77 78 private long mNativeProximityInfo; 79 static { 80 JniUtils.loadNativeLibrary(); 81 } 82 83 // TODO: Stop passing proximityCharsArray 84 private static native long setProximityInfoNative(int displayWidth, int displayHeight, 85 int gridWidth, int gridHeight, int mostCommonKeyWidth, int mostCommonKeyHeight, 86 int[] proximityCharsArray, int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, 87 int[] keyWidths, int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs, 88 float[] sweetSpotCenterYs, float[] sweetSpotRadii); 89 90 private static native void releaseProximityInfoNative(long nativeProximityInfo); 91 92 private static boolean needsProximityInfo(final Key key) { 93 // Don't include special keys into ProximityInfo. 94 return key.getCode() >= Constants.CODE_SPACE; 95 } 96 97 private static int getProximityInfoKeysCount(final List<Key> keys) { 98 int count = 0; 99 for (final Key key : keys) { 100 if (needsProximityInfo(key)) { 101 count++; 102 } 103 } 104 return count; 105 } 106 107 private long createNativeProximityInfo(final TouchPositionCorrection touchPositionCorrection) { 108 final List<Key>[] gridNeighborKeys = mGridNeighbors; 109 final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE]; 110 Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE); 111 for (int i = 0; i < mGridSize; ++i) { 112 final List<Key> neighborKeys = gridNeighborKeys[i]; 113 final int proximityCharsLength = neighborKeys.size(); 114 int infoIndex = i * MAX_PROXIMITY_CHARS_SIZE; 115 for (int j = 0; j < proximityCharsLength; ++j) { 116 final Key neighborKey = neighborKeys.get(j); 117 // Excluding from proximityCharsArray 118 if (!needsProximityInfo(neighborKey)) { 119 continue; 120 } 121 proximityCharsArray[infoIndex] = neighborKey.getCode(); 122 infoIndex++; 123 } 124 } 125 if (DEBUG) { 126 final StringBuilder sb = new StringBuilder(); 127 for (int i = 0; i < mGridSize; i++) { 128 sb.setLength(0); 129 for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE; j++) { 130 final int code = proximityCharsArray[i * MAX_PROXIMITY_CHARS_SIZE + j]; 131 if (code == Constants.NOT_A_CODE) { 132 break; 133 } 134 if (sb.length() > 0) sb.append(" "); 135 sb.append(Constants.printableCode(code)); 136 } 137 Log.d(TAG, "proxmityChars["+i+"]: " + sb); 138 } 139 } 140 141 final List<Key> sortedKeys = mSortedKeys; 142 final int keyCount = getProximityInfoKeysCount(sortedKeys); 143 final int[] keyXCoordinates = new int[keyCount]; 144 final int[] keyYCoordinates = new int[keyCount]; 145 final int[] keyWidths = new int[keyCount]; 146 final int[] keyHeights = new int[keyCount]; 147 final int[] keyCharCodes = new int[keyCount]; 148 final float[] sweetSpotCenterXs; 149 final float[] sweetSpotCenterYs; 150 final float[] sweetSpotRadii; 151 152 for (int infoIndex = 0, keyIndex = 0; keyIndex < sortedKeys.size(); keyIndex++) { 153 final Key key = sortedKeys.get(keyIndex); 154 // Excluding from key coordinate arrays 155 if (!needsProximityInfo(key)) { 156 continue; 157 } 158 keyXCoordinates[infoIndex] = key.getX(); 159 keyYCoordinates[infoIndex] = key.getY(); 160 keyWidths[infoIndex] = key.getWidth(); 161 keyHeights[infoIndex] = key.getHeight(); 162 keyCharCodes[infoIndex] = key.getCode(); 163 infoIndex++; 164 } 165 166 if (touchPositionCorrection != null && touchPositionCorrection.isValid()) { 167 if (DEBUG) { 168 Log.d(TAG, "touchPositionCorrection: ON"); 169 } 170 sweetSpotCenterXs = new float[keyCount]; 171 sweetSpotCenterYs = new float[keyCount]; 172 sweetSpotRadii = new float[keyCount]; 173 final int rows = touchPositionCorrection.getRows(); 174 final float defaultRadius = DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS 175 * (float)Math.hypot(mMostCommonKeyWidth, mMostCommonKeyHeight); 176 for (int infoIndex = 0, keyIndex = 0; keyIndex < sortedKeys.size(); keyIndex++) { 177 final Key key = sortedKeys.get(keyIndex); 178 // Excluding from touch position correction arrays 179 if (!needsProximityInfo(key)) { 180 continue; 181 } 182 final Rect hitBox = key.getHitBox(); 183 sweetSpotCenterXs[infoIndex] = hitBox.exactCenterX(); 184 sweetSpotCenterYs[infoIndex] = hitBox.exactCenterY(); 185 sweetSpotRadii[infoIndex] = defaultRadius; 186 final int row = hitBox.top / mMostCommonKeyHeight; 187 if (row < rows) { 188 final int hitBoxWidth = hitBox.width(); 189 final int hitBoxHeight = hitBox.height(); 190 final float hitBoxDiagonal = (float)Math.hypot(hitBoxWidth, hitBoxHeight); 191 sweetSpotCenterXs[infoIndex] += 192 touchPositionCorrection.getX(row) * hitBoxWidth; 193 sweetSpotCenterYs[infoIndex] += 194 touchPositionCorrection.getY(row) * hitBoxHeight; 195 sweetSpotRadii[infoIndex] = 196 touchPositionCorrection.getRadius(row) * hitBoxDiagonal; 197 } 198 if (DEBUG) { 199 Log.d(TAG, String.format( 200 " [%2d] row=%d x/y/r=%7.2f/%7.2f/%5.2f %s code=%s", infoIndex, row, 201 sweetSpotCenterXs[infoIndex], sweetSpotCenterYs[infoIndex], 202 sweetSpotRadii[infoIndex], (row < rows ? "correct" : "default"), 203 Constants.printableCode(key.getCode()))); 204 } 205 infoIndex++; 206 } 207 } else { 208 sweetSpotCenterXs = sweetSpotCenterYs = sweetSpotRadii = null; 209 if (DEBUG) { 210 Log.d(TAG, "touchPositionCorrection: OFF"); 211 } 212 } 213 214 // TODO: Stop passing proximityCharsArray 215 return setProximityInfoNative(mKeyboardMinWidth, mKeyboardHeight, mGridWidth, mGridHeight, 216 mMostCommonKeyWidth, mMostCommonKeyHeight, proximityCharsArray, keyCount, 217 keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes, 218 sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); 219 } 220 221 public long getNativeProximityInfo() { 222 return mNativeProximityInfo; 223 } 224 225 @Override 226 protected void finalize() throws Throwable { 227 try { 228 if (mNativeProximityInfo != 0) { 229 releaseProximityInfoNative(mNativeProximityInfo); 230 mNativeProximityInfo = 0; 231 } 232 } finally { 233 super.finalize(); 234 } 235 } 236 237 private void computeNearestNeighbors() { 238 final int defaultWidth = mMostCommonKeyWidth; 239 final int keyCount = mSortedKeys.size(); 240 final int gridSize = mGridNeighbors.length; 241 final int threshold = (int) (defaultWidth * SEARCH_DISTANCE); 242 final int thresholdSquared = threshold * threshold; 243 // Round-up so we don't have any pixels outside the grid 244 final int lastPixelXCoordinate = mGridWidth * mCellWidth - 1; 245 final int lastPixelYCoordinate = mGridHeight * mCellHeight - 1; 246 247 // For large layouts, 'neighborsFlatBuffer' is about 80k of memory: gridSize is usually 512, 248 // keycount is about 40 and a pointer to a Key is 4 bytes. This contains, for each cell, 249 // enough space for as many keys as there are on the keyboard. Hence, every 250 // keycount'th element is the start of a new cell, and each of these virtual subarrays 251 // start empty with keycount spaces available. This fills up gradually in the loop below. 252 // Since in the practice each cell does not have a lot of neighbors, most of this space is 253 // actually just empty padding in this fixed-size buffer. 254 final Key[] neighborsFlatBuffer = new Key[gridSize * keyCount]; 255 final int[] neighborCountPerCell = new int[gridSize]; 256 final int halfCellWidth = mCellWidth / 2; 257 final int halfCellHeight = mCellHeight / 2; 258 for (final Key key : mSortedKeys) { 259 if (key.isSpacer()) continue; 260 261/* HOW WE PRE-SELECT THE CELLS (iterate over only the relevant cells, instead of all of them) 262 263 We want to compute the distance for keys that are in the cells that are close enough to the 264 key border, as this method is performance-critical. These keys are represented with 'star' 265 background on the diagram below. Let's consider the Y case first. 266 267 We want to select the cells which center falls between the top of the key minus the threshold, 268 and the bottom of the key plus the threshold. 269 topPixelWithinThreshold is key.mY - threshold, and bottomPixelWithinThreshold is 270 key.mY + key.mHeight + threshold. 271 272 Then we need to compute the center of the top row that we need to evaluate, as we'll iterate 273 from there. 274 275(0,0)----> x 276| .-------------------------------------------. 277| | | | | | | | | | | | | 278| |---+---+---+---+---+---+---+---+---+---+---| .- top of top cell (aligned on the grid) 279| | | | | | | | | | | | | | 280| |-----------+---+---+---+---+---+---+---+---|---' v 281| | | | |***|***|*_________________________ topPixelWithinThreshold | yDeltaToGrid 282| |---+---+---+-----^-+-|-+---+---+---+---+---| ^ 283| | | | |***|*|*|*|*|***|***| | | | ______________________________________ 284v |---+---+--threshold--|-+---+---+---+---+---| | 285 | | | |***|*|*|*|*|***|***| | | | | Starting from key.mY, we substract 286y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get the top pixel 287 | | | |***|**########------------------- key.mY | within the threshold. We align that on 288 |---+---+---+---+--#+---+-#-+---+---+---+---| | the grid by computing the delta to the 289 | | | |***|**#|***|*#*|***| | | | | grid, and get the top of the top cell. 290 |---+---+---+---+--#+---+-#-+---+---+---+---| | 291 | | | |***|**########*|***| | | | | Adding half the cell height to the top 292 |---+---+---+---+---+-|-+---+---+---+---+---| | of the top cell, we get the middle of 293 | | | |***|***|*|*|***|***| | | | | the top cell (yMiddleOfTopCell). 294 |---+---+---+---+---+-|-+---+---+---+---+---| | 295 | | | |***|***|*|*|***|***| | | | | 296 |---+---+---+---+---+-|________________________ yEnd | Since we only want to add the key to 297 | | | | | | | (bottomPixelWithinThreshold) | the proximity if it's close enough to 298 |---+---+---+---+---+---+---+---+---+---+---| | the center of the cell, we only need 299 | | | | | | | | | | | | | to compute for these cells where 300 '---'---'---'---'---'---'---'---'---'---'---' | topPixelWithinThreshold is above the 301 (positive x,y) | center of the cell. This is the case 302 | when yDeltaToGrid is less than half 303 [Zoomed in diagram] | the height of the cell. 304 +-------+-------+-------+-------+-------+ | 305 | | | | | | | On the zoomed in diagram, on the right 306 | | | | | | | the topPixelWithinThreshold (represented 307 | | | | | | top of | with an = sign) is below and we can skip 308 +-------+-------+-------+--v----+-------+ .. top cell | this cell, while on the left it's above 309 | | = topPixelWT | | yDeltaToGrid | and we need to compute for this cell. 310 |..yStart.|.....|.......|..|....|.......|... y middle | Thus, if yDeltaToGrid is more than half 311 | (left)| | | ^ = | | of top cell | the height of the cell, we start the 312 +-------+-|-----+-------+----|--+-------+ | iteration one cell below the top cell, 313 | | | | | | | | | else we start it on the top cell. This 314 |.......|.|.....|.......|....|..|.....yStart (right) | is stored in yStart. 315 316 Since we only want to go up to bottomPixelWithinThreshold, and we only iterate on the center 317 of the keys, we can stop as soon as the y value exceeds bottomPixelThreshold, so we don't 318 have to align this on the center of the key. Hence, we don't need a separate value for 319 bottomPixelWithinThreshold and call this yEnd right away. 320*/ 321 final int keyX = key.getX(); 322 final int keyY = key.getY(); 323 final int topPixelWithinThreshold = keyY - threshold; 324 final int yDeltaToGrid = topPixelWithinThreshold % mCellHeight; 325 final int yMiddleOfTopCell = topPixelWithinThreshold - yDeltaToGrid + halfCellHeight; 326 final int yStart = Math.max(halfCellHeight, 327 yMiddleOfTopCell + (yDeltaToGrid <= halfCellHeight ? 0 : mCellHeight)); 328 final int yEnd = Math.min(lastPixelYCoordinate, keyY + key.getHeight() + threshold); 329 330 final int leftPixelWithinThreshold = keyX - threshold; 331 final int xDeltaToGrid = leftPixelWithinThreshold % mCellWidth; 332 final int xMiddleOfLeftCell = leftPixelWithinThreshold - xDeltaToGrid + halfCellWidth; 333 final int xStart = Math.max(halfCellWidth, 334 xMiddleOfLeftCell + (xDeltaToGrid <= halfCellWidth ? 0 : mCellWidth)); 335 final int xEnd = Math.min(lastPixelXCoordinate, keyX + key.getWidth() + threshold); 336 337 int baseIndexOfCurrentRow = (yStart / mCellHeight) * mGridWidth + (xStart / mCellWidth); 338 for (int centerY = yStart; centerY <= yEnd; centerY += mCellHeight) { 339 int index = baseIndexOfCurrentRow; 340 for (int centerX = xStart; centerX <= xEnd; centerX += mCellWidth) { 341 if (key.squaredDistanceToEdge(centerX, centerY) < thresholdSquared) { 342 neighborsFlatBuffer[index * keyCount + neighborCountPerCell[index]] = key; 343 ++neighborCountPerCell[index]; 344 } 345 ++index; 346 } 347 baseIndexOfCurrentRow += mGridWidth; 348 } 349 } 350 351 for (int i = 0; i < gridSize; ++i) { 352 final int indexStart = i * keyCount; 353 final int indexEnd = indexStart + neighborCountPerCell[i]; 354 final ArrayList<Key> neighbors = new ArrayList<>(indexEnd - indexStart); 355 for (int index = indexStart; index < indexEnd; index++) { 356 neighbors.add(neighborsFlatBuffer[index]); 357 } 358 mGridNeighbors[i] = Collections.unmodifiableList(neighbors); 359 } 360 } 361 362 public void fillArrayWithNearestKeyCodes(final int x, final int y, final int primaryKeyCode, 363 final int[] dest) { 364 final int destLength = dest.length; 365 if (destLength < 1) { 366 return; 367 } 368 int index = 0; 369 if (primaryKeyCode > Constants.CODE_SPACE) { 370 dest[index++] = primaryKeyCode; 371 } 372 final List<Key> nearestKeys = getNearestKeys(x, y); 373 for (Key key : nearestKeys) { 374 if (index >= destLength) { 375 break; 376 } 377 final int code = key.getCode(); 378 if (code <= Constants.CODE_SPACE) { 379 break; 380 } 381 dest[index++] = code; 382 } 383 if (index < destLength) { 384 dest[index] = Constants.NOT_A_CODE; 385 } 386 } 387 388 public List<Key> getNearestKeys(final int x, final int y) { 389 if (mGridNeighbors == null) { 390 return EMPTY_KEY_LIST; 391 } 392 if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) { 393 int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth); 394 if (index < mGridSize) { 395 return mGridNeighbors[index]; 396 } 397 } 398 return EMPTY_KEY_LIST; 399 } 400} 401