MoreKeysKeyboard.java revision 486c4894ce0917fc7b18eaee4bfd031051f2b05b
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.content.Context; 20import android.graphics.Paint; 21 22import com.android.inputmethod.annotations.UsedForTesting; 23import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 24import com.android.inputmethod.keyboard.internal.KeyboardParams; 25import com.android.inputmethod.keyboard.internal.MoreKeySpec; 26import com.android.inputmethod.latin.R; 27import com.android.inputmethod.latin.utils.StringUtils; 28import com.android.inputmethod.latin.utils.TypefaceUtils; 29 30public final class MoreKeysKeyboard extends Keyboard { 31 private final int mDefaultKeyCoordX; 32 33 MoreKeysKeyboard(final MoreKeysKeyboardParams params) { 34 super(params); 35 mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2; 36 } 37 38 public int getDefaultCoordX() { 39 return mDefaultKeyCoordX; 40 } 41 42 @UsedForTesting 43 static class MoreKeysKeyboardParams extends KeyboardParams { 44 public boolean mIsMoreKeysFixedOrder; 45 /* package */int mTopRowAdjustment; 46 public int mNumRows; 47 public int mNumColumns; 48 public int mTopKeys; 49 public int mLeftKeys; 50 public int mRightKeys; // includes default key. 51 public int mDividerWidth; 52 public int mColumnWidth; 53 54 public MoreKeysKeyboardParams() { 55 super(); 56 } 57 58 /** 59 * Set keyboard parameters of more keys keyboard. 60 * 61 * @param numKeys number of keys in this more keys keyboard. 62 * @param numColumn number of columns of this more keys keyboard. 63 * @param keyWidth more keys keyboard key width in pixel, including horizontal gap. 64 * @param rowHeight more keys keyboard row height in pixel, including vertical gap. 65 * @param coordXInParent coordinate x of the key preview in parent keyboard. 66 * @param parentKeyboardWidth parent keyboard width in pixel. 67 * @param isMoreKeysFixedColumn true if more keys keyboard should have 68 * <code>numColumn</code> columns. Otherwise more keys keyboard should have 69 * <code>numColumn</code> columns at most. 70 * @param isMoreKeysFixedOrder true if the order of more keys is determined by the order in 71 * the more keys' specification. Otherwise the order of more keys is automatically 72 * determined. 73 * @param dividerWidth width of divider, zero for no dividers. 74 */ 75 public void setParameters(final int numKeys, final int numColumn, final int keyWidth, 76 final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, 77 final boolean isMoreKeysFixedColumn, final boolean isMoreKeysFixedOrder, 78 final int dividerWidth) { 79 mIsMoreKeysFixedOrder = isMoreKeysFixedOrder; 80 if (parentKeyboardWidth / keyWidth < Math.min(numKeys, numColumn)) { 81 throw new IllegalArgumentException("Keyboard is too small to hold more keys: " 82 + parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + numColumn); 83 } 84 mDefaultKeyWidth = keyWidth; 85 mDefaultRowHeight = rowHeight; 86 87 final int numRows = (numKeys + numColumn - 1) / numColumn; 88 mNumRows = numRows; 89 final int numColumns = isMoreKeysFixedColumn ? Math.min(numKeys, numColumn) 90 : getOptimizedColumns(numKeys, numColumn); 91 mNumColumns = numColumns; 92 final int topKeys = numKeys % numColumns; 93 mTopKeys = topKeys == 0 ? numColumns : topKeys; 94 95 final int numLeftKeys = (numColumns - 1) / 2; 96 final int numRightKeys = numColumns - numLeftKeys; // including default key. 97 // Maximum number of keys we can layout both side of the parent key 98 final int maxLeftKeys = coordXInParent / keyWidth; 99 final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth; 100 int leftKeys, rightKeys; 101 if (numLeftKeys > maxLeftKeys) { 102 leftKeys = maxLeftKeys; 103 rightKeys = numColumns - leftKeys; 104 } else if (numRightKeys > maxRightKeys + 1) { 105 rightKeys = maxRightKeys + 1; // include default key 106 leftKeys = numColumns - rightKeys; 107 } else { 108 leftKeys = numLeftKeys; 109 rightKeys = numRightKeys; 110 } 111 // If the left keys fill the left side of the parent key, entire more keys keyboard 112 // should be shifted to the right unless the parent key is on the left edge. 113 if (maxLeftKeys == leftKeys && leftKeys > 0) { 114 leftKeys--; 115 rightKeys++; 116 } 117 // If the right keys fill the right side of the parent key, entire more keys 118 // should be shifted to the left unless the parent key is on the right edge. 119 if (maxRightKeys == rightKeys - 1 && rightKeys > 1) { 120 leftKeys++; 121 rightKeys--; 122 } 123 mLeftKeys = leftKeys; 124 mRightKeys = rightKeys; 125 126 // Adjustment of the top row. 127 mTopRowAdjustment = isMoreKeysFixedOrder ? getFixedOrderTopRowAdjustment() 128 : getAutoOrderTopRowAdjustment(); 129 mDividerWidth = dividerWidth; 130 mColumnWidth = mDefaultKeyWidth + mDividerWidth; 131 mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth; 132 // Need to subtract the bottom row's gutter only. 133 mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap 134 + mTopPadding + mBottomPadding; 135 } 136 137 private int getFixedOrderTopRowAdjustment() { 138 if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns 139 || mLeftKeys == 0 || mRightKeys == 1) { 140 return 0; 141 } 142 return -1; 143 } 144 145 private int getAutoOrderTopRowAdjustment() { 146 if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2 147 || mLeftKeys == 0 || mRightKeys == 1) { 148 return 0; 149 } 150 return -1; 151 } 152 153 // Return key position according to column count (0 is default). 154 /* package */int getColumnPos(final int n) { 155 return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n); 156 } 157 158 private int getFixedOrderColumnPos(final int n) { 159 final int col = n % mNumColumns; 160 final int row = n / mNumColumns; 161 if (!isTopRow(row)) { 162 return col - mLeftKeys; 163 } 164 final int rightSideKeys = mTopKeys / 2; 165 final int leftSideKeys = mTopKeys - (rightSideKeys + 1); 166 final int pos = col - leftSideKeys; 167 final int numLeftKeys = mLeftKeys + mTopRowAdjustment; 168 final int numRightKeys = mRightKeys - 1; 169 if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) { 170 return pos; 171 } else if (numRightKeys < rightSideKeys) { 172 return pos - (rightSideKeys - numRightKeys); 173 } else { // numLeftKeys < leftSideKeys 174 return pos + (leftSideKeys - numLeftKeys); 175 } 176 } 177 178 private int getAutomaticColumnPos(final int n) { 179 final int col = n % mNumColumns; 180 final int row = n / mNumColumns; 181 int leftKeys = mLeftKeys; 182 if (isTopRow(row)) { 183 leftKeys += mTopRowAdjustment; 184 } 185 if (col == 0) { 186 // default position. 187 return 0; 188 } 189 190 int pos = 0; 191 int right = 1; // include default position key. 192 int left = 0; 193 int i = 0; 194 while (true) { 195 // Assign right key if available. 196 if (right < mRightKeys) { 197 pos = right; 198 right++; 199 i++; 200 } 201 if (i >= col) 202 break; 203 // Assign left key if available. 204 if (left < leftKeys) { 205 left++; 206 pos = -left; 207 i++; 208 } 209 if (i >= col) 210 break; 211 } 212 return pos; 213 } 214 215 private static int getTopRowEmptySlots(final int numKeys, final int numColumns) { 216 final int remainings = numKeys % numColumns; 217 return remainings == 0 ? 0 : numColumns - remainings; 218 } 219 220 private int getOptimizedColumns(final int numKeys, final int maxColumns) { 221 int numColumns = Math.min(numKeys, maxColumns); 222 while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) { 223 numColumns--; 224 } 225 return numColumns; 226 } 227 228 public int getDefaultKeyCoordX() { 229 return mLeftKeys * mColumnWidth + mLeftPadding; 230 } 231 232 public int getX(final int n, final int row) { 233 final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX(); 234 if (isTopRow(row)) { 235 return x + mTopRowAdjustment * (mColumnWidth / 2); 236 } 237 return x; 238 } 239 240 public int getY(final int row) { 241 return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding; 242 } 243 244 public void markAsEdgeKey(final Key key, final int row) { 245 if (row == 0) 246 key.markAsTopEdge(this); 247 if (isTopRow(row)) 248 key.markAsBottomEdge(this); 249 } 250 251 private boolean isTopRow(final int rowCount) { 252 return mNumRows > 1 && rowCount == mNumRows - 1; 253 } 254 } 255 256 public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> { 257 private final Key mParentKey; 258 259 private static final float LABEL_PADDING_RATIO = 0.2f; 260 private static final float DIVIDER_RATIO = 0.2f; 261 262 /** 263 * The builder of MoreKeysKeyboard. 264 * @param context the context of {@link MoreKeysKeyboardView}. 265 * @param key the {@link Key} that invokes more keys keyboard. 266 * @param keyboard the {@link Keyboard} that contains the parentKey. 267 * @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single 268 * "more key" and its key popup preview is enabled. 269 * @param keyPreviewVisibleWidth the width of visible part of key popup preview. 270 * @param keyPreviewVisibleHeight the height of visible part of key popup preview 271 * @param paintToMeasure the {@link Paint} object to measure a "more key" width 272 */ 273 public Builder(final Context context, final Key key, final Keyboard keyboard, 274 final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth, 275 final int keyPreviewVisibleHeight, final Paint paintToMeasure) { 276 super(context, new MoreKeysKeyboardParams()); 277 load(keyboard.mMoreKeysTemplate, keyboard.mId); 278 279 // TODO: More keys keyboard's vertical gap is currently calculated heuristically. 280 // Should revise the algorithm. 281 mParams.mVerticalGap = keyboard.mVerticalGap / 2; 282 // This {@link MoreKeysKeyboard} is invoked from the <code>key</code>. 283 mParentKey = key; 284 285 final int keyWidth, rowHeight; 286 if (isSingleMoreKeyWithPreview) { 287 // Use pre-computed width and height if this more keys keyboard has only one key to 288 // mitigate visual flicker between key preview and more keys keyboard. 289 // Caveats for the visual assets: To achieve this effect, both the key preview 290 // backgrounds and the more keys keyboard panel background have the exact same 291 // left/right/top paddings. The bottom paddings of both backgrounds don't need to 292 // be considered because the vertical positions of both backgrounds were already 293 // adjusted with their bottom paddings deducted. 294 keyWidth = keyPreviewVisibleWidth; 295 rowHeight = keyPreviewVisibleHeight + mParams.mVerticalGap; 296 } else { 297 final float padding = context.getResources().getDimension( 298 R.dimen.config_more_keys_keyboard_key_horizontal_padding) 299 + (key.hasLabelsInMoreKeys() 300 ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f); 301 keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure); 302 rowHeight = keyboard.mMostCommonKeyHeight; 303 } 304 final int dividerWidth; 305 if (key.needsDividersInMoreKeys()) { 306 dividerWidth = (int)(keyWidth * DIVIDER_RATIO); 307 } else { 308 dividerWidth = 0; 309 } 310 final MoreKeySpec[] moreKeys = key.getMoreKeys(); 311 mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth, 312 rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth, 313 key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth); 314 } 315 316 private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth, 317 final float padding, final Paint paint) { 318 int maxWidth = minKeyWidth; 319 for (final MoreKeySpec spec : parentKey.getMoreKeys()) { 320 final String label = spec.mLabel; 321 // If the label is single letter, minKeyWidth is enough to hold the label. 322 if (label != null && StringUtils.codePointCount(label) > 1) { 323 maxWidth = Math.max(maxWidth, 324 (int)(TypefaceUtils.getStringWidth(label, paint) + padding)); 325 } 326 } 327 return maxWidth; 328 } 329 330 @Override 331 public MoreKeysKeyboard build() { 332 final MoreKeysKeyboardParams params = mParams; 333 final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags(); 334 final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys(); 335 for (int n = 0; n < moreKeys.length; n++) { 336 final MoreKeySpec moreKeySpec = moreKeys[n]; 337 final int row = n / params.mNumColumns; 338 final int x = params.getX(n, row); 339 final int y = params.getY(row); 340 final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params); 341 params.markAsEdgeKey(key, row); 342 params.onAddKey(key); 343 344 final int pos = params.getColumnPos(n); 345 // The "pos" value represents the offset from the default position. Negative means 346 // left of the default position. 347 if (params.mDividerWidth > 0 && pos != 0) { 348 final int dividerX = (pos > 0) ? x - params.mDividerWidth 349 : x + params.mDefaultKeyWidth; 350 final Key divider = new MoreKeyDivider( 351 params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight); 352 params.onAddKey(divider); 353 } 354 } 355 return new MoreKeysKeyboard(params); 356 } 357 } 358 359 // Used as a divider maker. A divider is drawn by {@link MoreKeysKeyboardView}. 360 public static class MoreKeyDivider extends Key.Spacer { 361 public MoreKeyDivider(final KeyboardParams params, final int x, final int y, 362 final int width, final int height) { 363 super(params, x, y, width, height); 364 } 365 } 366} 367