MoreKeysKeyboard.java revision 727e818e95ef68460ebafb2eb3b11c23a2d8fcd8
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;
21
22import com.android.inputmethod.annotations.UsedForTesting;
23import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
24import com.android.inputmethod.keyboard.internal.KeyboardParams;
25import com.android.inputmethod.keyboard.internal.MoreKeySpec;
26import com.android.inputmethod.latin.R;
27import com.android.inputmethod.latin.utils.StringUtils;
28import com.android.inputmethod.latin.utils.TypefaceUtils;
29
30public 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, rowHeight,
312                    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