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