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