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