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