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