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