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