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