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