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