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