1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.keyboard; 18 19 import android.graphics.Paint; 20 import android.graphics.drawable.Drawable; 21 import android.view.View; 22 23 import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 24 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 25 import com.android.inputmethod.keyboard.internal.KeyboardParams; 26 import com.android.inputmethod.keyboard.internal.MoreKeySpec; 27 import com.android.inputmethod.latin.R; 28 import com.android.inputmethod.latin.StringUtils; 29 30 public 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 /* package for test */ 43 static class MoreKeysKeyboardParams extends KeyboardParams { 44 public boolean mIsFixedOrder; 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 maxColumns number of maximum 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 isFixedColumnOrder if true, more keys should be laid out in fixed order. 68 * @param dividerWidth width of divider, zero for no dividers. 69 */ 70 public void setParameters(final int numKeys, final int maxColumns, final int keyWidth, 71 final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, 72 final boolean isFixedColumnOrder, final int dividerWidth) { 73 mIsFixedOrder = isFixedColumnOrder; 74 if (parentKeyboardWidth / keyWidth < maxColumns) { 75 throw new IllegalArgumentException( 76 "Keyboard is too small to hold more keys keyboard: " 77 + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); 78 } 79 mDefaultKeyWidth = keyWidth; 80 mDefaultRowHeight = rowHeight; 81 82 final int numRows = (numKeys + maxColumns - 1) / maxColumns; 83 mNumRows = numRows; 84 final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns) 85 : getOptimizedColumns(numKeys, maxColumns); 86 mNumColumns = numColumns; 87 final int topKeys = numKeys % numColumns; 88 mTopKeys = topKeys == 0 ? numColumns : topKeys; 89 90 final int numLeftKeys = (numColumns - 1) / 2; 91 final int numRightKeys = numColumns - numLeftKeys; // including default key. 92 // Maximum number of keys we can layout both side of the parent key 93 final int maxLeftKeys = coordXInParent / keyWidth; 94 final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth; 95 int leftKeys, rightKeys; 96 if (numLeftKeys > maxLeftKeys) { 97 leftKeys = maxLeftKeys; 98 rightKeys = numColumns - leftKeys; 99 } else if (numRightKeys > maxRightKeys + 1) { 100 rightKeys = maxRightKeys + 1; // include default key 101 leftKeys = numColumns - rightKeys; 102 } else { 103 leftKeys = numLeftKeys; 104 rightKeys = numRightKeys; 105 } 106 // If the left keys fill the left side of the parent key, entire more keys keyboard 107 // should be shifted to the right unless the parent key is on the left edge. 108 if (maxLeftKeys == leftKeys && leftKeys > 0) { 109 leftKeys--; 110 rightKeys++; 111 } 112 // If the right keys fill the right side of the parent key, entire more keys 113 // should be shifted to the left unless the parent key is on the right edge. 114 if (maxRightKeys == rightKeys - 1 && rightKeys > 1) { 115 leftKeys++; 116 rightKeys--; 117 } 118 mLeftKeys = leftKeys; 119 mRightKeys = rightKeys; 120 121 // Adjustment of the top row. 122 mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment() 123 : getAutoOrderTopRowAdjustment(); 124 mDividerWidth = dividerWidth; 125 mColumnWidth = mDefaultKeyWidth + mDividerWidth; 126 mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth; 127 // Need to subtract the bottom row's gutter only. 128 mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap 129 + mTopPadding + mBottomPadding; 130 } 131 132 private int getFixedOrderTopRowAdjustment() { 133 if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns 134 || mLeftKeys == 0 || mRightKeys == 1) { 135 return 0; 136 } 137 return -1; 138 } 139 140 private int getAutoOrderTopRowAdjustment() { 141 if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2 142 || mLeftKeys == 0 || mRightKeys == 1) { 143 return 0; 144 } 145 return -1; 146 } 147 148 // Return key position according to column count (0 is default). 149 /* package */int getColumnPos(final int n) { 150 return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n); 151 } 152 153 private int getFixedOrderColumnPos(final int n) { 154 final int col = n % mNumColumns; 155 final int row = n / mNumColumns; 156 if (!isTopRow(row)) { 157 return col - mLeftKeys; 158 } 159 final int rightSideKeys = mTopKeys / 2; 160 final int leftSideKeys = mTopKeys - (rightSideKeys + 1); 161 final int pos = col - leftSideKeys; 162 final int numLeftKeys = mLeftKeys + mTopRowAdjustment; 163 final int numRightKeys = mRightKeys - 1; 164 if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) { 165 return pos; 166 } else if (numRightKeys < rightSideKeys) { 167 return pos - (rightSideKeys - numRightKeys); 168 } else { // numLeftKeys < leftSideKeys 169 return pos + (leftSideKeys - numLeftKeys); 170 } 171 } 172 173 private int getAutomaticColumnPos(final int n) { 174 final int col = n % mNumColumns; 175 final int row = n / mNumColumns; 176 int leftKeys = mLeftKeys; 177 if (isTopRow(row)) { 178 leftKeys += mTopRowAdjustment; 179 } 180 if (col == 0) { 181 // default position. 182 return 0; 183 } 184 185 int pos = 0; 186 int right = 1; // include default position key. 187 int left = 0; 188 int i = 0; 189 while (true) { 190 // Assign right key if available. 191 if (right < mRightKeys) { 192 pos = right; 193 right++; 194 i++; 195 } 196 if (i >= col) 197 break; 198 // Assign left key if available. 199 if (left < leftKeys) { 200 left++; 201 pos = -left; 202 i++; 203 } 204 if (i >= col) 205 break; 206 } 207 return pos; 208 } 209 210 private static int getTopRowEmptySlots(final int numKeys, final int numColumns) { 211 final int remainings = numKeys % numColumns; 212 return remainings == 0 ? 0 : numColumns - remainings; 213 } 214 215 private int getOptimizedColumns(final int numKeys, final int maxColumns) { 216 int numColumns = Math.min(numKeys, maxColumns); 217 while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) { 218 numColumns--; 219 } 220 return numColumns; 221 } 222 223 public int getDefaultKeyCoordX() { 224 return mLeftKeys * mColumnWidth; 225 } 226 227 public int getX(final int n, final int row) { 228 final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX(); 229 if (isTopRow(row)) { 230 return x + mTopRowAdjustment * (mColumnWidth / 2); 231 } 232 return x; 233 } 234 235 public int getY(final int row) { 236 return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding; 237 } 238 239 public void markAsEdgeKey(final Key key, final int row) { 240 if (row == 0) 241 key.markAsTopEdge(this); 242 if (isTopRow(row)) 243 key.markAsBottomEdge(this); 244 } 245 246 private boolean isTopRow(final int rowCount) { 247 return mNumRows > 1 && rowCount == mNumRows - 1; 248 } 249 } 250 251 public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> { 252 private final Key mParentKey; 253 private final Drawable mDivider; 254 255 private static final float LABEL_PADDING_RATIO = 0.2f; 256 private static final float DIVIDER_RATIO = 0.2f; 257 258 259 /** 260 * The builder of MoreKeysKeyboard. 261 * @param containerView the container of {@link MoreKeysKeyboardView}. 262 * @param parentKey the {@link Key} that invokes more keys keyboard. 263 * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey. 264 */ 265 public Builder(final View containerView, final Key parentKey, 266 final KeyboardView parentKeyboardView) { 267 super(containerView.getContext(), new MoreKeysKeyboardParams()); 268 final Keyboard parentKeyboard = parentKeyboardView.getKeyboard(); 269 load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId); 270 271 // TODO: More keys keyboard's vertical gap is currently calculated heuristically. 272 // Should revise the algorithm. 273 mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2; 274 mParentKey = parentKey; 275 276 final int width, height; 277 final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled() 278 && !parentKey.noKeyPreview() && parentKey.mMoreKeys.length == 1; 279 if (singleMoreKeyWithPreview) { 280 // Use pre-computed width and height if this more keys keyboard has only one key to 281 // mitigate visual flicker between key preview and more keys keyboard. 282 // Caveats for the visual assets: To achieve this effect, both the key preview 283 // backgrounds and the more keys keyboard panel background have the exact same 284 // left/right/top paddings. The bottom paddings of both backgrounds don't need to 285 // be considered because the vertical positions of both backgrounds were already 286 // adjusted with their bottom paddings deducted. 287 width = parentKeyboardView.mKeyPreviewDrawParams.mPreviewVisibleWidth; 288 height = parentKeyboardView.mKeyPreviewDrawParams.mPreviewVisibleHeight 289 + mParams.mVerticalGap; 290 } else { 291 width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth); 292 height = parentKeyboard.mMostCommonKeyHeight; 293 } 294 final int dividerWidth; 295 if (parentKey.needsDividersInMoreKeys()) { 296 mDivider = mResources.getDrawable(R.drawable.more_keys_divider); 297 dividerWidth = (int)(width * DIVIDER_RATIO); 298 } else { 299 mDivider = null; 300 dividerWidth = 0; 301 } 302 mParams.setParameters(parentKey.mMoreKeys.length, parentKey.getMoreKeysColumn(), 303 width, height, parentKey.mX + parentKey.mWidth / 2, 304 parentKeyboardView.getMeasuredWidth(), parentKey.isFixedColumnOrderMoreKeys(), 305 dividerWidth); 306 } 307 308 private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey, 309 final int minKeyWidth) { 310 final int padding = (int)(view.getResources() 311 .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) 312 + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0)); 313 final Paint paint = view.newDefaultLabelPaint(); 314 paint.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams)); 315 paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams)); 316 int maxWidth = minKeyWidth; 317 for (final MoreKeySpec spec : parentKey.mMoreKeys) { 318 final String label = spec.mLabel; 319 // If the label is single letter, minKeyWidth is enough to hold the label. 320 if (label != null && StringUtils.codePointCount(label) > 1) { 321 final int width = (int)view.getLabelWidth(label, paint) + padding; 322 if (maxWidth < width) { 323 maxWidth = width; 324 } 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.mMoreKeys; 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 = new Key(params, moreKeySpec, x, y, 341 params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags); 342 params.markAsEdgeKey(key, row); 343 params.onAddKey(key); 344 345 final int pos = params.getColumnPos(n); 346 // The "pos" value represents the offset from the default position. Negative means 347 // left of the default position. 348 if (params.mDividerWidth > 0 && pos != 0) { 349 final int dividerX = (pos > 0) ? x - params.mDividerWidth 350 : x + params.mDefaultKeyWidth; 351 final Key divider = new MoreKeyDivider(params, mDivider, dividerX, y); 352 params.onAddKey(divider); 353 } 354 } 355 return new MoreKeysKeyboard(params); 356 } 357 } 358 359 private static class MoreKeyDivider extends Key.Spacer { 360 private final Drawable mIcon; 361 362 public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon, 363 final int x, final int y) { 364 super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight); 365 mIcon = icon; 366 } 367 368 @Override 369 public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { 370 // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the 371 // constructor. 372 // TODO: Drawable itself should have an alpha value. 373 mIcon.setAlpha(128); 374 return mIcon; 375 } 376 } 377 } 378