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