/*
* 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.content.Context;
import android.graphics.Paint;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.TypefaceUtils;
public final class MoreKeysKeyboard extends Keyboard {
private final int mDefaultKeyCoordX;
MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
super(params);
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
}
public int getDefaultCoordX() {
return mDefaultKeyCoordX;
}
@UsedForTesting
static class MoreKeysKeyboardParams extends KeyboardParams {
public boolean mIsMoreKeysFixedOrder;
/* package */int mTopRowAdjustment;
public int mNumRows;
public int mNumColumns;
public int mTopKeys;
public int mLeftKeys;
public int mRightKeys; // includes default key.
public int mDividerWidth;
public int mColumnWidth;
public MoreKeysKeyboardParams() {
super();
}
/**
* Set keyboard parameters of more keys keyboard.
*
* @param numKeys number of keys in this more keys keyboard.
* @param numColumn number of 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 isMoreKeysFixedColumn true if more keys keyboard should have
* numColumn
columns. Otherwise more keys keyboard should have
* numColumn
columns at most.
* @param isMoreKeysFixedOrder true if the order of more keys is determined by the order in
* the more keys' specification. Otherwise the order of more keys is automatically
* determined.
* @param dividerWidth width of divider, zero for no dividers.
*/
public void setParameters(final int numKeys, final int numColumn, final int keyWidth,
final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
final boolean isMoreKeysFixedColumn, final boolean isMoreKeysFixedOrder,
final int dividerWidth) {
mIsMoreKeysFixedOrder = isMoreKeysFixedOrder;
if (parentKeyboardWidth / keyWidth < Math.min(numKeys, numColumn)) {
throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
+ parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + numColumn);
}
mDefaultKeyWidth = keyWidth;
mDefaultRowHeight = rowHeight;
final int numRows = (numKeys + numColumn - 1) / numColumn;
mNumRows = numRows;
final int numColumns = isMoreKeysFixedColumn ? Math.min(numKeys, numColumn)
: getOptimizedColumns(numKeys, numColumn);
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 = isMoreKeysFixedOrder ? getFixedOrderTopRowAdjustment()
: getAutoOrderTopRowAdjustment();
mDividerWidth = dividerWidth;
mColumnWidth = mDefaultKeyWidth + mDividerWidth;
mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
// 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(final int n) {
return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
}
private int getFixedOrderColumnPos(final 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(final 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(final int numKeys, final int numColumns) {
final int remainings = numKeys % numColumns;
return remainings == 0 ? 0 : numColumns - remainings;
}
private int getOptimizedColumns(final int numKeys, final int maxColumns) {
int numColumns = Math.min(numKeys, maxColumns);
while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
numColumns--;
}
return numColumns;
}
public int getDefaultKeyCoordX() {
return mLeftKeys * mColumnWidth + mLeftPadding;
}
public int getX(final int n, final int row) {
final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
if (isTopRow(row)) {
return x + mTopRowAdjustment * (mColumnWidth / 2);
}
return x;
}
public int getY(final int row) {
return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
}
public void markAsEdgeKey(final Key key, final int row) {
if (row == 0)
key.markAsTopEdge(this);
if (isTopRow(row))
key.markAsBottomEdge(this);
}
private boolean isTopRow(final int rowCount) {
return mNumRows > 1 && rowCount == mNumRows - 1;
}
}
public static class Builder extends KeyboardBuilder {
private final Key mParentKey;
private static final float LABEL_PADDING_RATIO = 0.2f;
private static final float DIVIDER_RATIO = 0.2f;
/**
* The builder of MoreKeysKeyboard.
* @param context the context of {@link MoreKeysKeyboardView}.
* @param key the {@link Key} that invokes more keys keyboard.
* @param keyboard the {@link Keyboard} that contains the parentKey.
* @param isSingleMoreKeyWithPreview true if the key
has just a single
* "more key" and its key popup preview is enabled.
* @param keyPreviewVisibleWidth the width of visible part of key popup preview.
* @param keyPreviewVisibleHeight the height of visible part of key popup preview
* @param paintToMeasure the {@link Paint} object to measure a "more key" width
*/
public Builder(final Context context, final Key key, final Keyboard keyboard,
final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
super(context, new MoreKeysKeyboardParams());
load(keyboard.mMoreKeysTemplate, keyboard.mId);
// TODO: More keys keyboard's vertical gap is currently calculated heuristically.
// Should revise the algorithm.
mParams.mVerticalGap = keyboard.mVerticalGap / 2;
// This {@link MoreKeysKeyboard} is invoked from the key
.
mParentKey = key;
final int keyWidth, rowHeight;
if (isSingleMoreKeyWithPreview) {
// Use pre-computed width and height if this more keys keyboard has only one key to
// mitigate visual flicker between key preview and more keys keyboard.
// Caveats for the visual assets: To achieve this effect, both the key preview
// backgrounds and the more keys keyboard panel background have the exact same
// left/right/top paddings. The bottom paddings of both backgrounds don't need to
// be considered because the vertical positions of both backgrounds were already
// adjusted with their bottom paddings deducted.
keyWidth = keyPreviewVisibleWidth;
rowHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
} else {
final float padding = context.getResources().getDimension(
R.dimen.config_more_keys_keyboard_key_horizontal_padding)
+ (key.hasLabelsInMoreKeys()
? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure);
rowHeight = keyboard.mMostCommonKeyHeight;
}
final int dividerWidth;
if (key.needsDividersInMoreKeys()) {
dividerWidth = (int)(keyWidth * DIVIDER_RATIO);
} else {
dividerWidth = 0;
}
final MoreKeySpec[] moreKeys = key.getMoreKeys();
mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth, rowHeight,
key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
}
private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
final float padding, final Paint paint) {
int maxWidth = minKeyWidth;
for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
final String label = spec.mLabel;
// If the label is single letter, minKeyWidth is enough to hold the label.
if (label != null && StringUtils.codePointCount(label) > 1) {
maxWidth = Math.max(maxWidth,
(int)(TypefaceUtils.getStringWidth(label, paint) + padding));
}
}
return maxWidth;
}
@Override
public MoreKeysKeyboard build() {
final MoreKeysKeyboardParams params = mParams;
final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
for (int n = 0; n < moreKeys.length; n++) {
final MoreKeySpec moreKeySpec = moreKeys[n];
final int row = n / params.mNumColumns;
final int x = params.getX(n, row);
final int y = params.getY(row);
final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params);
params.markAsEdgeKey(key, row);
params.onAddKey(key);
final int pos = params.getColumnPos(n);
// The "pos" value represents the offset from the default position. Negative means
// left of the default position.
if (params.mDividerWidth > 0 && pos != 0) {
final int dividerX = (pos > 0) ? x - params.mDividerWidth
: x + params.mDefaultKeyWidth;
final Key divider = new MoreKeyDivider(
params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight);
params.onAddKey(divider);
}
}
return new MoreKeysKeyboard(params);
}
}
// Used as a divider maker. A divider is drawn by {@link MoreKeysKeyboardView}.
public static class MoreKeyDivider extends Key.Spacer {
public MoreKeyDivider(final KeyboardParams params, final int x, final int y,
final int width, final int height) {
super(params, x, y, width, height);
}
}
}