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