/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.inputmethod.keyboard; import android.graphics.Paint; import com.android.inputmethod.keyboard.internal.KeySpecParser; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.Utils; public class MoreKeysKeyboard extends Keyboard { private final int mDefaultKeyCoordX; MoreKeysKeyboard(Builder.MoreKeysKeyboardParams params) { super(params); mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2; } public int getDefaultCoordX() { return mDefaultKeyCoordX; } public static class Builder extends Keyboard.Builder { private final Key mParentKey; private static final float LABEL_PADDING_RATIO = 0.2f; public static class MoreKeysKeyboardParams extends Keyboard.Params { public boolean mIsFixedOrder; /* package */int mTopRowAdjustment; public int mNumRows; public int mNumColumns; public int mTopKeys; public int mLeftKeys; public int mRightKeys; // includes default key. public MoreKeysKeyboardParams() { super(); } /** * Set keyboard parameters of more keys keyboard. * * @param numKeys number of keys in this more keys keyboard. * @param maxColumns number of maximum columns of this more keys keyboard. * @param keyWidth more keys keyboard key width in pixel, including horizontal gap. * @param rowHeight more keys keyboard row height in pixel, including vertical gap. * @param coordXInParent coordinate x of the key preview in parent keyboard. * @param parentKeyboardWidth parent keyboard width in pixel. * @param isFixedColumnOrder if true, more keys should be laid out in fixed order. */ public void setParameters(int numKeys, int maxColumns, int keyWidth, int rowHeight, int coordXInParent, int parentKeyboardWidth, boolean isFixedColumnOrder) { mIsFixedOrder = isFixedColumnOrder; if (parentKeyboardWidth / keyWidth < maxColumns) { throw new IllegalArgumentException( "Keyboard is too small to hold more keys keyboard: " + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); } mDefaultKeyWidth = keyWidth; mDefaultRowHeight = rowHeight; final int numRows = (numKeys + maxColumns - 1) / maxColumns; mNumRows = numRows; final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns) : getOptimizedColumns(numKeys, maxColumns); mNumColumns = numColumns; final int topKeys = numKeys % numColumns; mTopKeys = topKeys == 0 ? numColumns : topKeys; final int numLeftKeys = (numColumns - 1) / 2; final int numRightKeys = numColumns - numLeftKeys; // including default key. // Maximum number of keys we can layout both side of the parent key final int maxLeftKeys = coordXInParent / keyWidth; final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth; int leftKeys, rightKeys; if (numLeftKeys > maxLeftKeys) { leftKeys = maxLeftKeys; rightKeys = numColumns - leftKeys; } else if (numRightKeys > maxRightKeys + 1) { rightKeys = maxRightKeys + 1; // include default key leftKeys = numColumns - rightKeys; } else { leftKeys = numLeftKeys; rightKeys = numRightKeys; } // If the left keys fill the left side of the parent key, entire more keys keyboard // should be shifted to the right unless the parent key is on the left edge. if (maxLeftKeys == leftKeys && leftKeys > 0) { leftKeys--; rightKeys++; } // If the right keys fill the right side of the parent key, entire more keys // should be shifted to the left unless the parent key is on the right edge. if (maxRightKeys == rightKeys - 1 && rightKeys > 1) { leftKeys++; rightKeys--; } mLeftKeys = leftKeys; mRightKeys = rightKeys; // Adjustment of the top row. mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment() : getAutoOrderTopRowAdjustment(); mBaseWidth = mOccupiedWidth = mNumColumns * mDefaultKeyWidth; // Need to subtract the bottom row's gutter only. mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap + mTopPadding + mBottomPadding; } private int getFixedOrderTopRowAdjustment() { if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns || mLeftKeys == 0 || mRightKeys == 1) { return 0; } return -1; } private int getAutoOrderTopRowAdjustment() { if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2 || mLeftKeys == 0 || mRightKeys == 1) { return 0; } return -1; } // Return key position according to column count (0 is default). /* package */int getColumnPos(int n) { return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n); } private int getFixedOrderColumnPos(int n) { final int col = n % mNumColumns; final int row = n / mNumColumns; if (!isTopRow(row)) { return col - mLeftKeys; } final int rightSideKeys = mTopKeys / 2; final int leftSideKeys = mTopKeys - (rightSideKeys + 1); final int pos = col - leftSideKeys; final int numLeftKeys = mLeftKeys + mTopRowAdjustment; final int numRightKeys = mRightKeys - 1; if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) { return pos; } else if (numRightKeys < rightSideKeys) { return pos - (rightSideKeys - numRightKeys); } else { // numLeftKeys < leftSideKeys return pos + (leftSideKeys - numLeftKeys); } } private int getAutomaticColumnPos(int n) { final int col = n % mNumColumns; final int row = n / mNumColumns; int leftKeys = mLeftKeys; if (isTopRow(row)) { leftKeys += mTopRowAdjustment; } if (col == 0) { // default position. return 0; } int pos = 0; int right = 1; // include default position key. int left = 0; int i = 0; while (true) { // Assign right key if available. if (right < mRightKeys) { pos = right; right++; i++; } if (i >= col) break; // Assign left key if available. if (left < leftKeys) { left++; pos = -left; i++; } if (i >= col) break; } return pos; } private static int getTopRowEmptySlots(int numKeys, int numColumns) { final int remainings = numKeys % numColumns; return remainings == 0 ? 0 : numColumns - remainings; } private int getOptimizedColumns(int numKeys, int maxColumns) { int numColumns = Math.min(numKeys, maxColumns); while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) { numColumns--; } return numColumns; } public int getDefaultKeyCoordX() { return mLeftKeys * mDefaultKeyWidth; } public int getX(int n, int row) { final int x = getColumnPos(n) * mDefaultKeyWidth + getDefaultKeyCoordX(); if (isTopRow(row)) { return x + mTopRowAdjustment * (mDefaultKeyWidth / 2); } return x; } public int getY(int row) { return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding; } public void markAsEdgeKey(Key key, int row) { if (row == 0) key.markAsTopEdge(this); if (isTopRow(row)) key.markAsBottomEdge(this); } private boolean isTopRow(int rowCount) { return mNumRows > 1 && rowCount == mNumRows - 1; } } public Builder(KeyboardView view, int xmlId, Key parentKey, Keyboard parentKeyboard) { super(view.getContext(), new MoreKeysKeyboardParams()); load(xmlId, parentKeyboard.mId); // TODO: More keys keyboard's vertical gap is currently calculated heuristically. // Should revise the algorithm. mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2; mParentKey = parentKey; final int previewWidth = view.mKeyPreviewDrawParams.mPreviewBackgroundWidth; final int previewHeight = view.mKeyPreviewDrawParams.mPreviewBackgroundHeight; final int width, height; // Use pre-computed width and height if these values are available and more keys // keyboard has only one key to mitigate visual flicker between key preview and more // keys keyboard. final boolean validKeyPreview = view.isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview() && (previewWidth > 0) && (previewHeight > 0); final boolean singleMoreKeyWithPreview = validKeyPreview && parentKey.mMoreKeys.length == 1; if (singleMoreKeyWithPreview) { width = previewWidth; height = previewHeight + mParams.mVerticalGap; } else { width = getMaxKeyWidth(view, parentKey, mParams.mDefaultKeyWidth); height = parentKeyboard.mMostCommonKeyHeight; } mParams.setParameters(parentKey.mMoreKeys.length, parentKey.getMoreKeysColumn(), width, height, parentKey.mX + parentKey.mWidth / 2, view.getMeasuredWidth(), parentKey.isFixedColumnOrderMoreKeys()); } private static int getMaxKeyWidth(KeyboardView view, Key parentKey, int minKeyWidth) { final int padding = (int)(view.getResources() .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0)); Paint paint = null; int maxWidth = minKeyWidth; for (String moreKeySpec : parentKey.mMoreKeys) { final String label = KeySpecParser.getLabel(moreKeySpec); // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && Utils.codePointCount(label) > 1) { if (paint == null) { paint = new Paint(); paint.setAntiAlias(true); } final int width = (int)view.getDefaultLabelWidth(label, paint) + padding; if (maxWidth < width) { maxWidth = width; } } } return maxWidth; } @Override public MoreKeysKeyboard build() { final MoreKeysKeyboardParams params = mParams; // moreKeyFlags == 0 means that the rendered text size will be determined by its // label's code point count. final int moreKeyFlags = mParentKey.hasLabelsInMoreKeys() ? 0 : Key.LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO; final String[] moreKeys = mParentKey.mMoreKeys; for (int n = 0; n < moreKeys.length; n++) { final String moreKeySpec = moreKeys[n]; final int row = n / params.mNumColumns; final Key key = new Key(mResources, params, moreKeySpec, params.getX(n, row), params.getY(row), params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags); params.markAsEdgeKey(key, row); params.onAddKey(key); } return new MoreKeysKeyboard(params); } } }