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