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