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