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