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