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 
17 package com.android.inputmethod.keyboard;
18 
19 import android.content.Context;
20 import android.graphics.Paint;
21 
22 import com.android.inputmethod.annotations.UsedForTesting;
23 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
24 import com.android.inputmethod.keyboard.internal.KeyboardParams;
25 import com.android.inputmethod.keyboard.internal.MoreKeySpec;
26 import com.android.inputmethod.latin.R;
27 import com.android.inputmethod.latin.common.StringUtils;
28 import com.android.inputmethod.latin.utils.TypefaceUtils;
29 
30 import javax.annotation.Nonnull;
31 
32 public final class MoreKeysKeyboard extends Keyboard {
33     private final int mDefaultKeyCoordX;
34 
MoreKeysKeyboard(final MoreKeysKeyboardParams params)35     MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
36         super(params);
37         mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
38     }
39 
getDefaultCoordX()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 
MoreKeysKeyboardParams()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          */
setParameters(final int numKeys, final int numColumn, final int keyWidth, final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, final boolean isMoreKeysFixedColumn, final boolean isMoreKeysFixedOrder, final int dividerWidth)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 
getFixedOrderTopRowAdjustment()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 
getAutoOrderTopRowAdjustment()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).
getColumnPos(final int n)156         /* package */int getColumnPos(final int n) {
157             return mIsMoreKeysFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
158         }
159 
getFixedOrderColumnPos(final int n)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 
getAutomaticColumnPos(final int n)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 
getTopRowEmptySlots(final int numKeys, final int numColumns)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 
getOptimizedColumns(final int numKeys, final int maxColumns)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 
getDefaultKeyCoordX()230         public int getDefaultKeyCoordX() {
231             return mLeftKeys * mColumnWidth + mLeftPadding;
232         }
233 
getX(final int n, final int row)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 
getY(final int row)242         public int getY(final int row) {
243             return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
244         }
245 
markAsEdgeKey(final Key key, final int row)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 
isTopRow(final int rowCount)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 
261         private static final float LABEL_PADDING_RATIO = 0.2f;
262         private static final float DIVIDER_RATIO = 0.2f;
263 
264         /**
265          * The builder of MoreKeysKeyboard.
266          * @param context the context of {@link MoreKeysKeyboardView}.
267          * @param key the {@link Key} that invokes more keys keyboard.
268          * @param keyboard the {@link Keyboard} that contains the parentKey.
269          * @param isSingleMoreKeyWithPreview true if the <code>key</code> has just a single
270          *        "more key" and its key popup preview is enabled.
271          * @param keyPreviewVisibleWidth the width of visible part of key popup preview.
272          * @param keyPreviewVisibleHeight the height of visible part of key popup preview
273          * @param paintToMeasure the {@link Paint} object to measure a "more key" width
274          */
Builder(final Context context, final Key key, final Keyboard keyboard, final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth, final int keyPreviewVisibleHeight, final Paint paintToMeasure)275         public Builder(final Context context, final Key key, final Keyboard keyboard,
276                 final boolean isSingleMoreKeyWithPreview, final int keyPreviewVisibleWidth,
277                 final int keyPreviewVisibleHeight, final Paint paintToMeasure) {
278             super(context, new MoreKeysKeyboardParams());
279             load(keyboard.mMoreKeysTemplate, keyboard.mId);
280 
281             // TODO: More keys keyboard's vertical gap is currently calculated heuristically.
282             // Should revise the algorithm.
283             mParams.mVerticalGap = keyboard.mVerticalGap / 2;
284             // This {@link MoreKeysKeyboard} is invoked from the <code>key</code>.
285             mParentKey = key;
286 
287             final int keyWidth, rowHeight;
288             if (isSingleMoreKeyWithPreview) {
289                 // Use pre-computed width and height if this more keys keyboard has only one key to
290                 // mitigate visual flicker between key preview and more keys keyboard.
291                 // Caveats for the visual assets: To achieve this effect, both the key preview
292                 // backgrounds and the more keys keyboard panel background have the exact same
293                 // left/right/top paddings. The bottom paddings of both backgrounds don't need to
294                 // be considered because the vertical positions of both backgrounds were already
295                 // adjusted with their bottom paddings deducted.
296                 keyWidth = keyPreviewVisibleWidth;
297                 rowHeight = keyPreviewVisibleHeight + mParams.mVerticalGap;
298             } else {
299                 final float padding = context.getResources().getDimension(
300                         R.dimen.config_more_keys_keyboard_key_horizontal_padding)
301                         + (key.hasLabelsInMoreKeys()
302                                 ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
303                 keyWidth = getMaxKeyWidth(key, mParams.mDefaultKeyWidth, padding, paintToMeasure);
304                 rowHeight = keyboard.mMostCommonKeyHeight;
305             }
306             final int dividerWidth;
307             if (key.needsDividersInMoreKeys()) {
308                 dividerWidth = (int)(keyWidth * DIVIDER_RATIO);
309             } else {
310                 dividerWidth = 0;
311             }
312             final MoreKeySpec[] moreKeys = key.getMoreKeys();
313             mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth,
314                     rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
315                     key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
316         }
317 
getMaxKeyWidth(final Key parentKey, final int minKeyWidth, final float padding, final Paint paint)318         private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
319                 final float padding, final Paint paint) {
320             int maxWidth = minKeyWidth;
321             for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
322                 final String label = spec.mLabel;
323                 // If the label is single letter, minKeyWidth is enough to hold the label.
324                 if (label != null && StringUtils.codePointCount(label) > 1) {
325                     maxWidth = Math.max(maxWidth,
326                             (int)(TypefaceUtils.getStringWidth(label, paint) + padding));
327                 }
328             }
329             return maxWidth;
330         }
331 
332         @Override
333         @Nonnull
build()334         public MoreKeysKeyboard build() {
335             final MoreKeysKeyboardParams params = mParams;
336             final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
337             final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
338             for (int n = 0; n < moreKeys.length; n++) {
339                 final MoreKeySpec moreKeySpec = moreKeys[n];
340                 final int row = n / params.mNumColumns;
341                 final int x = params.getX(n, row);
342                 final int y = params.getY(row);
343                 final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params);
344                 params.markAsEdgeKey(key, row);
345                 params.onAddKey(key);
346 
347                 final int pos = params.getColumnPos(n);
348                 // The "pos" value represents the offset from the default position. Negative means
349                 // left of the default position.
350                 if (params.mDividerWidth > 0 && pos != 0) {
351                     final int dividerX = (pos > 0) ? x - params.mDividerWidth
352                             : x + params.mDefaultKeyWidth;
353                     final Key divider = new MoreKeyDivider(
354                             params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight);
355                     params.onAddKey(divider);
356                 }
357             }
358             return new MoreKeysKeyboard(params);
359         }
360     }
361 
362     // Used as a divider maker. A divider is drawn by {@link MoreKeysKeyboardView}.
363     public static class MoreKeyDivider extends Key.Spacer {
MoreKeyDivider(final KeyboardParams params, final int x, final int y, final int width, final int height)364         public MoreKeyDivider(final KeyboardParams params, final int x, final int y,
365                 final int width, final int height) {
366             super(params, x, y, width, height);
367         }
368     }
369 }
370