• 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.TypedArray;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.Color;
24 import android.graphics.Paint;
25 import android.graphics.Paint.Align;
26 import android.graphics.PorterDuff;
27 import android.graphics.Rect;
28 import android.graphics.Region;
29 import android.graphics.Typeface;
30 import android.graphics.drawable.Drawable;
31 import android.os.Message;
32 import android.util.AttributeSet;
33 import android.util.DisplayMetrics;
34 import android.util.Log;
35 import android.util.SparseArray;
36 import android.util.TypedValue;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.TextView;
41 
42 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
43 import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
44 import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
45 import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
46 import com.android.inputmethod.latin.CollectionUtils;
47 import com.android.inputmethod.latin.Constants;
48 import com.android.inputmethod.latin.LatinImeLogger;
49 import com.android.inputmethod.latin.R;
50 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
51 import com.android.inputmethod.latin.StringUtils;
52 import com.android.inputmethod.latin.define.ProductionFlag;
53 import com.android.inputmethod.research.ResearchLogger;
54 
55 import java.util.HashSet;
56 
57 /**
58  * A view that renders a virtual {@link Keyboard}.
59  *
60  * @attr ref R.styleable#KeyboardView_keyBackground
61  * @attr ref R.styleable#KeyboardView_moreKeysLayout
62  * @attr ref R.styleable#KeyboardView_keyPreviewLayout
63  * @attr ref R.styleable#KeyboardView_keyPreviewOffset
64  * @attr ref R.styleable#KeyboardView_keyPreviewHeight
65  * @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout
66  * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
67  * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
68  * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
69  * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
70  * @attr ref R.styleable#KeyboardView_keyTextShadowRadius
71  * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
72  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize
73  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor
74  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset
75  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewColor
76  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewHorizontalPadding
77  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding
78  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius
79  * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout
80  * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay
81  * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration
82  * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval
83  * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor
84  * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth
85  * @attr ref R.styleable#KeyboardView_verticalCorrection
86  * @attr ref R.styleable#Keyboard_Key_keyTypeface
87  * @attr ref R.styleable#Keyboard_Key_keyLetterSize
88  * @attr ref R.styleable#Keyboard_Key_keyLabelSize
89  * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
90  * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
91  * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
92  * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
93  * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
94  * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
95  * @attr ref R.styleable#Keyboard_Key_keyTextColor
96  * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
97  * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
98  * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
99  * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
100  * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
101  * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
102  * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
103  */
104 public class KeyboardView extends View implements PointerTracker.DrawingProxy {
105     private static final String TAG = KeyboardView.class.getSimpleName();
106 
107     // XML attributes
108     protected final KeyVisualAttributes mKeyVisualAttributes;
109     private final int mKeyLabelHorizontalPadding;
110     private final float mKeyHintLetterPadding;
111     private final float mKeyPopupHintLetterPadding;
112     private final float mKeyShiftedLetterHintPadding;
113     private final float mKeyTextShadowRadius;
114     protected final float mVerticalCorrection;
115     protected final int mMoreKeysLayout;
116     protected final Drawable mKeyBackground;
117     protected final Rect mKeyBackgroundPadding = new Rect();
118     private final int mBackgroundDimAlpha;
119 
120     // HORIZONTAL ELLIPSIS "...", character for popup hint.
121     private static final String POPUP_HINT_CHAR = "\u2026";
122 
123     // Margin between the label and the icon on a key that has both of them.
124     // Specified by the fraction of the key width.
125     // TODO: Use resource parameter for this value.
126     private static final float LABEL_ICON_MARGIN = 0.05f;
127 
128     // The maximum key label width in the proportion to the key width.
129     private static final float MAX_LABEL_RATIO = 0.90f;
130 
131     // Main keyboard
132     private Keyboard mKeyboard;
133     protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
134 
135     // Preview placer view
136     private final PreviewPlacerView mPreviewPlacerView;
137     private final int[] mCoordinates = new int[2];
138 
139     // Key preview
140     private static final int PREVIEW_ALPHA = 240;
141     private final int mKeyPreviewLayoutId;
142     private final int mPreviewOffset;
143     private final int mPreviewHeight;
144     private final int mPreviewLingerTimeout;
145     private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
146     protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
147     private boolean mShowKeyPreviewPopup = true;
148     private int mDelayAfterPreview;
149     // Background state set
150     private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
151         { // STATE_MIDDLE
152             EMPTY_STATE_SET,
153             { R.attr.state_has_morekeys }
154         },
155         { // STATE_LEFT
156             { R.attr.state_left_edge },
157             { R.attr.state_left_edge, R.attr.state_has_morekeys }
158         },
159         { // STATE_RIGHT
160             { R.attr.state_right_edge },
161             { R.attr.state_right_edge, R.attr.state_has_morekeys }
162         }
163     };
164     private static final int STATE_MIDDLE = 0;
165     private static final int STATE_LEFT = 1;
166     private static final int STATE_RIGHT = 2;
167     private static final int STATE_NORMAL = 0;
168     private static final int STATE_HAS_MOREKEYS = 1;
169     private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE =
170             KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL];
171 
172     // Drawing
173     /** True if the entire keyboard needs to be dimmed. */
174     private boolean mNeedsToDimEntireKeyboard;
175     /** True if all keys should be drawn */
176     private boolean mInvalidateAllKeys;
177     /** The keys that should be drawn */
178     private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet();
179     /** The working rectangle variable */
180     private final Rect mWorkingRect = new Rect();
181     /** The keyboard bitmap buffer for faster updates */
182     /** The clip region to draw keys */
183     private final Region mClipRegion = new Region();
184     private Bitmap mOffscreenBuffer;
185     /** The canvas for the above mutable keyboard bitmap */
186     private final Canvas mOffscreenCanvas = new Canvas();
187     private final Paint mPaint = new Paint();
188     private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
189     // This sparse array caches key label text height in pixel indexed by key label text size.
190     private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
191     // This sparse array caches key label text width in pixel indexed by key label text size.
192     private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
193     private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
194     private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
195 
196     private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
197 
198     public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
199         private static final int MSG_DISMISS_KEY_PREVIEW = 0;
200 
DrawingHandler(final KeyboardView outerInstance)201         public DrawingHandler(final KeyboardView outerInstance) {
202             super(outerInstance);
203         }
204 
205         @Override
handleMessage(final Message msg)206         public void handleMessage(final Message msg) {
207             final KeyboardView keyboardView = getOuterInstance();
208             if (keyboardView == null) return;
209             final PointerTracker tracker = (PointerTracker) msg.obj;
210             switch (msg.what) {
211             case MSG_DISMISS_KEY_PREVIEW:
212                 final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId);
213                 if (previewText != null) {
214                     previewText.setVisibility(INVISIBLE);
215                 }
216                 break;
217             }
218         }
219 
dismissKeyPreview(final long delay, final PointerTracker tracker)220         public void dismissKeyPreview(final long delay, final PointerTracker tracker) {
221             sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
222         }
223 
cancelDismissKeyPreview(final PointerTracker tracker)224         public void cancelDismissKeyPreview(final PointerTracker tracker) {
225             removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
226         }
227 
cancelAllDismissKeyPreviews()228         private void cancelAllDismissKeyPreviews() {
229             removeMessages(MSG_DISMISS_KEY_PREVIEW);
230         }
231 
cancelAllMessages()232         public void cancelAllMessages() {
233             cancelAllDismissKeyPreviews();
234         }
235     }
236 
KeyboardView(final Context context, final AttributeSet attrs)237     public KeyboardView(final Context context, final AttributeSet attrs) {
238         this(context, attrs, R.attr.keyboardViewStyle);
239     }
240 
KeyboardView(final Context context, final AttributeSet attrs, final int defStyle)241     public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
242         super(context, attrs, defStyle);
243 
244         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
245                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
246         mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
247         mKeyBackground.getPadding(mKeyBackgroundPadding);
248         mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset(
249                 R.styleable.KeyboardView_keyPreviewOffset, 0);
250         mPreviewHeight = keyboardViewAttr.getDimensionPixelSize(
251                 R.styleable.KeyboardView_keyPreviewHeight, 80);
252         mPreviewLingerTimeout = keyboardViewAttr.getInt(
253                 R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
254         mDelayAfterPreview = mPreviewLingerTimeout;
255         mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset(
256                 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
257         mKeyHintLetterPadding = keyboardViewAttr.getDimension(
258                 R.styleable.KeyboardView_keyHintLetterPadding, 0);
259         mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
260                 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
261         mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
262                 R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
263         mKeyTextShadowRadius = keyboardViewAttr.getFloat(
264                 R.styleable.KeyboardView_keyTextShadowRadius, 0.0f);
265         mKeyPreviewLayoutId = keyboardViewAttr.getResourceId(
266                 R.styleable.KeyboardView_keyPreviewLayout, 0);
267         if (mKeyPreviewLayoutId == 0) {
268             mShowKeyPreviewPopup = false;
269         }
270         mVerticalCorrection = keyboardViewAttr.getDimension(
271                 R.styleable.KeyboardView_verticalCorrection, 0);
272         mMoreKeysLayout = keyboardViewAttr.getResourceId(
273                 R.styleable.KeyboardView_moreKeysLayout, 0);
274         mBackgroundDimAlpha = keyboardViewAttr.getInt(
275                 R.styleable.KeyboardView_backgroundDimAlpha, 0);
276         keyboardViewAttr.recycle();
277 
278         final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
279                 R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
280         mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
281         keyAttr.recycle();
282 
283         mPreviewPlacerView = new PreviewPlacerView(context, attrs);
284         mPaint.setAntiAlias(true);
285     }
286 
blendAlpha(final Paint paint, final int alpha)287     private static void blendAlpha(final Paint paint, final int alpha) {
288         final int color = paint.getColor();
289         paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
290                 Color.red(color), Color.green(color), Color.blue(color));
291     }
292 
293     /**
294      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
295      * view will re-layout itself to accommodate the keyboard.
296      * @see Keyboard
297      * @see #getKeyboard()
298      * @param keyboard the keyboard to display in this view
299      */
setKeyboard(final Keyboard keyboard)300     public void setKeyboard(final Keyboard keyboard) {
301         mKeyboard = keyboard;
302         LatinImeLogger.onSetKeyboard(keyboard);
303         requestLayout();
304         invalidateAllKeys();
305         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
306         mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
307         mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
308     }
309 
310     /**
311      * Returns the current keyboard being displayed by this view.
312      * @return the currently attached keyboard
313      * @see #setKeyboard(Keyboard)
314      */
getKeyboard()315     public Keyboard getKeyboard() {
316         return mKeyboard;
317     }
318 
319     /**
320      * Enables or disables the key feedback popup. This is a popup that shows a magnified
321      * version of the depressed key. By default the preview is enabled.
322      * @param previewEnabled whether or not to enable the key feedback preview
323      * @param delay the delay after which the preview is dismissed
324      * @see #isKeyPreviewPopupEnabled()
325      */
setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay)326     public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
327         mShowKeyPreviewPopup = previewEnabled;
328         mDelayAfterPreview = delay;
329     }
330 
331     /**
332      * Returns the enabled state of the key feedback preview
333      * @return whether or not the key feedback preview is enabled
334      * @see #setKeyPreviewPopupEnabled(boolean, int)
335      */
isKeyPreviewPopupEnabled()336     public boolean isKeyPreviewPopupEnabled() {
337         return mShowKeyPreviewPopup;
338     }
339 
setGesturePreviewMode(final boolean drawsGesturePreviewTrail, final boolean drawsGestureFloatingPreviewText)340     public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
341             final boolean drawsGestureFloatingPreviewText) {
342         mPreviewPlacerView.setGesturePreviewMode(
343                 drawsGesturePreviewTrail, drawsGestureFloatingPreviewText);
344     }
345 
346     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)347     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
348         if (mKeyboard != null) {
349             // The main keyboard expands to the display width.
350             final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
351             setMeasuredDimension(widthMeasureSpec, height);
352         } else {
353             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
354         }
355     }
356 
357     @Override
onDraw(final Canvas canvas)358     public void onDraw(final Canvas canvas) {
359         super.onDraw(canvas);
360         if (canvas.isHardwareAccelerated()) {
361             onDrawKeyboard(canvas);
362             return;
363         }
364 
365         final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
366         if (bufferNeedsUpdates || mOffscreenBuffer == null) {
367             if (maybeAllocateOffscreenBuffer()) {
368                 mInvalidateAllKeys = true;
369                 // TODO: Stop using the offscreen canvas even when in software rendering
370                 mOffscreenCanvas.setBitmap(mOffscreenBuffer);
371             }
372             onDrawKeyboard(mOffscreenCanvas);
373         }
374         canvas.drawBitmap(mOffscreenBuffer, 0, 0, null);
375     }
376 
maybeAllocateOffscreenBuffer()377     private boolean maybeAllocateOffscreenBuffer() {
378         final int width = getWidth();
379         final int height = getHeight();
380         if (width == 0 || height == 0) {
381             return false;
382         }
383         if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
384                 && mOffscreenBuffer.getHeight() == height) {
385             return false;
386         }
387         freeOffscreenBuffer();
388         mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
389         return true;
390     }
391 
freeOffscreenBuffer()392     private void freeOffscreenBuffer() {
393         if (mOffscreenBuffer != null) {
394             mOffscreenBuffer.recycle();
395             mOffscreenBuffer = null;
396         }
397     }
398 
onDrawKeyboard(final Canvas canvas)399     private void onDrawKeyboard(final Canvas canvas) {
400         if (mKeyboard == null) return;
401 
402         final int width = getWidth();
403         final int height = getHeight();
404         final Paint paint = mPaint;
405 
406         // Calculate clip region and set.
407         final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
408         final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
409         // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
410         if (drawAllKeys || isHardwareAccelerated) {
411             mClipRegion.set(0, 0, width, height);
412         } else {
413             mClipRegion.setEmpty();
414             for (final Key key : mInvalidatedKeys) {
415                 if (mKeyboard.hasKey(key)) {
416                     final int x = key.mX + getPaddingLeft();
417                     final int y = key.mY + getPaddingTop();
418                     mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
419                     mClipRegion.union(mWorkingRect);
420                 }
421             }
422         }
423         if (!isHardwareAccelerated) {
424             canvas.clipRegion(mClipRegion, Region.Op.REPLACE);
425             // Draw keyboard background.
426             canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
427             final Drawable background = getBackground();
428             if (background != null) {
429                 background.draw(canvas);
430             }
431         }
432 
433         // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
434         if (drawAllKeys || isHardwareAccelerated) {
435             // Draw all keys.
436             for (final Key key : mKeyboard.mKeys) {
437                 onDrawKey(key, canvas, paint);
438             }
439         } else {
440             // Draw invalidated keys.
441             for (final Key key : mInvalidatedKeys) {
442                 if (mKeyboard.hasKey(key)) {
443                     onDrawKey(key, canvas, paint);
444                 }
445             }
446         }
447 
448         // Overlay a dark rectangle to dim.
449         if (mNeedsToDimEntireKeyboard) {
450             paint.setColor(Color.BLACK);
451             paint.setAlpha(mBackgroundDimAlpha);
452             // Note: clipRegion() above is in effect if it was called.
453             canvas.drawRect(0, 0, width, height, paint);
454         }
455 
456         // ResearchLogging indicator.
457         // TODO: Reimplement using a keyboard background image specific to the ResearchLogger,
458         // and remove this call.
459         if (ProductionFlag.IS_EXPERIMENTAL) {
460             ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height);
461         }
462 
463         mInvalidatedKeys.clear();
464         mInvalidateAllKeys = false;
465     }
466 
dimEntireKeyboard(final boolean dimmed)467     public void dimEntireKeyboard(final boolean dimmed) {
468         final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
469         mNeedsToDimEntireKeyboard = dimmed;
470         if (needsRedrawing) {
471             invalidateAllKeys();
472         }
473     }
474 
onDrawKey(final Key key, final Canvas canvas, final Paint paint)475     private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
476         final int keyDrawX = key.getDrawX() + getPaddingLeft();
477         final int keyDrawY = key.mY + getPaddingTop();
478         canvas.translate(keyDrawX, keyDrawY);
479 
480         final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
481         final KeyVisualAttributes attr = key.mKeyVisualAttributes;
482         final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
483         params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
484 
485         if (!key.isSpacer()) {
486             onDrawKeyBackground(key, canvas);
487         }
488         onDrawKeyTopVisuals(key, canvas, paint, params);
489 
490         canvas.translate(-keyDrawX, -keyDrawY);
491     }
492 
493     // Draw key background.
onDrawKeyBackground(final Key key, final Canvas canvas)494     protected void onDrawKeyBackground(final Key key, final Canvas canvas) {
495         final Rect padding = mKeyBackgroundPadding;
496         final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
497         final int bgHeight = key.mHeight + padding.top + padding.bottom;
498         final int bgX = -padding.left;
499         final int bgY = -padding.top;
500         final int[] drawableState = key.getCurrentDrawableState();
501         final Drawable background = mKeyBackground;
502         background.setState(drawableState);
503         final Rect bounds = background.getBounds();
504         if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
505             background.setBounds(0, 0, bgWidth, bgHeight);
506         }
507         canvas.translate(bgX, bgY);
508         background.draw(canvas);
509         if (LatinImeLogger.sVISUALDEBUG) {
510             drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
511         }
512         canvas.translate(-bgX, -bgY);
513     }
514 
515     // Draw key top visuals.
onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, final KeyDrawParams params)516     protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
517             final KeyDrawParams params) {
518         final int keyWidth = key.getDrawWidth();
519         final int keyHeight = key.mHeight;
520         final float centerX = keyWidth * 0.5f;
521         final float centerY = keyHeight * 0.5f;
522 
523         if (LatinImeLogger.sVISUALDEBUG) {
524             drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
525         }
526 
527         // Draw key label.
528         final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha);
529         float positionX = centerX;
530         if (key.mLabel != null) {
531             final String label = key.mLabel;
532             paint.setTypeface(key.selectTypeface(params));
533             paint.setTextSize(key.selectTextSize(params));
534             final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
535             final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
536 
537             // Vertical label text alignment.
538             final float baseline = centerY + labelCharHeight / 2;
539 
540             // Horizontal label text alignment
541             float labelWidth = 0;
542             if (key.isAlignLeft()) {
543                 positionX = mKeyLabelHorizontalPadding;
544                 paint.setTextAlign(Align.LEFT);
545             } else if (key.isAlignRight()) {
546                 positionX = keyWidth - mKeyLabelHorizontalPadding;
547                 paint.setTextAlign(Align.RIGHT);
548             } else if (key.isAlignLeftOfCenter()) {
549                 // TODO: Parameterise this?
550                 positionX = centerX - labelCharWidth * 7 / 4;
551                 paint.setTextAlign(Align.LEFT);
552             } else if (key.hasLabelWithIconLeft() && icon != null) {
553                 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
554                         + LABEL_ICON_MARGIN * keyWidth;
555                 positionX = centerX + labelWidth / 2;
556                 paint.setTextAlign(Align.RIGHT);
557             } else if (key.hasLabelWithIconRight() && icon != null) {
558                 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
559                         + LABEL_ICON_MARGIN * keyWidth;
560                 positionX = centerX - labelWidth / 2;
561                 paint.setTextAlign(Align.LEFT);
562             } else {
563                 positionX = centerX;
564                 paint.setTextAlign(Align.CENTER);
565             }
566             if (key.needsXScale()) {
567                 paint.setTextScaleX(
568                         Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
569             }
570 
571             paint.setColor(key.selectTextColor(params));
572             if (key.isEnabled()) {
573                 // Set a drop shadow for the text
574                 paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor);
575             } else {
576                 // Make label invisible
577                 paint.setColor(Color.TRANSPARENT);
578             }
579             blendAlpha(paint, params.mAnimAlpha);
580             canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
581             // Turn off drop shadow and reset x-scale.
582             paint.setShadowLayer(0, 0, 0, 0);
583             paint.setTextScaleX(1.0f);
584 
585             if (icon != null) {
586                 final int iconWidth = icon.getIntrinsicWidth();
587                 final int iconHeight = icon.getIntrinsicHeight();
588                 final int iconY = (keyHeight - iconHeight) / 2;
589                 if (key.hasLabelWithIconLeft()) {
590                     final int iconX = (int)(centerX - labelWidth / 2);
591                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
592                 } else if (key.hasLabelWithIconRight()) {
593                     final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
594                     drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
595                 }
596             }
597 
598             if (LatinImeLogger.sVISUALDEBUG) {
599                 final Paint line = new Paint();
600                 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line);
601                 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line);
602             }
603         }
604 
605         // Draw hint label.
606         if (key.mHintLabel != null) {
607             final String hintLabel = key.mHintLabel;
608             paint.setTextSize(key.selectHintTextSize(params));
609             paint.setColor(key.selectHintTextColor(params));
610             blendAlpha(paint, params.mAnimAlpha);
611             final float hintX, hintY;
612             if (key.hasHintLabel()) {
613                 // The hint label is placed just right of the key label. Used mainly on
614                 // "phone number" layout.
615                 // TODO: Generalize the following calculations.
616                 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2;
617                 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
618                 paint.setTextAlign(Align.LEFT);
619             } else if (key.hasShiftedLetterHint()) {
620                 // The hint label is placed at top-right corner of the key. Used mainly on tablet.
621                 hintX = keyWidth - mKeyShiftedLetterHintPadding
622                         - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
623                 paint.getFontMetrics(mFontMetrics);
624                 hintY = -mFontMetrics.top;
625                 paint.setTextAlign(Align.CENTER);
626             } else { // key.hasHintLetter()
627                 // The hint letter is placed at top-right corner of the key. Used mainly on phone.
628                 hintX = keyWidth - mKeyHintLetterPadding
629                         - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
630                 hintY = -paint.ascent();
631                 paint.setTextAlign(Align.CENTER);
632             }
633             canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint);
634 
635             if (LatinImeLogger.sVISUALDEBUG) {
636                 final Paint line = new Paint();
637                 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
638                 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
639             }
640         }
641 
642         // Draw key icon.
643         if (key.mLabel == null && icon != null) {
644             final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
645             final int iconHeight = icon.getIntrinsicHeight();
646             final int iconX, alignX;
647             final int iconY = (keyHeight - iconHeight) / 2;
648             if (key.isAlignLeft()) {
649                 iconX = mKeyLabelHorizontalPadding;
650                 alignX = iconX;
651             } else if (key.isAlignRight()) {
652                 iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth;
653                 alignX = iconX + iconWidth;
654             } else { // Align center
655                 iconX = (keyWidth - iconWidth) / 2;
656                 alignX = iconX + iconWidth / 2;
657             }
658             drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
659 
660             if (LatinImeLogger.sVISUALDEBUG) {
661                 final Paint line = new Paint();
662                 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line);
663                 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line);
664             }
665         }
666 
667         if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) {
668             drawKeyPopupHint(key, canvas, paint, params);
669         }
670     }
671 
672     // Draw popup hint "..." at the bottom right corner of the key.
drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint, final KeyDrawParams params)673     protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
674             final KeyDrawParams params) {
675         final int keyWidth = key.getDrawWidth();
676         final int keyHeight = key.mHeight;
677 
678         paint.setTypeface(params.mTypeface);
679         paint.setTextSize(params.mHintLetterSize);
680         paint.setColor(params.mHintLabelColor);
681         paint.setTextAlign(Align.CENTER);
682         final float hintX = keyWidth - mKeyHintLetterPadding
683                 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
684         final float hintY = keyHeight - mKeyPopupHintLetterPadding;
685         canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
686 
687         if (LatinImeLogger.sVISUALDEBUG) {
688             final Paint line = new Paint();
689             drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
690             drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
691         }
692     }
693 
getCharGeometryCacheKey(final char referenceChar, final Paint paint)694     private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
695         final int labelSize = (int)paint.getTextSize();
696         final Typeface face = paint.getTypeface();
697         final int codePointOffset = referenceChar << 15;
698         if (face == Typeface.DEFAULT) {
699             return codePointOffset + labelSize;
700         } else if (face == Typeface.DEFAULT_BOLD) {
701             return codePointOffset + labelSize + 0x1000;
702         } else if (face == Typeface.MONOSPACE) {
703             return codePointOffset + labelSize + 0x2000;
704         } else {
705             return codePointOffset + labelSize;
706         }
707     }
708 
709     // Working variable for the following methods.
710     private final Rect mTextBounds = new Rect();
711 
getCharHeight(final char[] referenceChar, final Paint paint)712     private float getCharHeight(final char[] referenceChar, final Paint paint) {
713         final int key = getCharGeometryCacheKey(referenceChar[0], paint);
714         final Float cachedValue = sTextHeightCache.get(key);
715         if (cachedValue != null)
716             return cachedValue;
717 
718         paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
719         final float height = mTextBounds.height();
720         sTextHeightCache.put(key, height);
721         return height;
722     }
723 
getCharWidth(final char[] referenceChar, final Paint paint)724     private float getCharWidth(final char[] referenceChar, final Paint paint) {
725         final int key = getCharGeometryCacheKey(referenceChar[0], paint);
726         final Float cachedValue = sTextWidthCache.get(key);
727         if (cachedValue != null)
728             return cachedValue;
729 
730         paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
731         final float width = mTextBounds.width();
732         sTextWidthCache.put(key, width);
733         return width;
734     }
735 
736     // TODO: Remove this method.
getLabelWidth(final String label, final Paint paint)737     public float getLabelWidth(final String label, final Paint paint) {
738         paint.getTextBounds(label, 0, label.length(), mTextBounds);
739         return mTextBounds.width();
740     }
741 
drawIcon(final Canvas canvas, final Drawable icon, final int x, final int y, final int width, final int height)742     protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x,
743             final int y, final int width, final int height) {
744         canvas.translate(x, y);
745         icon.setBounds(0, 0, width, height);
746         icon.draw(canvas);
747         canvas.translate(-x, -y);
748     }
749 
drawHorizontalLine(final Canvas canvas, final float y, final float w, final int color, final Paint paint)750     private static void drawHorizontalLine(final Canvas canvas, final float y, final float w,
751             final int color, final Paint paint) {
752         paint.setStyle(Paint.Style.STROKE);
753         paint.setStrokeWidth(1.0f);
754         paint.setColor(color);
755         canvas.drawLine(0, y, w, y, paint);
756     }
757 
drawVerticalLine(final Canvas canvas, final float x, final float h, final int color, final Paint paint)758     private static void drawVerticalLine(final Canvas canvas, final float x, final float h,
759             final int color, final Paint paint) {
760         paint.setStyle(Paint.Style.STROKE);
761         paint.setStrokeWidth(1.0f);
762         paint.setColor(color);
763         canvas.drawLine(x, 0, x, h, paint);
764     }
765 
drawRectangle(final Canvas canvas, final float x, final float y, final float w, final float h, final int color, final Paint paint)766     private static void drawRectangle(final Canvas canvas, final float x, final float y,
767             final float w, final float h, final int color, final Paint paint) {
768         paint.setStyle(Paint.Style.STROKE);
769         paint.setStrokeWidth(1.0f);
770         paint.setColor(color);
771         canvas.translate(x, y);
772         canvas.drawRect(0, 0, w, h, paint);
773         canvas.translate(-x, -y);
774     }
775 
newDefaultLabelPaint()776     public Paint newDefaultLabelPaint() {
777         final Paint paint = new Paint();
778         paint.setAntiAlias(true);
779         paint.setTypeface(mKeyDrawParams.mTypeface);
780         paint.setTextSize(mKeyDrawParams.mLabelSize);
781         return paint;
782     }
783 
cancelAllMessages()784     public void cancelAllMessages() {
785         mDrawingHandler.cancelAllMessages();
786     }
787 
getKeyPreviewText(final int pointerId)788     private TextView getKeyPreviewText(final int pointerId) {
789         TextView previewText = mKeyPreviewTexts.get(pointerId);
790         if (previewText != null) {
791             return previewText;
792         }
793         final Context context = getContext();
794         if (mKeyPreviewLayoutId != 0) {
795             previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
796         } else {
797             previewText = new TextView(context);
798         }
799         mKeyPreviewTexts.put(pointerId, previewText);
800         return previewText;
801     }
802 
dismissAllKeyPreviews()803     private void dismissAllKeyPreviews() {
804         final int pointerCount = mKeyPreviewTexts.size();
805         for (int id = 0; id < pointerCount; id++) {
806             final TextView previewText = mKeyPreviewTexts.get(id);
807             if (previewText != null) {
808                 previewText.setVisibility(INVISIBLE);
809             }
810         }
811         PointerTracker.setReleasedKeyGraphicsToAllKeys();
812     }
813 
814     @Override
dismissKeyPreview(final PointerTracker tracker)815     public void dismissKeyPreview(final PointerTracker tracker) {
816         mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
817     }
818 
addKeyPreview(final TextView keyPreview)819     private void addKeyPreview(final TextView keyPreview) {
820         locatePreviewPlacerView();
821         mPreviewPlacerView.addView(
822                 keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
823     }
824 
locatePreviewPlacerView()825     private void locatePreviewPlacerView() {
826         if (mPreviewPlacerView.getParent() != null) {
827             return;
828         }
829         final int width = getWidth();
830         final int height = getHeight();
831         if (width == 0 || height == 0) {
832             // In transient state.
833             return;
834         }
835         final int[] viewOrigin = new int[2];
836         getLocationInWindow(viewOrigin);
837         final DisplayMetrics dm = getResources().getDisplayMetrics();
838         if (viewOrigin[1] < dm.heightPixels / 4) {
839             // In transient state.
840             return;
841         }
842         final View rootView = getRootView();
843         if (rootView == null) {
844             Log.w(TAG, "Cannot find root view");
845             return;
846         }
847         final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
848         // Note: It'd be very weird if we get null by android.R.id.content.
849         if (windowContentView == null) {
850             Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView");
851         } else {
852             windowContentView.addView(mPreviewPlacerView);
853             mPreviewPlacerView.setKeyboardViewGeometry(viewOrigin[0], viewOrigin[1], width, height);
854         }
855     }
856 
showGestureFloatingPreviewText(final String gestureFloatingPreviewText)857     public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
858         locatePreviewPlacerView();
859         mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText);
860     }
861 
dismissGestureFloatingPreviewText()862     public void dismissGestureFloatingPreviewText() {
863         locatePreviewPlacerView();
864         mPreviewPlacerView.dismissGestureFloatingPreviewText();
865     }
866 
867     @Override
showGesturePreviewTrail(final PointerTracker tracker, final boolean isOldestTracker)868     public void showGesturePreviewTrail(final PointerTracker tracker,
869             final boolean isOldestTracker) {
870         locatePreviewPlacerView();
871         mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker);
872     }
873 
874     @Override
showKeyPreview(final PointerTracker tracker)875     public void showKeyPreview(final PointerTracker tracker) {
876         final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
877         if (!mShowKeyPreviewPopup) {
878             previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap;
879             return;
880         }
881 
882         final TextView previewText = getKeyPreviewText(tracker.mPointerId);
883         // If the key preview has no parent view yet, add it to the ViewGroup which can place
884         // key preview absolutely in SoftInputWindow.
885         if (previewText.getParent() == null) {
886             addKeyPreview(previewText);
887         }
888 
889         mDrawingHandler.cancelDismissKeyPreview(tracker);
890         final Key key = tracker.getKey();
891         // If key is invalid or IME is already closed, we must not show key preview.
892         // Trying to show key preview while root window is closed causes
893         // WindowManager.BadTokenException.
894         if (key == null) {
895             return;
896         }
897 
898         final KeyDrawParams drawParams = mKeyDrawParams;
899         previewText.setTextColor(drawParams.mPreviewTextColor);
900         final Drawable background = previewText.getBackground();
901         if (background != null) {
902             background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE);
903             background.setAlpha(PREVIEW_ALPHA);
904         }
905         final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel;
906         // What we show as preview should match what we show on a key top in onDraw().
907         if (label != null) {
908             // TODO Should take care of temporaryShiftLabel here.
909             previewText.setCompoundDrawables(null, null, null, null);
910             if (StringUtils.codePointCount(label) > 1) {
911                 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize);
912                 previewText.setTypeface(Typeface.DEFAULT_BOLD);
913             } else {
914                 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize);
915                 previewText.setTypeface(key.selectTypeface(drawParams));
916             }
917             previewText.setText(label);
918         } else {
919             previewText.setCompoundDrawables(null, null, null,
920                     key.getPreviewIcon(mKeyboard.mIconsSet));
921             previewText.setText(null);
922         }
923 
924         previewText.measure(
925                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
926         final int keyDrawWidth = key.getDrawWidth();
927         final int previewWidth = previewText.getMeasuredWidth();
928         final int previewHeight = mPreviewHeight;
929         // The width and height of visible part of the key preview background. The content marker
930         // of the background 9-patch have to cover the visible part of the background.
931         previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
932                 - previewText.getPaddingRight();
933         previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
934                 - previewText.getPaddingBottom();
935         // The distance between the top edge of the parent key and the bottom of the visible part
936         // of the key preview background.
937         previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom();
938         getLocationInWindow(mCoordinates);
939         // The key preview is horizontally aligned with the center of the visible part of the
940         // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
941         // the left/right background is used if such background is specified.
942         final int statePosition;
943         int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
944         if (previewX < 0) {
945             previewX = 0;
946             statePosition = STATE_LEFT;
947         } else if (previewX > getWidth() - previewWidth) {
948             previewX = getWidth() - previewWidth;
949             statePosition = STATE_RIGHT;
950         } else {
951             statePosition = STATE_MIDDLE;
952         }
953         // The key preview is placed vertically above the top edge of the parent key with an
954         // arbitrary offset.
955         final int previewY = key.mY - previewHeight + mPreviewOffset + mCoordinates[1];
956 
957         if (background != null) {
958             final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
959             background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
960         }
961         ViewLayoutUtils.placeViewAt(
962                 previewText, previewX, previewY, previewWidth, previewHeight);
963         previewText.setVisibility(VISIBLE);
964     }
965 
966     /**
967      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
968      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
969      * draws the cached buffer.
970      * @see #invalidateKey(Key)
971      */
invalidateAllKeys()972     public void invalidateAllKeys() {
973         mInvalidatedKeys.clear();
974         mInvalidateAllKeys = true;
975         invalidate();
976     }
977 
978     /**
979      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
980      * one key is changing it's content. Any changes that affect the position or size of the key
981      * may not be honored.
982      * @param key key in the attached {@link Keyboard}.
983      * @see #invalidateAllKeys
984      */
985     @Override
invalidateKey(final Key key)986     public void invalidateKey(final Key key) {
987         if (mInvalidateAllKeys) return;
988         if (key == null) return;
989         mInvalidatedKeys.add(key);
990         final int x = key.mX + getPaddingLeft();
991         final int y = key.mY + getPaddingTop();
992         invalidate(x, y, x + key.mWidth, y + key.mHeight);
993     }
994 
closing()995     public void closing() {
996         dismissAllKeyPreviews();
997         cancelAllMessages();
998 
999         mInvalidateAllKeys = true;
1000         requestLayout();
1001     }
1002 
1003     @Override
dismissMoreKeysPanel()1004     public boolean dismissMoreKeysPanel() {
1005         return false;
1006     }
1007 
purgeKeyboardAndClosing()1008     public void purgeKeyboardAndClosing() {
1009         mKeyboard = null;
1010         closing();
1011     }
1012 
1013     @Override
onDetachedFromWindow()1014     protected void onDetachedFromWindow() {
1015         super.onDetachedFromWindow();
1016         closing();
1017         mPreviewPlacerView.removeAllViews();
1018         freeOffscreenBuffer();
1019     }
1020 }
1021