• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Align;
27 import android.graphics.PorterDuff;
28 import android.graphics.Rect;
29 import android.graphics.Region.Op;
30 import android.graphics.Typeface;
31 import android.graphics.drawable.Drawable;
32 import android.os.Message;
33 import android.util.AttributeSet;
34 import android.util.TypedValue;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.RelativeLayout;
39 import android.widget.TextView;
40 
41 import com.android.inputmethod.compat.FrameLayoutCompatUtils;
42 import com.android.inputmethod.latin.LatinImeLogger;
43 import com.android.inputmethod.latin.R;
44 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
45 
46 import java.util.HashMap;
47 
48 /**
49  * A view that renders a virtual {@link Keyboard}.
50  *
51  * @attr ref R.styleable#KeyboardView_backgroundDimAmount
52  * @attr ref R.styleable#KeyboardView_keyBackground
53  * @attr ref R.styleable#KeyboardView_keyLetterRatio
54  * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio
55  * @attr ref R.styleable#KeyboardView_keyLabelRatio
56  * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
57  * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio
58  * @attr ref R.styleable#KeyboardView_keyHintLabelRatio
59  * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
60  * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
61  * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
62  * @attr ref R.styleable#KeyboardView_keyUppercaseLetterPadding
63  * @attr ref R.styleable#KeyboardView_keyTextStyle
64  * @attr ref R.styleable#KeyboardView_keyPreviewLayout
65  * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio
66  * @attr ref R.styleable#KeyboardView_keyPreviewOffset
67  * @attr ref R.styleable#KeyboardView_keyPreviewHeight
68  * @attr ref R.styleable#KeyboardView_keyTextColor
69  * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
70  * @attr ref R.styleable#KeyboardView_keyHintLetterColor
71  * @attr ref R.styleable#KeyboardView_keyHintLabelColor
72  * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor
73  * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor
74  * @attr ref R.styleable#KeyboardView_shadowColor
75  * @attr ref R.styleable#KeyboardView_shadowRadius
76  */
77 public class KeyboardView extends View implements PointerTracker.DrawingProxy {
78     // Miscellaneous constants
79     private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
80 
81     // XML attributes
82     protected final float mVerticalCorrection;
83     protected final int mMoreKeysLayout;
84     private final float mBackgroundDimAmount;
85 
86     // HORIZONTAL ELLIPSIS "...", character for popup hint.
87     private static final String POPUP_HINT_CHAR = "\u2026";
88 
89     // Margin between the label and the icon on a key that has both of them.
90     // Specified by the fraction of the key width.
91     // TODO: Use resource parameter for this value.
92     private static final float LABEL_ICON_MARGIN = 0.05f;
93 
94     // The maximum key label width in the proportion to the key width.
95     private static final float MAX_LABEL_RATIO = 0.90f;
96 
97     // Main keyboard
98     private Keyboard mKeyboard;
99     private final KeyDrawParams mKeyDrawParams;
100 
101     // Key preview
102     private final int mKeyPreviewLayoutId;
103     protected final KeyPreviewDrawParams mKeyPreviewDrawParams;
104     private boolean mShowKeyPreviewPopup = true;
105     private final int mDelayBeforePreview;
106     private int mDelayAfterPreview;
107     private ViewGroup mPreviewPlacer;
108 
109     // Drawing
110     /** True if the entire keyboard needs to be dimmed. */
111     private boolean mNeedsToDimBackground;
112     /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/
113     private boolean mBufferNeedsUpdate;
114     /** The dirty region in the keyboard bitmap */
115     private final Rect mDirtyRect = new Rect();
116     /** The key to invalidate. */
117     private Key mInvalidatedKey;
118     /** The dirty region for single key drawing */
119     private final Rect mInvalidatedKeyRect = new Rect();
120     /** The keyboard bitmap buffer for faster updates */
121     private Bitmap mBuffer;
122     /** The canvas for the above mutable keyboard bitmap */
123     private Canvas mCanvas;
124     private final Paint mPaint = new Paint();
125     // This map caches key label text height in pixel as value and key label text size as map key.
126     private static final HashMap<Integer, Float> sTextHeightCache =
127             new HashMap<Integer, Float>();
128     // This map caches key label text width in pixel as value and key label text size as map key.
129     private static final HashMap<Integer, Float> sTextWidthCache =
130             new HashMap<Integer, Float>();
131     private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
132     private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
133 
134     private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
135 
136     public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
137         private static final int MSG_SHOW_KEY_PREVIEW = 1;
138         private static final int MSG_DISMISS_KEY_PREVIEW = 2;
139 
DrawingHandler(KeyboardView outerInstance)140         public DrawingHandler(KeyboardView outerInstance) {
141             super(outerInstance);
142         }
143 
144         @Override
handleMessage(Message msg)145         public void handleMessage(Message msg) {
146             final KeyboardView keyboardView = getOuterInstance();
147             if (keyboardView == null) return;
148             final PointerTracker tracker = (PointerTracker) msg.obj;
149             switch (msg.what) {
150             case MSG_SHOW_KEY_PREVIEW:
151                 keyboardView.showKey(msg.arg1, tracker);
152                 break;
153             case MSG_DISMISS_KEY_PREVIEW:
154                 tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
155                 break;
156             }
157         }
158 
showKeyPreview(long delay, int keyIndex, PointerTracker tracker)159         public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
160             removeMessages(MSG_SHOW_KEY_PREVIEW);
161             final KeyboardView keyboardView = getOuterInstance();
162             if (keyboardView == null) return;
163             if (tracker.getKeyPreviewText().getVisibility() == VISIBLE || delay == 0) {
164                 // Show right away, if it's already visible and finger is moving around
165                 keyboardView.showKey(keyIndex, tracker);
166             } else {
167                 sendMessageDelayed(
168                         obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
169             }
170         }
171 
cancelShowKeyPreview(PointerTracker tracker)172         public void cancelShowKeyPreview(PointerTracker tracker) {
173             removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
174         }
175 
cancelAllShowKeyPreviews()176         public void cancelAllShowKeyPreviews() {
177             removeMessages(MSG_SHOW_KEY_PREVIEW);
178         }
179 
dismissKeyPreview(long delay, PointerTracker tracker)180         public void dismissKeyPreview(long delay, PointerTracker tracker) {
181             sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
182         }
183 
cancelDismissKeyPreview(PointerTracker tracker)184         public void cancelDismissKeyPreview(PointerTracker tracker) {
185             removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
186         }
187 
cancelAllDismissKeyPreviews()188         public void cancelAllDismissKeyPreviews() {
189             removeMessages(MSG_DISMISS_KEY_PREVIEW);
190         }
191 
cancelAllMessages()192         public void cancelAllMessages() {
193             cancelAllShowKeyPreviews();
194             cancelAllDismissKeyPreviews();
195         }
196     }
197 
198     private static class KeyDrawParams {
199         // XML attributes
200         public final int mKeyTextColor;
201         public final int mKeyTextInactivatedColor;
202         public final Typeface mKeyTextStyle;
203         public final float mKeyLabelHorizontalPadding;
204         public final float mKeyHintLetterPadding;
205         public final float mKeyPopupHintLetterPadding;
206         public final float mKeyUppercaseLetterPadding;
207         public final int mShadowColor;
208         public final float mShadowRadius;
209         public final Drawable mKeyBackground;
210         public final int mKeyHintLetterColor;
211         public final int mKeyHintLabelColor;
212         public final int mKeyUppercaseLetterInactivatedColor;
213         public final int mKeyUppercaseLetterActivatedColor;
214 
215         private final float mKeyLetterRatio;
216         private final float mKeyLargeLetterRatio;
217         private final float mKeyLabelRatio;
218         private final float mKeyHintLetterRatio;
219         private final float mKeyUppercaseLetterRatio;
220         private final float mKeyHintLabelRatio;
221         private static final float UNDEFINED_RATIO = -1.0f;
222 
223         public final Rect mPadding = new Rect();
224         public int mKeyLetterSize;
225         public int mKeyLargeLetterSize;
226         public int mKeyLabelSize;
227         public int mKeyHintLetterSize;
228         public int mKeyUppercaseLetterSize;
229         public int mKeyHintLabelSize;
230 
KeyDrawParams(TypedArray a)231         public KeyDrawParams(TypedArray a) {
232             mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
233             if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) {
234                 mKeyLetterRatio = UNDEFINED_RATIO;
235                 mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0);
236             } else {
237                 mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
238             }
239             if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) {
240                 mKeyLabelRatio = UNDEFINED_RATIO;
241                 mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0);
242             } else {
243                 mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
244             }
245             mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
246             mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
247             mKeyUppercaseLetterRatio = getRatio(a,
248                     R.styleable.KeyboardView_keyUppercaseLetterRatio);
249             mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
250             mKeyLabelHorizontalPadding = a.getDimension(
251                     R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
252             mKeyHintLetterPadding = a.getDimension(
253                     R.styleable.KeyboardView_keyHintLetterPadding, 0);
254             mKeyPopupHintLetterPadding = a.getDimension(
255                     R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
256             mKeyUppercaseLetterPadding = a.getDimension(
257                     R.styleable.KeyboardView_keyUppercaseLetterPadding, 0);
258             mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
259             mKeyTextInactivatedColor = a.getColor(
260                     R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
261             mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
262             mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
263             mKeyUppercaseLetterInactivatedColor = a.getColor(
264                     R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0);
265             mKeyUppercaseLetterActivatedColor = a.getColor(
266                     R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0);
267             mKeyTextStyle = Typeface.defaultFromStyle(
268                     a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
269             mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
270             mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
271 
272             mKeyBackground.getPadding(mPadding);
273         }
274 
updateKeyHeight(int keyHeight)275         public void updateKeyHeight(int keyHeight) {
276             if (mKeyLetterRatio >= 0.0f)
277                 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
278             if (mKeyLabelRatio >= 0.0f)
279                 mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
280             mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
281             mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
282             mKeyUppercaseLetterSize = (int)(keyHeight * mKeyUppercaseLetterRatio);
283             mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
284         }
285     }
286 
287     protected static class KeyPreviewDrawParams {
288         // XML attributes.
289         public final Drawable mPreviewBackground;
290         public final Drawable mPreviewLeftBackground;
291         public final Drawable mPreviewRightBackground;
292         public final int mPreviewBackgroundWidth;
293         public final int mPreviewBackgroundHeight;
294         public final int mPreviewTextColor;
295         public final int mPreviewOffset;
296         public final int mPreviewHeight;
297         public final Typeface mKeyTextStyle;
298 
299         private final float mPreviewTextRatio;
300         private final float mKeyLetterRatio;
301 
302         public int mPreviewTextSize;
303         public int mKeyLetterSize;
304         public final int[] mCoordinates = new int[2];
305 
306         private static final int PREVIEW_ALPHA = 240;
307 
KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams)308         public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) {
309             mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
310             mPreviewLeftBackground = a.getDrawable(
311                     R.styleable.KeyboardView_keyPreviewLeftBackground);
312             mPreviewRightBackground = a.getDrawable(
313                     R.styleable.KeyboardView_keyPreviewRightBackground);
314             setAlpha(mPreviewBackground, PREVIEW_ALPHA);
315             setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA);
316             setAlpha(mPreviewRightBackground, PREVIEW_ALPHA);
317             mPreviewBackgroundWidth = a.getDimensionPixelSize(
318                     R.styleable.KeyboardView_keyPreviewBackgroundWidth, 0);
319             mPreviewBackgroundHeight = a.getDimensionPixelSize(
320                     R.styleable.KeyboardView_keyPreviewBackgroundHeight, 0);
321             mPreviewOffset = a.getDimensionPixelOffset(
322                     R.styleable.KeyboardView_keyPreviewOffset, 0);
323             mPreviewHeight = a.getDimensionPixelSize(
324                     R.styleable.KeyboardView_keyPreviewHeight, 80);
325             mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
326             mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
327 
328             mKeyLetterRatio = keyDrawParams.mKeyLetterRatio;
329             mKeyTextStyle = keyDrawParams.mKeyTextStyle;
330         }
331 
updateKeyHeight(int keyHeight)332         public void updateKeyHeight(int keyHeight) {
333             mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
334             mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
335         }
336 
setAlpha(Drawable drawable, int alpha)337         private static void setAlpha(Drawable drawable, int alpha) {
338             if (drawable == null)
339                 return;
340             drawable.setAlpha(alpha);
341         }
342     }
343 
KeyboardView(Context context, AttributeSet attrs)344     public KeyboardView(Context context, AttributeSet attrs) {
345         this(context, attrs, R.attr.keyboardViewStyle);
346     }
347 
KeyboardView(Context context, AttributeSet attrs, int defStyle)348     public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
349         super(context, attrs, defStyle);
350 
351         final TypedArray a = context.obtainStyledAttributes(
352                 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
353 
354         mKeyDrawParams = new KeyDrawParams(a);
355         mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
356         mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
357         if (mKeyPreviewLayoutId == 0) {
358             mShowKeyPreviewPopup = false;
359         }
360         mVerticalCorrection = a.getDimensionPixelOffset(
361                 R.styleable.KeyboardView_verticalCorrection, 0);
362         mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0);
363         mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f);
364         a.recycle();
365 
366         final Resources res = getResources();
367 
368         mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
369         mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
370 
371         mPaint.setAntiAlias(true);
372         mPaint.setTextAlign(Align.CENTER);
373         mPaint.setAlpha(255);
374     }
375 
376     // Read fraction value in TypedArray as float.
getRatio(TypedArray a, int index)377     private static float getRatio(TypedArray a, int index) {
378         return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
379     }
380 
381     /**
382      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
383      * view will re-layout itself to accommodate the keyboard.
384      * @see Keyboard
385      * @see #getKeyboard()
386      * @param keyboard the keyboard to display in this view
387      */
setKeyboard(Keyboard keyboard)388     public void setKeyboard(Keyboard keyboard) {
389         // Remove any pending dismissing preview
390         mDrawingHandler.cancelAllShowKeyPreviews();
391         if (mKeyboard != null) {
392             PointerTracker.dismissAllKeyPreviews();
393         }
394         mKeyboard = keyboard;
395         LatinImeLogger.onSetKeyboard(keyboard);
396         requestLayout();
397         mDirtyRect.set(0, 0, getWidth(), getHeight());
398         mBufferNeedsUpdate = true;
399         invalidateAllKeys();
400         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
401         mKeyDrawParams.updateKeyHeight(keyHeight);
402         mKeyPreviewDrawParams.updateKeyHeight(keyHeight);
403     }
404 
405     /**
406      * Returns the current keyboard being displayed by this view.
407      * @return the currently attached keyboard
408      * @see #setKeyboard(Keyboard)
409      */
getKeyboard()410     public Keyboard getKeyboard() {
411         return mKeyboard;
412     }
413 
414     /**
415      * Enables or disables the key feedback popup. This is a popup that shows a magnified
416      * version of the depressed key. By default the preview is enabled.
417      * @param previewEnabled whether or not to enable the key feedback preview
418      * @param delay the delay after which the preview is dismissed
419      * @see #isKeyPreviewPopupEnabled()
420      */
setKeyPreviewPopupEnabled(boolean previewEnabled, int delay)421     public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
422         mShowKeyPreviewPopup = previewEnabled;
423         mDelayAfterPreview = delay;
424     }
425 
426     /**
427      * Returns the enabled state of the key feedback preview
428      * @return whether or not the key feedback preview is enabled
429      * @see #setKeyPreviewPopupEnabled(boolean, int)
430      */
isKeyPreviewPopupEnabled()431     public boolean isKeyPreviewPopupEnabled() {
432         return mShowKeyPreviewPopup;
433     }
434 
435     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)436     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
437         if (mKeyboard != null) {
438             // The main keyboard expands to the display width.
439             final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
440             setMeasuredDimension(widthMeasureSpec, height);
441         } else {
442             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
443         }
444     }
445 
446     @Override
onDraw(Canvas canvas)447     public void onDraw(Canvas canvas) {
448         super.onDraw(canvas);
449         if (mBufferNeedsUpdate || mBuffer == null) {
450             mBufferNeedsUpdate = false;
451             onBufferDraw();
452         }
453         canvas.drawBitmap(mBuffer, 0, 0, null);
454     }
455 
onBufferDraw()456     private void onBufferDraw() {
457         final int width = getWidth();
458         final int height = getHeight();
459         if (width == 0 || height == 0)
460             return;
461         if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
462             if (mBuffer != null)
463                 mBuffer.recycle();
464             mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
465             mDirtyRect.union(0, 0, width, height);
466             if (mCanvas != null) {
467                 mCanvas.setBitmap(mBuffer);
468             } else {
469                 mCanvas = new Canvas(mBuffer);
470             }
471         }
472         final Canvas canvas = mCanvas;
473         canvas.clipRect(mDirtyRect, Op.REPLACE);
474         canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
475 
476         if (mKeyboard == null) return;
477 
478         final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
479         final KeyDrawParams params = mKeyDrawParams;
480         if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
481             // Draw a single key.
482             final int keyDrawX = mInvalidatedKey.mX + mInvalidatedKey.mVisualInsetsLeft
483                     + getPaddingLeft();
484             final int keyDrawY = mInvalidatedKey.mY + getPaddingTop();
485             canvas.translate(keyDrawX, keyDrawY);
486             onBufferDrawKey(mInvalidatedKey, mKeyboard, canvas, mPaint, params,
487                     isManualTemporaryUpperCase);
488             canvas.translate(-keyDrawX, -keyDrawY);
489         } else {
490             // Draw all keys.
491             for (final Key key : mKeyboard.mKeys) {
492                 final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft();
493                 final int keyDrawY = key.mY + getPaddingTop();
494                 canvas.translate(keyDrawX, keyDrawY);
495                 onBufferDrawKey(key, mKeyboard, canvas, mPaint, params, isManualTemporaryUpperCase);
496                 canvas.translate(-keyDrawX, -keyDrawY);
497             }
498         }
499 
500         // Overlay a dark rectangle to dim the entire keyboard
501         if (mNeedsToDimBackground) {
502             mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
503             canvas.drawRect(0, 0, width, height, mPaint);
504         }
505 
506         mInvalidatedKey = null;
507         mDirtyRect.setEmpty();
508     }
509 
dimEntireKeyboard(boolean dimmed)510     public void dimEntireKeyboard(boolean dimmed) {
511         final boolean needsRedrawing = mNeedsToDimBackground != dimmed;
512         mNeedsToDimBackground = dimmed;
513         if (needsRedrawing) {
514             invalidateAllKeys();
515         }
516     }
517 
onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas, Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase)518     private static void onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas,
519             Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase) {
520         final boolean debugShowAlign = LatinImeLogger.sVISUALDEBUG;
521         // Draw key background.
522         if (!key.isSpacer()) {
523             final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight
524                     + params.mPadding.left + params.mPadding.right;
525             final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom;
526             final int bgX = -params.mPadding.left;
527             final int bgY = -params.mPadding.top;
528             final int[] drawableState = key.getCurrentDrawableState();
529             final Drawable background = params.mKeyBackground;
530             background.setState(drawableState);
531             final Rect bounds = background.getBounds();
532             if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
533                 background.setBounds(0, 0, bgWidth, bgHeight);
534             }
535             canvas.translate(bgX, bgY);
536             background.draw(canvas);
537             if (debugShowAlign) {
538                 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
539             }
540             canvas.translate(-bgX, -bgY);
541         }
542 
543         // Draw key top visuals.
544         final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
545         final int keyHeight = key.mHeight;
546         final float centerX = keyWidth * 0.5f;
547         final float centerY = keyHeight * 0.5f;
548 
549         if (debugShowAlign) {
550             drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
551         }
552 
553         // Draw key label.
554         final Drawable icon = key.getIcon();
555         float positionX = centerX;
556         if (key.mLabel != null) {
557             // Switch the character to uppercase if shift is pressed
558             final CharSequence label = keyboard.adjustLabelCase(key.mLabel);
559             // For characters, use large font. For labels like "Done", use smaller font.
560             paint.setTypeface(key.selectTypeface(params.mKeyTextStyle));
561             final int labelSize = key.selectTextSize(params.mKeyLetterSize,
562                     params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyHintLabelSize);
563             paint.setTextSize(labelSize);
564             final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
565             final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
566 
567             // Vertical label text alignment.
568             final float baseline = centerY + labelCharHeight / 2;
569 
570             // Horizontal label text alignment
571             float labelWidth = 0;
572             if (key.isAlignLeft()) {
573                 positionX = (int)params.mKeyLabelHorizontalPadding;
574                 paint.setTextAlign(Align.LEFT);
575             } else if (key.isAlignRight()) {
576                 positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding;
577                 paint.setTextAlign(Align.RIGHT);
578             } else if (key.isAlignLeftOfCenter()) {
579                 // TODO: Parameterise this?
580                 positionX = centerX - labelCharWidth * 7 / 4;
581                 paint.setTextAlign(Align.LEFT);
582             } else if (key.hasLabelWithIconLeft() && icon != null) {
583                 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
584                         + LABEL_ICON_MARGIN * keyWidth;
585                 positionX = centerX + labelWidth / 2;
586                 paint.setTextAlign(Align.RIGHT);
587             } else if (key.hasLabelWithIconRight() && icon != null) {
588                 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
589                         + LABEL_ICON_MARGIN * keyWidth;
590                 positionX = centerX - labelWidth / 2;
591                 paint.setTextAlign(Align.LEFT);
592             } else {
593                 positionX = centerX;
594                 paint.setTextAlign(Align.CENTER);
595             }
596             if (key.needsXScale()) {
597                 paint.setTextScaleX(
598                         Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
599             }
600 
601             if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) {
602                 paint.setColor(params.mKeyTextInactivatedColor);
603             } else {
604                 paint.setColor(params.mKeyTextColor);
605             }
606             if (key.isEnabled()) {
607                 // Set a drop shadow for the text
608                 paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor);
609             } else {
610                 // Make label invisible
611                 paint.setColor(Color.TRANSPARENT);
612             }
613             canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
614             // Turn off drop shadow and reset x-scale.
615             paint.setShadowLayer(0, 0, 0, 0);
616             paint.setTextScaleX(1.0f);
617 
618             if (icon != null) {
619                 final int iconWidth = icon.getIntrinsicWidth();
620                 final int iconHeight = icon.getIntrinsicHeight();
621                 final int iconY = (keyHeight - iconHeight) / 2;
622                 if (key.hasLabelWithIconLeft()) {
623                     final int iconX = (int)(centerX - labelWidth / 2);
624                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
625                 } else if (key.hasLabelWithIconRight()) {
626                     final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
627                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
628                 }
629             }
630 
631             if (debugShowAlign) {
632                 final Paint line = new Paint();
633                 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line);
634                 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line);
635             }
636         }
637 
638         // Draw hint label.
639         if (key.mHintLabel != null) {
640             final CharSequence hint = key.mHintLabel;
641             final int hintColor;
642             final int hintSize;
643             if (key.hasHintLabel()) {
644                 hintColor = params.mKeyHintLabelColor;
645                 hintSize = params.mKeyHintLabelSize;
646                 paint.setTypeface(Typeface.DEFAULT);
647             } else if (key.hasUppercaseLetter()) {
648                 hintColor = isManualTemporaryUpperCase
649                         ? params.mKeyUppercaseLetterActivatedColor
650                         : params.mKeyUppercaseLetterInactivatedColor;
651                 hintSize = params.mKeyUppercaseLetterSize;
652             } else { // key.hasHintLetter()
653                 hintColor = params.mKeyHintLetterColor;
654                 hintSize = params.mKeyHintLetterSize;
655             }
656             paint.setColor(hintColor);
657             paint.setTextSize(hintSize);
658             final float hintX, hintY;
659             if (key.hasHintLabel()) {
660                 // The hint label is placed just right of the key label. Used mainly on
661                 // "phone number" layout.
662                 // TODO: Generalize the following calculations.
663                 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2;
664                 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
665                 paint.setTextAlign(Align.LEFT);
666             } else if (key.hasUppercaseLetter()) {
667                 // The hint label is placed at top-right corner of the key. Used mainly on tablet.
668                 hintX = keyWidth - params.mKeyUppercaseLetterPadding
669                         - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
670                 hintY = -paint.ascent();
671                 paint.setTextAlign(Align.CENTER);
672             } else { // key.hasHintLetter()
673                 // The hint label is placed at top-right corner of the key. Used mainly on phone.
674                 hintX = keyWidth - params.mKeyHintLetterPadding
675                         - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
676                 hintY = -paint.ascent();
677                 paint.setTextAlign(Align.CENTER);
678             }
679             canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint);
680 
681             if (debugShowAlign) {
682                 final Paint line = new Paint();
683                 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
684                 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
685             }
686         }
687 
688         // Draw key icon.
689         if (key.mLabel == null && icon != null) {
690             final int iconWidth = icon.getIntrinsicWidth();
691             final int iconHeight = icon.getIntrinsicHeight();
692             final int iconX, alignX;
693             final int iconY = (keyHeight - iconHeight) / 2;
694             if (key.isAlignLeft()) {
695                 iconX = (int)params.mKeyLabelHorizontalPadding;
696                 alignX = iconX;
697             } else if (key.isAlignRight()) {
698                 iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth;
699                 alignX = iconX + iconWidth;
700             } else { // Align center
701                 iconX = (keyWidth - iconWidth) / 2;
702                 alignX = iconX + iconWidth / 2;
703             }
704             drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
705 
706             if (debugShowAlign) {
707                 final Paint line = new Paint();
708                 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line);
709                 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line);
710             }
711         }
712 
713         // Draw popup hint "..." at the bottom right corner of the key.
714         if ((key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0)
715                 || key.needsSpecialPopupHint()) {
716             paint.setTextSize(params.mKeyHintLetterSize);
717             paint.setColor(params.mKeyHintLabelColor);
718             paint.setTextAlign(Align.CENTER);
719             final float hintX = keyWidth - params.mKeyHintLetterPadding
720                     - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
721             final float hintY = keyHeight - params.mKeyPopupHintLetterPadding;
722             canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
723 
724             if (debugShowAlign) {
725                 final Paint line = new Paint();
726                 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
727                 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
728             }
729         }
730     }
731 
732     private static final Rect sTextBounds = new Rect();
733 
getCharGeometryCacheKey(char reference, Paint paint)734     private static int getCharGeometryCacheKey(char reference, Paint paint) {
735         final int labelSize = (int)paint.getTextSize();
736         final Typeface face = paint.getTypeface();
737         final int codePointOffset = reference << 15;
738         if (face == Typeface.DEFAULT) {
739             return codePointOffset + labelSize;
740         } else if (face == Typeface.DEFAULT_BOLD) {
741             return codePointOffset + labelSize + 0x1000;
742         } else if (face == Typeface.MONOSPACE) {
743             return codePointOffset + labelSize + 0x2000;
744         } else {
745             return codePointOffset + labelSize;
746         }
747     }
748 
getCharHeight(char[] character, Paint paint)749     private static float getCharHeight(char[] character, Paint paint) {
750         final Integer key = getCharGeometryCacheKey(character[0], paint);
751         final Float cachedValue = sTextHeightCache.get(key);
752         if (cachedValue != null)
753             return cachedValue;
754 
755         paint.getTextBounds(character, 0, 1, sTextBounds);
756         final float height = sTextBounds.height();
757         sTextHeightCache.put(key, height);
758         return height;
759     }
760 
getCharWidth(char[] character, Paint paint)761     private static float getCharWidth(char[] character, Paint paint) {
762         final Integer key = getCharGeometryCacheKey(character[0], paint);
763         final Float cachedValue = sTextWidthCache.get(key);
764         if (cachedValue != null)
765             return cachedValue;
766 
767         paint.getTextBounds(character, 0, 1, sTextBounds);
768         final float width = sTextBounds.width();
769         sTextWidthCache.put(key, width);
770         return width;
771     }
772 
getLabelWidth(CharSequence label, Paint paint)773     private static float getLabelWidth(CharSequence label, Paint paint) {
774         paint.getTextBounds(label.toString(), 0, label.length(), sTextBounds);
775         return sTextBounds.width();
776     }
777 
getDefaultLabelWidth(CharSequence label, Paint paint)778     public float getDefaultLabelWidth(CharSequence label, Paint paint) {
779         paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
780         paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
781         return getLabelWidth(label, paint);
782     }
783 
drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, int height)784     private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
785             int height) {
786         canvas.translate(x, y);
787         icon.setBounds(0, 0, width, height);
788         icon.draw(canvas);
789         canvas.translate(-x, -y);
790     }
791 
drawHorizontalLine(Canvas canvas, float y, float w, int color, Paint paint)792     private static void drawHorizontalLine(Canvas canvas, float y, float w, int color,
793             Paint paint) {
794         paint.setStyle(Paint.Style.STROKE);
795         paint.setStrokeWidth(1.0f);
796         paint.setColor(color);
797         canvas.drawLine(0, y, w, y, paint);
798     }
799 
drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint)800     private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) {
801         paint.setStyle(Paint.Style.STROKE);
802         paint.setStrokeWidth(1.0f);
803         paint.setColor(color);
804         canvas.drawLine(x, 0, x, h, paint);
805     }
806 
drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, Paint paint)807     private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color,
808             Paint paint) {
809         paint.setStyle(Paint.Style.STROKE);
810         paint.setStrokeWidth(1.0f);
811         paint.setColor(color);
812         canvas.translate(x, y);
813         canvas.drawRect(0, 0, w, h, paint);
814         canvas.translate(-x, -y);
815     }
816 
cancelAllMessages()817     public void cancelAllMessages() {
818         mDrawingHandler.cancelAllMessages();
819     }
820 
821     // Called by {@link PointerTracker} constructor to create a TextView.
822     @Override
inflateKeyPreviewText()823     public TextView inflateKeyPreviewText() {
824         final Context context = getContext();
825         if (mKeyPreviewLayoutId != 0) {
826             return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
827         } else {
828             return new TextView(context);
829         }
830     }
831 
832     @Override
showKeyPreview(int keyIndex, PointerTracker tracker)833     public void showKeyPreview(int keyIndex, PointerTracker tracker) {
834         if (mShowKeyPreviewPopup) {
835             mDrawingHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
836         }
837     }
838 
839     @Override
cancelShowKeyPreview(PointerTracker tracker)840     public void cancelShowKeyPreview(PointerTracker tracker) {
841         mDrawingHandler.cancelShowKeyPreview(tracker);
842     }
843 
844     @Override
dismissKeyPreview(PointerTracker tracker)845     public void dismissKeyPreview(PointerTracker tracker) {
846         mDrawingHandler.cancelShowKeyPreview(tracker);
847         mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
848     }
849 
addKeyPreview(TextView keyPreview)850     private void addKeyPreview(TextView keyPreview) {
851         if (mPreviewPlacer == null) {
852             mPreviewPlacer = new RelativeLayout(getContext());
853             final ViewGroup windowContentView =
854                     (ViewGroup)getRootView().findViewById(android.R.id.content);
855             windowContentView.addView(mPreviewPlacer);
856         }
857         mPreviewPlacer.addView(
858                 keyPreview, FrameLayoutCompatUtils.newLayoutParam(mPreviewPlacer, 0, 0));
859     }
860 
showKey(final int keyIndex, PointerTracker tracker)861     private void showKey(final int keyIndex, PointerTracker tracker) {
862         final TextView previewText = tracker.getKeyPreviewText();
863         // If the key preview has no parent view yet, add it to the ViewGroup which can place
864         // key preview absolutely in SoftInputWindow.
865         if (previewText.getParent() == null) {
866             addKeyPreview(previewText);
867         }
868 
869         mDrawingHandler.cancelDismissKeyPreview(tracker);
870         final Key key = tracker.getKey(keyIndex);
871         // If keyIndex is invalid or IME is already closed, we must not show key preview.
872         // Trying to show key preview while root window is closed causes
873         // WindowManager.BadTokenException.
874         if (key == null)
875             return;
876 
877         final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
878         final int keyDrawX = key.mX + key.mVisualInsetsLeft;
879         final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
880         // What we show as preview should match what we show on key top in onBufferDraw().
881         if (key.mLabel != null) {
882             // TODO Should take care of temporaryShiftLabel here.
883             previewText.setCompoundDrawables(null, null, null, null);
884             if (key.mLabel.length() > 1) {
885                 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
886                 previewText.setTypeface(Typeface.DEFAULT_BOLD);
887             } else {
888                 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize);
889                 previewText.setTypeface(params.mKeyTextStyle);
890             }
891             previewText.setText(mKeyboard.adjustLabelCase(key.mLabel));
892         } else {
893             final Drawable previewIcon = key.getPreviewIcon();
894             previewText.setCompoundDrawables(null, null, null,
895                    previewIcon != null ? previewIcon : key.getIcon());
896             previewText.setText(null);
897         }
898         previewText.setBackgroundDrawable(params.mPreviewBackground);
899 
900         previewText.measure(
901                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
902         final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
903                 + previewText.getPaddingLeft() + previewText.getPaddingRight());
904         final int previewHeight = params.mPreviewHeight;
905         getLocationInWindow(params.mCoordinates);
906         int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + params.mCoordinates[0];
907         final int previewY = key.mY - previewHeight
908                 + params.mCoordinates[1] + params.mPreviewOffset;
909         if (previewX < 0 && params.mPreviewLeftBackground != null) {
910             previewText.setBackgroundDrawable(params.mPreviewLeftBackground);
911             previewX = 0;
912         } else if (previewX + previewWidth > getWidth() && params.mPreviewRightBackground != null) {
913             previewText.setBackgroundDrawable(params.mPreviewRightBackground);
914             previewX = getWidth() - previewWidth;
915         }
916 
917         // Set the preview background state
918         previewText.getBackground().setState(
919                 key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
920         previewText.setTextColor(params.mPreviewTextColor);
921         FrameLayoutCompatUtils.placeViewAt(
922                 previewText, previewX, previewY, previewWidth, previewHeight);
923         previewText.setVisibility(VISIBLE);
924     }
925 
926     /**
927      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
928      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
929      * draws the cached buffer.
930      * @see #invalidateKey(Key)
931      */
invalidateAllKeys()932     public void invalidateAllKeys() {
933         mDirtyRect.union(0, 0, getWidth(), getHeight());
934         mBufferNeedsUpdate = true;
935         invalidate();
936     }
937 
938     /**
939      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
940      * one key is changing it's content. Any changes that affect the position or size of the key
941      * may not be honored.
942      * @param key key in the attached {@link Keyboard}.
943      * @see #invalidateAllKeys
944      */
945     @Override
invalidateKey(Key key)946     public void invalidateKey(Key key) {
947         if (key == null)
948             return;
949         mInvalidatedKey = key;
950         final int x = key.mX + getPaddingLeft();
951         final int y = key.mY + getPaddingTop();
952         mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
953         mDirtyRect.union(mInvalidatedKeyRect);
954         mBufferNeedsUpdate = true;
955         invalidate(mInvalidatedKeyRect);
956     }
957 
closing()958     public void closing() {
959         PointerTracker.dismissAllKeyPreviews();
960         cancelAllMessages();
961 
962         mDirtyRect.union(0, 0, getWidth(), getHeight());
963         requestLayout();
964     }
965 
966     @Override
dismissMoreKeysPanel()967     public boolean dismissMoreKeysPanel() {
968         return false;
969     }
970 
purgeKeyboardAndClosing()971     public void purgeKeyboardAndClosing() {
972         mKeyboard = null;
973         closing();
974     }
975 
976     @Override
onDetachedFromWindow()977     protected void onDetachedFromWindow() {
978         super.onDetachedFromWindow();
979         closing();
980         if (mPreviewPlacer != null) {
981             mPreviewPlacer.removeAllViews();
982         }
983         if (mBuffer != null) {
984             mBuffer.recycle();
985             mBuffer = null;
986         }
987     }
988 }
989