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