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