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