KeyboardView.java revision 62d4a96497951d4de784156f86626efdd55813fe
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.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
26import android.graphics.Paint.Align;
27import android.graphics.PorterDuff;
28import android.graphics.Rect;
29import android.graphics.Region.Op;
30import android.graphics.Typeface;
31import android.graphics.drawable.Drawable;
32import android.os.Message;
33import android.util.AttributeSet;
34import android.util.TypedValue;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.TextView;
39
40import com.android.inputmethod.compat.FrameLayoutCompatUtils;
41import com.android.inputmethod.latin.LatinImeLogger;
42import com.android.inputmethod.latin.R;
43import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
44
45import java.util.HashMap;
46
47/**
48 * A view that renders a virtual {@link Keyboard}.
49 *
50 * @attr ref R.styleable#KeyboardView_backgroundDimAmount
51 * @attr ref R.styleable#KeyboardView_keyBackground
52 * @attr ref R.styleable#KeyboardView_keyLetterRatio
53 * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio
54 * @attr ref R.styleable#KeyboardView_keyLabelRatio
55 * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
56 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio
57 * @attr ref R.styleable#KeyboardView_keyHintLabelRatio
58 * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
59 * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
60 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterPadding
61 * @attr ref R.styleable#KeyboardView_keyTextStyle
62 * @attr ref R.styleable#KeyboardView_keyPreviewLayout
63 * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio
64 * @attr ref R.styleable#KeyboardView_keyPreviewOffset
65 * @attr ref R.styleable#KeyboardView_keyPreviewHeight
66 * @attr ref R.styleable#KeyboardView_keyTextColor
67 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
68 * @attr ref R.styleable#KeyboardView_keyHintLetterColor
69 * @attr ref R.styleable#KeyboardView_keyHintLabelColor
70 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor
71 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor
72 * @attr ref R.styleable#KeyboardView_shadowColor
73 * @attr ref R.styleable#KeyboardView_shadowRadius
74 */
75public class KeyboardView extends View implements PointerTracker.DrawingProxy {
76    // Miscellaneous constants
77    private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
78
79    // XML attribute
80    private final float mBackgroundDimAmount;
81
82    // HORIZONTAL ELLIPSIS "...", character for popup hint.
83    private static final String POPUP_HINT_CHAR = "\u2026";
84
85    // Main keyboard
86    private Keyboard mKeyboard;
87    private final KeyDrawParams mKeyDrawParams;
88
89    // Key preview
90    private final int mKeyPreviewLayoutId;
91    private final KeyPreviewDrawParams mKeyPreviewDrawParams;
92    private boolean mShowKeyPreviewPopup = true;
93    private final int mDelayBeforePreview;
94    private int mDelayAfterPreview;
95    private ViewGroup mPreviewPlacer;
96
97    // Drawing
98    /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/
99    private boolean mBufferNeedsUpdate;
100    /** The dirty region in the keyboard bitmap */
101    private final Rect mDirtyRect = new Rect();
102    /** The key to invalidate. */
103    private Key mInvalidatedKey;
104    /** The dirty region for single key drawing */
105    private final Rect mInvalidatedKeyRect = new Rect();
106    /** The keyboard bitmap buffer for faster updates */
107    private Bitmap mBuffer;
108    /** The canvas for the above mutable keyboard bitmap */
109    private Canvas mCanvas;
110    private final Paint mPaint = new Paint();
111    // This map caches key label text height in pixel as value and key label text size as map key.
112    private static final HashMap<Integer, Float> sTextHeightCache =
113            new HashMap<Integer, Float>();
114    // This map caches key label text width in pixel as value and key label text size as map key.
115    private static final HashMap<Integer, Float> sTextWidthCache =
116            new HashMap<Integer, Float>();
117    private static final String KEY_LABEL_REFERENCE_CHAR = "M";
118
119    private static final int MEASURESPEC_UNSPECIFIED = MeasureSpec.makeMeasureSpec(
120            0, MeasureSpec.UNSPECIFIED);
121
122    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
123
124    public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
125        private static final int MSG_SHOW_KEY_PREVIEW = 1;
126        private static final int MSG_DISMISS_KEY_PREVIEW = 2;
127
128        public DrawingHandler(KeyboardView outerInstance) {
129            super(outerInstance);
130        }
131
132        @Override
133        public void handleMessage(Message msg) {
134            final KeyboardView keyboardView = getOuterInstance();
135            if (keyboardView == null) return;
136            final PointerTracker tracker = (PointerTracker) msg.obj;
137            switch (msg.what) {
138            case MSG_SHOW_KEY_PREVIEW:
139                keyboardView.showKey(msg.arg1, tracker);
140                break;
141            case MSG_DISMISS_KEY_PREVIEW:
142                tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
143                break;
144            }
145        }
146
147        public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
148            removeMessages(MSG_SHOW_KEY_PREVIEW);
149            final KeyboardView keyboardView = getOuterInstance();
150            if (keyboardView == null) return;
151            if (tracker.getKeyPreviewText().getVisibility() == VISIBLE || delay == 0) {
152                // Show right away, if it's already visible and finger is moving around
153                keyboardView.showKey(keyIndex, tracker);
154            } else {
155                sendMessageDelayed(
156                        obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
157            }
158        }
159
160        public void cancelShowKeyPreview(PointerTracker tracker) {
161            removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
162        }
163
164        public void cancelAllShowKeyPreviews() {
165            removeMessages(MSG_SHOW_KEY_PREVIEW);
166        }
167
168        public void dismissKeyPreview(long delay, PointerTracker tracker) {
169            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
170        }
171
172        public void cancelDismissKeyPreview(PointerTracker tracker) {
173            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
174        }
175
176        public void cancelAllDismissKeyPreviews() {
177            removeMessages(MSG_DISMISS_KEY_PREVIEW);
178        }
179
180        public void cancelAllMessages() {
181            cancelAllShowKeyPreviews();
182            cancelAllDismissKeyPreviews();
183        }
184    }
185
186    private static class KeyDrawParams {
187        // XML attributes
188        public final int mKeyTextColor;
189        public final int mKeyTextInactivatedColor;
190        public final Typeface mKeyTextStyle;
191        public final float mKeyLabelHorizontalPadding;
192        public final float mKeyHintLetterPadding;
193        public final float mKeyUppercaseLetterPadding;
194        public final int mShadowColor;
195        public final float mShadowRadius;
196        public final Drawable mKeyBackground;
197        public final int mKeyHintLetterColor;
198        public final int mKeyHintLabelColor;
199        public final int mKeyUppercaseLetterInactivatedColor;
200        public final int mKeyUppercaseLetterActivatedColor;
201
202        private final float mKeyLetterRatio;
203        private final float mKeyLargeLetterRatio;
204        private final float mKeyLabelRatio;
205        private final float mKeyHintLetterRatio;
206        private final float mKeyUppercaseLetterRatio;
207        private final float mKeyHintLabelRatio;
208
209        public final Rect mPadding = new Rect();
210        public int mKeyLetterSize;
211        public int mKeyLargeLetterSize;
212        public int mKeyLabelSize;
213        public int mKeyHintLetterSize;
214        public int mKeyUppercaseLetterSize;
215        public int mKeyHintLabelSize;
216
217        public KeyDrawParams(TypedArray a) {
218            mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
219            mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
220            mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
221            mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
222            mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
223            mKeyUppercaseLetterRatio = getRatio(a,
224                    R.styleable.KeyboardView_keyUppercaseLetterRatio);
225            mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
226            mKeyLabelHorizontalPadding = a.getDimension(
227                    R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
228            mKeyHintLetterPadding = a.getDimension(
229                    R.styleable.KeyboardView_keyHintLetterPadding, 0);
230            mKeyUppercaseLetterPadding = a.getDimension(
231                    R.styleable.KeyboardView_keyUppercaseLetterPadding, 0);
232            mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
233            mKeyTextInactivatedColor = a.getColor(
234                    R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
235            mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
236            mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
237            mKeyUppercaseLetterInactivatedColor = a.getColor(
238                    R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0);
239            mKeyUppercaseLetterActivatedColor = a.getColor(
240                    R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0);
241            mKeyTextStyle = Typeface.defaultFromStyle(
242                    a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
243            mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
244            mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
245
246            mKeyBackground.getPadding(mPadding);
247        }
248
249        public void updateKeyHeight(int keyHeight) {
250            mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
251            mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
252            mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
253            mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
254            mKeyUppercaseLetterSize = (int)(keyHeight * mKeyUppercaseLetterRatio);
255            mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
256        }
257    }
258
259    private static class KeyPreviewDrawParams {
260        // XML attributes.
261        public final Drawable mPreviewBackground;
262        public final Drawable mPreviewLeftBackground;
263        public final Drawable mPreviewRightBackground;
264        public final int mPreviewTextColor;
265        public final int mPreviewOffset;
266        public final int mPreviewHeight;
267        public final Typeface mKeyTextStyle;
268
269        private final float mPreviewTextRatio;
270        private final float mKeyLetterRatio;
271
272        public int mPreviewTextSize;
273        public int mKeyLetterSize;
274        public final int[] mCoordinates = new int[2];
275
276        private static final int PREVIEW_ALPHA = 240;
277
278        public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) {
279            mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
280            mPreviewLeftBackground = a.getDrawable(
281                    R.styleable.KeyboardView_keyPreviewLeftBackground);
282            mPreviewRightBackground = a.getDrawable(
283                    R.styleable.KeyboardView_keyPreviewRightBackground);
284            setAlpha(mPreviewBackground, PREVIEW_ALPHA);
285            setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA);
286            setAlpha(mPreviewRightBackground, PREVIEW_ALPHA);
287            mPreviewOffset = a.getDimensionPixelOffset(
288                    R.styleable.KeyboardView_keyPreviewOffset, 0);
289            mPreviewHeight = a.getDimensionPixelSize(
290                    R.styleable.KeyboardView_keyPreviewHeight, 80);
291            mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
292            mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
293
294            mKeyLetterRatio = keyDrawParams.mKeyLetterRatio;
295            mKeyTextStyle = keyDrawParams.mKeyTextStyle;
296        }
297
298        public void updateKeyHeight(int keyHeight) {
299            mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
300            mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
301        }
302
303        private static void setAlpha(Drawable drawable, int alpha) {
304            if (drawable == null)
305                return;
306            drawable.setAlpha(alpha);
307        }
308    }
309
310    public KeyboardView(Context context, AttributeSet attrs) {
311        this(context, attrs, R.attr.keyboardViewStyle);
312    }
313
314    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
315        super(context, attrs, defStyle);
316
317        final TypedArray a = context.obtainStyledAttributes(
318                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
319
320        mKeyDrawParams = new KeyDrawParams(a);
321        mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
322        mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
323        if (mKeyPreviewLayoutId == 0) {
324            mShowKeyPreviewPopup = false;
325        }
326        mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f);
327        a.recycle();
328
329        final Resources res = getResources();
330
331        mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
332        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
333
334        mPaint.setAntiAlias(true);
335        mPaint.setTextAlign(Align.CENTER);
336        mPaint.setAlpha(255);
337    }
338
339    // Read fraction value in TypedArray as float.
340    private static float getRatio(TypedArray a, int index) {
341        return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
342    }
343
344    /**
345     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
346     * view will re-layout itself to accommodate the keyboard.
347     * @see Keyboard
348     * @see #getKeyboard()
349     * @param keyboard the keyboard to display in this view
350     */
351    public void setKeyboard(Keyboard keyboard) {
352        // Remove any pending messages, except dismissing preview
353        mDrawingHandler.cancelAllShowKeyPreviews();
354        mKeyboard = keyboard;
355        LatinImeLogger.onSetKeyboard(keyboard);
356        requestLayout();
357        mDirtyRect.set(0, 0, getWidth(), getHeight());
358        mBufferNeedsUpdate = true;
359        invalidateAllKeys();
360        final int keyHeight = keyboard.mDefaultRowHeight - keyboard.mVerticalGap;
361        mKeyDrawParams.updateKeyHeight(keyHeight);
362        mKeyPreviewDrawParams.updateKeyHeight(keyHeight);
363    }
364
365    /**
366     * Returns the current keyboard being displayed by this view.
367     * @return the currently attached keyboard
368     * @see #setKeyboard(Keyboard)
369     */
370    public Keyboard getKeyboard() {
371        return mKeyboard;
372    }
373
374    /**
375     * Enables or disables the key feedback popup. This is a popup that shows a magnified
376     * version of the depressed key. By default the preview is enabled.
377     * @param previewEnabled whether or not to enable the key feedback preview
378     * @param delay the delay after which the preview is dismissed
379     * @see #isKeyPreviewPopupEnabled()
380     */
381    public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
382        mShowKeyPreviewPopup = previewEnabled;
383        mDelayAfterPreview = delay;
384    }
385
386    /**
387     * Returns the enabled state of the key feedback preview
388     * @return whether or not the key feedback preview is enabled
389     * @see #setKeyPreviewPopupEnabled(boolean, int)
390     */
391    public boolean isKeyPreviewPopupEnabled() {
392        return mShowKeyPreviewPopup;
393    }
394
395    @Override
396    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
397        if (mKeyboard != null) {
398            // The main keyboard expands to the display width.
399            final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
400            setMeasuredDimension(widthMeasureSpec, height);
401        } else {
402            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
403        }
404    }
405
406    @Override
407    public void onDraw(Canvas canvas) {
408        super.onDraw(canvas);
409        if (mBufferNeedsUpdate || mBuffer == null) {
410            mBufferNeedsUpdate = false;
411            onBufferDraw();
412        }
413        canvas.drawBitmap(mBuffer, 0, 0, null);
414    }
415
416    private void onBufferDraw() {
417        final int width = getWidth();
418        final int height = getHeight();
419        if (width == 0 || height == 0)
420            return;
421        if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
422            if (mBuffer != null)
423                mBuffer.recycle();
424            mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
425            mDirtyRect.union(0, 0, width, height);
426            if (mCanvas != null) {
427                mCanvas.setBitmap(mBuffer);
428            } else {
429                mCanvas = new Canvas(mBuffer);
430            }
431        }
432        final Canvas canvas = mCanvas;
433        canvas.clipRect(mDirtyRect, Op.REPLACE);
434        canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
435
436        if (mKeyboard == null) return;
437
438        final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
439        final KeyDrawParams params = mKeyDrawParams;
440        if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
441            // Draw a single key.
442            final int keyDrawX = mInvalidatedKey.mX + mInvalidatedKey.mVisualInsetsLeft
443                    + getPaddingLeft();
444            final int keyDrawY = mInvalidatedKey.mY + getPaddingTop();
445            canvas.translate(keyDrawX, keyDrawY);
446            onBufferDrawKey(mInvalidatedKey, mKeyboard, canvas, mPaint, params,
447                    isManualTemporaryUpperCase);
448            canvas.translate(-keyDrawX, -keyDrawY);
449        } else {
450            // Draw all keys.
451            for (final Key key : mKeyboard.mKeys) {
452                final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft();
453                final int keyDrawY = key.mY + getPaddingTop();
454                canvas.translate(keyDrawX, keyDrawY);
455                onBufferDrawKey(key, mKeyboard, canvas, mPaint, params, isManualTemporaryUpperCase);
456                canvas.translate(-keyDrawX, -keyDrawY);
457            }
458        }
459
460        // Overlay a dark rectangle to dim the keyboard
461        if (needsToDimKeyboard()) {
462            mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
463            canvas.drawRect(0, 0, width, height, mPaint);
464        }
465
466        mInvalidatedKey = null;
467        mDirtyRect.setEmpty();
468    }
469
470    protected boolean needsToDimKeyboard() {
471        return false;
472    }
473
474    private static void onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas,
475            Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase) {
476        final boolean debugShowAlign = LatinImeLogger.sVISUALDEBUG;
477        // Draw key background.
478        final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight
479                + params.mPadding.left + params.mPadding.right;
480        final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom;
481        final int bgX = -params.mPadding.left;
482        final int bgY = -params.mPadding.top;
483        final int[] drawableState = key.getCurrentDrawableState();
484        final Drawable background = params.mKeyBackground;
485        background.setState(drawableState);
486        final Rect bounds = background.getBounds();
487        if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
488            background.setBounds(0, 0, bgWidth, bgHeight);
489        }
490        canvas.translate(bgX, bgY);
491        background.draw(canvas);
492        if (debugShowAlign) {
493            drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
494        }
495        canvas.translate(-bgX, -bgY);
496
497        // Draw key top visuals.
498        final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
499        final int keyHeight = key.mHeight;
500        final float centerX = keyWidth * 0.5f;
501        final float centerY = keyHeight * 0.5f;
502
503        if (debugShowAlign) {
504            drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
505        }
506
507        // Draw key label.
508        final Drawable icon = key.getIcon();
509        float positionX = centerX;
510        if (key.mLabel != null) {
511            // Switch the character to uppercase if shift is pressed
512            final CharSequence label = keyboard.adjustLabelCase(key.mLabel);
513            // For characters, use large font. For labels like "Done", use smaller font.
514            paint.setTypeface(key.selectTypeface(params.mKeyTextStyle));
515            final int labelSize = key.selectTextSize(params.mKeyLetterSize,
516                    params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyHintLabelSize);
517            paint.setTextSize(labelSize);
518            final float labelCharHeight = getCharHeight(paint);
519            final float labelCharWidth = getCharWidth(paint);
520
521            // Vertical label text alignment.
522            final float baseline = centerY + labelCharHeight / 2;
523
524            // Horizontal label text alignment
525            float labelWidth = 0;
526            if (key.isAlignLeft()) {
527                positionX = (int)params.mKeyLabelHorizontalPadding;
528                paint.setTextAlign(Align.LEFT);
529            } else if (key.isAlignRight()) {
530                positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding;
531                paint.setTextAlign(Align.RIGHT);
532            } else if (key.isAlignLeftOfCenter()) {
533                // TODO: Parameterise this?
534                positionX = centerX - labelCharWidth * 7 / 4;
535                paint.setTextAlign(Align.LEFT);
536            } else if (key.hasLabelWithIconLeft() && icon != null) {
537                labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth();
538                positionX = centerX + labelWidth / 2;
539                paint.setTextAlign(Align.RIGHT);
540            } else if (key.hasLabelWithIconRight() && icon != null) {
541                labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth();
542                positionX = centerX - labelWidth / 2;
543                paint.setTextAlign(Align.LEFT);
544            } else {
545                positionX = centerX;
546                paint.setTextAlign(Align.CENTER);
547            }
548
549            if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) {
550                paint.setColor(params.mKeyTextInactivatedColor);
551            } else {
552                paint.setColor(params.mKeyTextColor);
553            }
554            if (key.isEnabled()) {
555                // Set a drop shadow for the text
556                paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor);
557            } else {
558                // Make label invisible
559                paint.setColor(Color.TRANSPARENT);
560            }
561            canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
562            // Turn off drop shadow
563            paint.setShadowLayer(0, 0, 0, 0);
564
565            if (icon != null) {
566                final int iconWidth = icon.getIntrinsicWidth();
567                final int iconHeight = icon.getIntrinsicHeight();
568                final int iconY = (keyHeight - iconHeight) / 2;
569                if (key.hasLabelWithIconLeft()) {
570                    final int iconX = (int)(centerX - labelWidth / 2);
571                    drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
572                } else if (key.hasLabelWithIconRight()) {
573                    final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
574                    drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
575                }
576            }
577
578            if (debugShowAlign) {
579                final Paint line = new Paint();
580                drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line);
581                drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line);
582            }
583        }
584
585        // Draw hint label.
586        if (key.mHintLabel != null) {
587            final CharSequence hint = key.mHintLabel;
588            final int hintColor;
589            final int hintSize;
590            if (key.hasHintLabel()) {
591                hintColor = params.mKeyHintLabelColor;
592                hintSize = params.mKeyHintLabelSize;
593                paint.setTypeface(Typeface.DEFAULT);
594            } else if (key.hasUppercaseLetter()) {
595                hintColor = isManualTemporaryUpperCase
596                        ? params.mKeyUppercaseLetterActivatedColor
597                        : params.mKeyUppercaseLetterInactivatedColor;
598                hintSize = params.mKeyUppercaseLetterSize;
599            } else { // key.hasHintLetter()
600                hintColor = params.mKeyHintLetterColor;
601                hintSize = params.mKeyHintLetterSize;
602            }
603            paint.setColor(hintColor);
604            paint.setTextSize(hintSize);
605            final float hintCharWidth = getCharWidth(paint);
606            final float hintX, hintY;
607            if (key.hasHintLabel()) {
608                // TODO: Generalize the following calculations.
609                hintX = positionX + hintCharWidth * 2;
610                hintY = centerY + getCharHeight(paint) / 2;
611                paint.setTextAlign(Align.LEFT);
612            } else if (key.hasUppercaseLetter()) {
613                hintX = keyWidth - params.mKeyUppercaseLetterPadding - hintCharWidth / 2;
614                hintY = -paint.ascent() + params.mKeyUppercaseLetterPadding;
615                paint.setTextAlign(Align.CENTER);
616            } else { // key.hasHintLetter()
617                hintX = keyWidth - params.mKeyHintLetterPadding - hintCharWidth / 2;
618                hintY = -paint.ascent() + params.mKeyHintLetterPadding;
619                paint.setTextAlign(Align.CENTER);
620            }
621            canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint);
622
623            if (debugShowAlign) {
624                final Paint line = new Paint();
625                drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
626                drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
627            }
628        }
629
630        // Draw key icon.
631        if (key.mLabel == null && icon != null) {
632            final int iconWidth = icon.getIntrinsicWidth();
633            final int iconHeight = icon.getIntrinsicHeight();
634            final int iconX, alignX;
635            final int iconY = (keyHeight - iconHeight) / 2;
636            if (key.isAlignLeft()) {
637                iconX = (int)params.mKeyLabelHorizontalPadding;
638                alignX = iconX;
639            } else if (key.isAlignRight()) {
640                iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth;
641                alignX = iconX + iconWidth;
642            } else { // Align center
643                iconX = (keyWidth - iconWidth) / 2;
644                alignX = iconX + iconWidth / 2;
645            }
646            drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
647
648            if (debugShowAlign) {
649                final Paint line = new Paint();
650                drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line);
651                drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line);
652            }
653        }
654
655        // Draw popup hint "..." at the bottom right corner of the key.
656        if (key.hasPopupHint() && key.mPopupCharacters != null && key.mPopupCharacters.length > 0) {
657            paint.setTextSize(params.mKeyHintLetterSize);
658            paint.setColor(params.mKeyHintLabelColor);
659            paint.setTextAlign(Align.CENTER);
660            final float hintX = keyWidth - params.mKeyHintLetterPadding - getCharWidth(paint) / 2;
661            final float hintY = keyHeight - params.mKeyHintLetterPadding;
662            canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
663
664            if (debugShowAlign) {
665                final Paint line = new Paint();
666                drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
667                drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
668            }
669        }
670    }
671
672    // This method is currently being used only by MiniKeyboardBuilder
673    public int getDefaultLabelSizeAndSetPaint(Paint paint) {
674        // For characters, use large font. For labels like "Done", use small font.
675        final int labelSize = mKeyDrawParams.mKeyLabelSize;
676        paint.setTextSize(labelSize);
677        paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
678        return labelSize;
679    }
680
681    private static final Rect sTextBounds = new Rect();
682
683    private static float getCharHeight(Paint paint) {
684        final int labelSize = (int)paint.getTextSize();
685        final Float cachedValue = sTextHeightCache.get(labelSize);
686        if (cachedValue != null)
687            return cachedValue;
688
689        paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, sTextBounds);
690        final float height = sTextBounds.height();
691        sTextHeightCache.put(labelSize, height);
692        return height;
693    }
694
695    private static float getCharWidth(Paint paint) {
696        final int labelSize = (int)paint.getTextSize();
697        final Typeface face = paint.getTypeface();
698        final Integer key;
699        if (face == Typeface.DEFAULT) {
700            key = labelSize;
701        } else if (face == Typeface.DEFAULT_BOLD) {
702            key = labelSize + 1000;
703        } else if (face == Typeface.MONOSPACE) {
704            key = labelSize + 2000;
705        } else {
706            key = labelSize;
707        }
708
709        final Float cached = sTextWidthCache.get(key);
710        if (cached != null)
711            return cached;
712
713        paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, sTextBounds);
714        final float width = sTextBounds.width();
715        sTextWidthCache.put(key, width);
716        return width;
717    }
718
719    private static float getLabelWidth(CharSequence label, Paint paint) {
720        paint.getTextBounds(label.toString(), 0, label.length(), sTextBounds);
721        return sTextBounds.width();
722    }
723
724    private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
725            int height) {
726        canvas.translate(x, y);
727        icon.setBounds(0, 0, width, height);
728        icon.draw(canvas);
729        canvas.translate(-x, -y);
730    }
731
732    private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, Paint paint) {
733        paint.setStyle(Paint.Style.STROKE);
734        paint.setStrokeWidth(1.0f);
735        paint.setColor(color);
736        canvas.drawLine(0, y, w, y, paint);
737    }
738
739    private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) {
740        paint.setStyle(Paint.Style.STROKE);
741        paint.setStrokeWidth(1.0f);
742        paint.setColor(color);
743        canvas.drawLine(x, 0, x, h, paint);
744    }
745
746    private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color,
747            Paint paint) {
748        paint.setStyle(Paint.Style.STROKE);
749        paint.setStrokeWidth(1.0f);
750        paint.setColor(color);
751        canvas.translate(x, y);
752        canvas.drawRect(0, 0, w, h, paint);
753        canvas.translate(-x, -y);
754    }
755
756    public void cancelAllMessages() {
757        mDrawingHandler.cancelAllMessages();
758    }
759
760    // Called by {@link PointerTracker} constructor to create a TextView.
761    @Override
762    public TextView inflateKeyPreviewText() {
763        final Context context = getContext();
764        if (mKeyPreviewLayoutId != 0) {
765            return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
766        } else {
767            return new TextView(context);
768        }
769    }
770
771    @Override
772    public void showKeyPreview(int keyIndex, PointerTracker tracker) {
773        if (mShowKeyPreviewPopup) {
774            mDrawingHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
775        }
776    }
777
778    @Override
779    public void cancelShowKeyPreview(PointerTracker tracker) {
780        mDrawingHandler.cancelShowKeyPreview(tracker);
781    }
782
783    @Override
784    public void dismissKeyPreview(PointerTracker tracker) {
785        mDrawingHandler.cancelShowKeyPreview(tracker);
786        mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
787    }
788
789    private void addKeyPreview(TextView keyPreview) {
790        if (mPreviewPlacer == null) {
791            mPreviewPlacer = FrameLayoutCompatUtils.getPlacer(
792                    (ViewGroup)getRootView().findViewById(android.R.id.content));
793        }
794        final ViewGroup placer = mPreviewPlacer;
795        placer.addView(keyPreview, FrameLayoutCompatUtils.newLayoutParam(placer, 0, 0));
796    }
797
798    // TODO: Introduce minimum duration for displaying key previews
799    private void showKey(final int keyIndex, PointerTracker tracker) {
800        final TextView previewText = tracker.getKeyPreviewText();
801        // If the key preview has no parent view yet, add it to the ViewGroup which can place
802        // key preview absolutely in SoftInputWindow.
803        if (previewText.getParent() == null) {
804            addKeyPreview(previewText);
805        }
806
807        mDrawingHandler.cancelDismissKeyPreview(tracker);
808        final Key key = tracker.getKey(keyIndex);
809        // If keyIndex is invalid or IME is already closed, we must not show key preview.
810        // Trying to show key preview while root window is closed causes
811        // WindowManager.BadTokenException.
812        if (key == null)
813            return;
814
815        final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
816        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
817        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
818        // What we show as preview should match what we show on key top in onBufferDraw().
819        if (key.mLabel != null) {
820            // TODO Should take care of temporaryShiftLabel here.
821            previewText.setCompoundDrawables(null, null, null, null);
822            if (key.mLabel.length() > 1) {
823                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
824                previewText.setTypeface(Typeface.DEFAULT_BOLD);
825            } else {
826                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize);
827                previewText.setTypeface(params.mKeyTextStyle);
828            }
829            previewText.setText(mKeyboard.adjustLabelCase(key.mLabel));
830        } else {
831            final Drawable previewIcon = key.getPreviewIcon();
832            previewText.setCompoundDrawables(null, null, null,
833                   previewIcon != null ? previewIcon : key.getIcon());
834            previewText.setText(null);
835        }
836        previewText.setBackgroundDrawable(params.mPreviewBackground);
837
838        previewText.measure(MEASURESPEC_UNSPECIFIED, MEASURESPEC_UNSPECIFIED);
839        final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
840                + previewText.getPaddingLeft() + previewText.getPaddingRight());
841        final int previewHeight = params.mPreviewHeight;
842        getLocationInWindow(params.mCoordinates);
843        int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + params.mCoordinates[0];
844        final int previewY = key.mY - previewHeight
845                + params.mCoordinates[1] + params.mPreviewOffset;
846        if (previewX < 0 && params.mPreviewLeftBackground != null) {
847            previewText.setBackgroundDrawable(params.mPreviewLeftBackground);
848            previewX = 0;
849        } else if (previewX + previewWidth > getWidth() && params.mPreviewRightBackground != null) {
850            previewText.setBackgroundDrawable(params.mPreviewRightBackground);
851            previewX = getWidth() - previewWidth;
852        }
853
854        // Set the preview background state
855        previewText.getBackground().setState(
856                key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
857        previewText.setTextColor(params.mPreviewTextColor);
858        FrameLayoutCompatUtils.placeViewAt(
859                previewText, previewX, previewY, previewWidth, previewHeight);
860        previewText.setVisibility(VISIBLE);
861    }
862
863    /**
864     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
865     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
866     * draws the cached buffer.
867     * @see #invalidateKey(Key)
868     */
869    public void invalidateAllKeys() {
870        mDirtyRect.union(0, 0, getWidth(), getHeight());
871        mBufferNeedsUpdate = true;
872        invalidate();
873    }
874
875    /**
876     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
877     * one key is changing it's content. Any changes that affect the position or size of the key
878     * may not be honored.
879     * @param key key in the attached {@link Keyboard}.
880     * @see #invalidateAllKeys
881     */
882    @Override
883    public void invalidateKey(Key key) {
884        if (key == null)
885            return;
886        mInvalidatedKey = key;
887        final int x = key.mX + getPaddingLeft();
888        final int y = key.mY + getPaddingTop();
889        mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
890        mDirtyRect.union(mInvalidatedKeyRect);
891        mBufferNeedsUpdate = true;
892        invalidate(mInvalidatedKeyRect);
893    }
894
895    public void closing() {
896        PointerTracker.dismissAllKeyPreviews();
897        cancelAllMessages();
898
899        mDirtyRect.union(0, 0, getWidth(), getHeight());
900        requestLayout();
901    }
902
903    @Override
904    public boolean dismissPopupPanel() {
905        return false;
906    }
907
908    public void purgeKeyboardAndClosing() {
909        mKeyboard = null;
910        closing();
911    }
912
913    @Override
914    public void onDetachedFromWindow() {
915        super.onDetachedFromWindow();
916        closing();
917    }
918}
919