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