Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.content.res.XmlResourceParser;
     23 import android.graphics.Bitmap;
     24 import android.graphics.Canvas;
     25 import android.graphics.ColorFilter;
     26 import android.graphics.Paint;
     27 import android.graphics.Paint.Align;
     28 import android.graphics.PixelFormat;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.BitmapDrawable;
     32 import android.graphics.drawable.Drawable;
     33 import android.inputmethodservice.Keyboard;
     34 import android.text.TextPaint;
     35 import android.util.Log;
     36 import android.view.ViewConfiguration;
     37 import android.view.inputmethod.EditorInfo;
     38 
     39 import java.util.List;
     40 import java.util.Locale;
     41 
     42 public class LatinKeyboard extends Keyboard {
     43 
     44     private static final boolean DEBUG_PREFERRED_LETTER = false;
     45     private static final String TAG = "LatinKeyboard";
     46     private static final int OPACITY_FULLY_OPAQUE = 255;
     47     private static final int SPACE_LED_LENGTH_PERCENT = 80;
     48 
     49     private Drawable mShiftLockIcon;
     50     private Drawable mShiftLockPreviewIcon;
     51     private Drawable mOldShiftIcon;
     52     private Drawable mSpaceIcon;
     53     private Drawable mSpaceAutoCompletionIndicator;
     54     private Drawable mSpacePreviewIcon;
     55     private Drawable mMicIcon;
     56     private Drawable mMicPreviewIcon;
     57     private Drawable m123MicIcon;
     58     private Drawable m123MicPreviewIcon;
     59     private final Drawable mButtonArrowLeftIcon;
     60     private final Drawable mButtonArrowRightIcon;
     61     private Key mShiftKey;
     62     private Key mEnterKey;
     63     private Key mF1Key;
     64     private final Drawable mHintIcon;
     65     private Key mSpaceKey;
     66     private Key m123Key;
     67     private final int NUMBER_HINT_COUNT = 10;
     68     private Key[] mNumberHintKeys;
     69     private Drawable[] mNumberHintIcons = new Drawable[NUMBER_HINT_COUNT];
     70     private int mSpaceKeyIndex = -1;
     71     private int mSpaceDragStartX;
     72     private int mSpaceDragLastDiff;
     73     private Locale mLocale;
     74     private LanguageSwitcher mLanguageSwitcher;
     75     private final Resources mRes;
     76     private final Context mContext;
     77     private int mMode;
     78     // Whether this keyboard has voice icon on it
     79     private boolean mHasVoiceButton;
     80     // Whether voice icon is enabled at all
     81     private boolean mVoiceEnabled;
     82     private final boolean mIsAlphaKeyboard;
     83     private CharSequence m123Label;
     84     private boolean mCurrentlyInSpace;
     85     private SlidingLocaleDrawable mSlidingLocaleIcon;
     86     private int[] mPrefLetterFrequencies;
     87     private int mPrefLetter;
     88     private int mPrefLetterX;
     89     private int mPrefLetterY;
     90     private int mPrefDistance;
     91 
     92     // TODO: generalize for any keyboardId
     93     private boolean mIsBlackSym;
     94 
     95     // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes
     96     // non-private.
     97     private final int mVerticalGap;
     98 
     99     private static final int SHIFT_OFF = 0;
    100     private static final int SHIFT_ON = 1;
    101     private static final int SHIFT_LOCKED = 2;
    102 
    103     private int mShiftState = SHIFT_OFF;
    104 
    105     private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
    106     private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
    107     private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f;
    108     // Minimum width of space key preview (proportional to keyboard width)
    109     private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f;
    110     // Height in space key the language name will be drawn. (proportional to space key height)
    111     private static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
    112     // If the full language name needs to be smaller than this value to be drawn on space key,
    113     // its short language name will be used instead.
    114     private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
    115 
    116     private static int sSpacebarVerticalCorrection;
    117 
    118     public LatinKeyboard(Context context, int xmlLayoutResId) {
    119         this(context, xmlLayoutResId, 0);
    120     }
    121 
    122     public LatinKeyboard(Context context, int xmlLayoutResId, int mode) {
    123         super(context, xmlLayoutResId, mode);
    124         final Resources res = context.getResources();
    125         mContext = context;
    126         mMode = mode;
    127         mRes = res;
    128         mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
    129         mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked);
    130         setDefaultBounds(mShiftLockPreviewIcon);
    131         mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
    132         mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led);
    133         mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space);
    134         mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic);
    135         mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic);
    136         setDefaultBounds(mMicPreviewIcon);
    137         mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left);
    138         mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right);
    139         m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic);
    140         m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic);
    141         mHintIcon = res.getDrawable(R.drawable.hint_popup);
    142         setDefaultBounds(m123MicPreviewIcon);
    143         sSpacebarVerticalCorrection = res.getDimensionPixelOffset(
    144                 R.dimen.spacebar_vertical_correction);
    145         mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty
    146                 || xmlLayoutResId == R.xml.kbd_qwerty_black;
    147         mSpaceKeyIndex = indexOf(LatinIME.KEYCODE_SPACE);
    148         initializeNumberHintResources(context);
    149         // TODO remove this initialization after cleanup
    150         mVerticalGap = super.getVerticalGap();
    151     }
    152 
    153     private void initializeNumberHintResources(Context context) {
    154         final Resources res = context.getResources();
    155         mNumberHintIcons[0] = res.getDrawable(R.drawable.keyboard_hint_0);
    156         mNumberHintIcons[1] = res.getDrawable(R.drawable.keyboard_hint_1);
    157         mNumberHintIcons[2] = res.getDrawable(R.drawable.keyboard_hint_2);
    158         mNumberHintIcons[3] = res.getDrawable(R.drawable.keyboard_hint_3);
    159         mNumberHintIcons[4] = res.getDrawable(R.drawable.keyboard_hint_4);
    160         mNumberHintIcons[5] = res.getDrawable(R.drawable.keyboard_hint_5);
    161         mNumberHintIcons[6] = res.getDrawable(R.drawable.keyboard_hint_6);
    162         mNumberHintIcons[7] = res.getDrawable(R.drawable.keyboard_hint_7);
    163         mNumberHintIcons[8] = res.getDrawable(R.drawable.keyboard_hint_8);
    164         mNumberHintIcons[9] = res.getDrawable(R.drawable.keyboard_hint_9);
    165     }
    166 
    167     @Override
    168     protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
    169             XmlResourceParser parser) {
    170         Key key = new LatinKey(res, parent, x, y, parser);
    171         switch (key.codes[0]) {
    172         case LatinIME.KEYCODE_ENTER:
    173             mEnterKey = key;
    174             break;
    175         case LatinKeyboardView.KEYCODE_F1:
    176             mF1Key = key;
    177             break;
    178         case LatinIME.KEYCODE_SPACE:
    179             mSpaceKey = key;
    180             break;
    181         case KEYCODE_MODE_CHANGE:
    182             m123Key = key;
    183             m123Label = key.label;
    184             break;
    185         }
    186 
    187         // For number hints on the upper-right corner of key
    188         if (mNumberHintKeys == null) {
    189             // NOTE: This protected method is being called from the base class constructor before
    190             // mNumberHintKeys gets initialized.
    191             mNumberHintKeys = new Key[NUMBER_HINT_COUNT];
    192         }
    193         int hintNumber = -1;
    194         if (LatinKeyboardBaseView.isNumberAtLeftmostPopupChar(key)) {
    195             hintNumber = key.popupCharacters.charAt(0) - '0';
    196         } else if (LatinKeyboardBaseView.isNumberAtRightmostPopupChar(key)) {
    197             hintNumber = key.popupCharacters.charAt(key.popupCharacters.length() - 1) - '0';
    198         }
    199         if (hintNumber >= 0 && hintNumber <= 9) {
    200             mNumberHintKeys[hintNumber] = key;
    201         }
    202 
    203         return key;
    204     }
    205 
    206     void setImeOptions(Resources res, int mode, int options) {
    207         mMode = mode;
    208         // TODO should clean up this method
    209         if (mEnterKey != null) {
    210             // Reset some of the rarely used attributes.
    211             mEnterKey.popupCharacters = null;
    212             mEnterKey.popupResId = 0;
    213             mEnterKey.text = null;
    214             switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) {
    215                 case EditorInfo.IME_ACTION_GO:
    216                     mEnterKey.iconPreview = null;
    217                     mEnterKey.icon = null;
    218                     mEnterKey.label = res.getText(R.string.label_go_key);
    219                     break;
    220                 case EditorInfo.IME_ACTION_NEXT:
    221                     mEnterKey.iconPreview = null;
    222                     mEnterKey.icon = null;
    223                     mEnterKey.label = res.getText(R.string.label_next_key);
    224                     break;
    225                 case EditorInfo.IME_ACTION_DONE:
    226                     mEnterKey.iconPreview = null;
    227                     mEnterKey.icon = null;
    228                     mEnterKey.label = res.getText(R.string.label_done_key);
    229                     break;
    230                 case EditorInfo.IME_ACTION_SEARCH:
    231                     mEnterKey.iconPreview = res.getDrawable(
    232                             R.drawable.sym_keyboard_feedback_search);
    233                     mEnterKey.icon = res.getDrawable(mIsBlackSym ?
    234                             R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search);
    235                     mEnterKey.label = null;
    236                     break;
    237                 case EditorInfo.IME_ACTION_SEND:
    238                     mEnterKey.iconPreview = null;
    239                     mEnterKey.icon = null;
    240                     mEnterKey.label = res.getText(R.string.label_send_key);
    241                     break;
    242                 default:
    243                     if (mode == KeyboardSwitcher.MODE_IM) {
    244                         mEnterKey.icon = mHintIcon;
    245                         mEnterKey.iconPreview = null;
    246                         mEnterKey.label = ":-)";
    247                         mEnterKey.text = ":-) ";
    248                         mEnterKey.popupResId = R.xml.popup_smileys;
    249                     } else {
    250                         mEnterKey.iconPreview = res.getDrawable(
    251                                 R.drawable.sym_keyboard_feedback_return);
    252                         mEnterKey.icon = res.getDrawable(mIsBlackSym ?
    253                                 R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return);
    254                         mEnterKey.label = null;
    255                     }
    256                     break;
    257             }
    258             // Set the initial size of the preview icon
    259             if (mEnterKey.iconPreview != null) {
    260                 setDefaultBounds(mEnterKey.iconPreview);
    261             }
    262         }
    263     }
    264 
    265     void enableShiftLock() {
    266         int index = getShiftKeyIndex();
    267         if (index >= 0) {
    268             mShiftKey = getKeys().get(index);
    269             if (mShiftKey instanceof LatinKey) {
    270                 ((LatinKey)mShiftKey).enableShiftLock();
    271             }
    272             mOldShiftIcon = mShiftKey.icon;
    273         }
    274     }
    275 
    276     void setShiftLocked(boolean shiftLocked) {
    277         if (mShiftKey != null) {
    278             if (shiftLocked) {
    279                 mShiftKey.on = true;
    280                 mShiftKey.icon = mShiftLockIcon;
    281                 mShiftState = SHIFT_LOCKED;
    282             } else {
    283                 mShiftKey.on = false;
    284                 mShiftKey.icon = mShiftLockIcon;
    285                 mShiftState = SHIFT_ON;
    286             }
    287         }
    288     }
    289 
    290     boolean isShiftLocked() {
    291         return mShiftState == SHIFT_LOCKED;
    292     }
    293 
    294     @Override
    295     public boolean setShifted(boolean shiftState) {
    296         boolean shiftChanged = false;
    297         if (mShiftKey != null) {
    298             if (shiftState == false) {
    299                 shiftChanged = mShiftState != SHIFT_OFF;
    300                 mShiftState = SHIFT_OFF;
    301                 mShiftKey.on = false;
    302                 mShiftKey.icon = mOldShiftIcon;
    303             } else {
    304                 if (mShiftState == SHIFT_OFF) {
    305                     shiftChanged = mShiftState == SHIFT_OFF;
    306                     mShiftState = SHIFT_ON;
    307                     mShiftKey.icon = mShiftLockIcon;
    308                 }
    309             }
    310         } else {
    311             return super.setShifted(shiftState);
    312         }
    313         return shiftChanged;
    314     }
    315 
    316     @Override
    317     public boolean isShifted() {
    318         if (mShiftKey != null) {
    319             return mShiftState != SHIFT_OFF;
    320         } else {
    321             return super.isShifted();
    322         }
    323     }
    324 
    325     /* package */ boolean isAlphaKeyboard() {
    326         return mIsAlphaKeyboard;
    327     }
    328 
    329     public void setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack) {
    330         mIsBlackSym = isBlack;
    331         if (isBlack) {
    332             mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked);
    333             mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space);
    334             mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic);
    335             m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic);
    336         } else {
    337             mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked);
    338             mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space);
    339             mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic);
    340             m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic);
    341         }
    342         updateDynamicKeys();
    343         if (mSpaceKey != null) {
    344             updateSpaceBarForLocale(isAutoCompletion, isBlack);
    345         }
    346         updateNumberHintKeys();
    347     }
    348 
    349     private void setDefaultBounds(Drawable drawable) {
    350         drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    351     }
    352 
    353     public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) {
    354         mHasVoiceButton = hasVoiceButton;
    355         mVoiceEnabled = hasVoice;
    356         updateDynamicKeys();
    357     }
    358 
    359     private void updateDynamicKeys() {
    360         update123Key();
    361         updateF1Key();
    362     }
    363 
    364     private void update123Key() {
    365         // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode.
    366         if (m123Key != null && mIsAlphaKeyboard) {
    367             if (mVoiceEnabled && !mHasVoiceButton) {
    368                 m123Key.icon = m123MicIcon;
    369                 m123Key.iconPreview = m123MicPreviewIcon;
    370                 m123Key.label = null;
    371             } else {
    372                 m123Key.icon = null;
    373                 m123Key.iconPreview = null;
    374                 m123Key.label = m123Label;
    375             }
    376         }
    377     }
    378 
    379     private void updateF1Key() {
    380         // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key.
    381         if (mF1Key == null)
    382             return;
    383 
    384         if (mIsAlphaKeyboard) {
    385             if (mMode == KeyboardSwitcher.MODE_URL) {
    386                 setNonMicF1Key(mF1Key, "/", R.xml.popup_slash);
    387             } else if (mMode == KeyboardSwitcher.MODE_EMAIL) {
    388                 setNonMicF1Key(mF1Key, "@", R.xml.popup_at);
    389             } else {
    390                 if (mVoiceEnabled && mHasVoiceButton) {
    391                     setMicF1Key(mF1Key);
    392                 } else {
    393                     setNonMicF1Key(mF1Key, ",", R.xml.popup_comma);
    394                 }
    395             }
    396         } else {  // Symbols keyboard
    397             if (mVoiceEnabled && mHasVoiceButton) {
    398                 setMicF1Key(mF1Key);
    399             } else {
    400                 setNonMicF1Key(mF1Key, ",", R.xml.popup_comma);
    401             }
    402         }
    403     }
    404 
    405     private void setMicF1Key(Key key) {
    406         // HACK: draw mMicIcon and mHintIcon at the same time
    407         final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes,
    408                 drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon));
    409 
    410         key.label = null;
    411         key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE };
    412         key.popupResId = R.xml.popup_mic;
    413         key.icon = micWithSettingsHintDrawable;
    414         key.iconPreview = mMicPreviewIcon;
    415     }
    416 
    417     private void setNonMicF1Key(Key key, String label, int popupResId) {
    418         key.label = label;
    419         key.codes = new int[] { label.charAt(0) };
    420         key.popupResId = popupResId;
    421         key.icon = mHintIcon;
    422         key.iconPreview = null;
    423     }
    424 
    425     public boolean isF1Key(Key key) {
    426         return key == mF1Key;
    427     }
    428 
    429     public static boolean hasPuncOrSmileysPopup(Key key) {
    430         return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys;
    431     }
    432 
    433     /**
    434      * @return a key which should be invalidated.
    435      */
    436     public Key onAutoCompletionStateChanged(boolean isAutoCompletion) {
    437         updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym);
    438         return mSpaceKey;
    439     }
    440 
    441     private void updateNumberHintKeys() {
    442         for (int i = 0; i < mNumberHintKeys.length; ++i) {
    443             if (mNumberHintKeys[i] != null) {
    444                 mNumberHintKeys[i].icon = mNumberHintIcons[i];
    445             }
    446         }
    447     }
    448 
    449     public boolean isLanguageSwitchEnabled() {
    450         return mLocale != null;
    451     }
    452 
    453     private void updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack) {
    454         // If application locales are explicitly selected.
    455         if (mLocale != null) {
    456             mSpaceKey.icon = new BitmapDrawable(mRes,
    457                     drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack));
    458         } else {
    459             // sym_keyboard_space_led can be shared with Black and White symbol themes.
    460             if (isAutoCompletion) {
    461                 mSpaceKey.icon = new BitmapDrawable(mRes,
    462                         drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack));
    463             } else {
    464                 mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space)
    465                         : mRes.getDrawable(R.drawable.sym_keyboard_space);
    466             }
    467         }
    468     }
    469 
    470     // Compute width of text with specified text size using paint.
    471     private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
    472         paint.setTextSize(textSize);
    473         paint.getTextBounds(text, 0, text.length(), bounds);
    474         return bounds.width();
    475     }
    476 
    477     // Overlay two images: mainIcon and hintIcon.
    478     private Bitmap drawSynthesizedSettingsHintImage(
    479             int width, int height, Drawable mainIcon, Drawable hintIcon) {
    480         if (mainIcon == null || hintIcon == null)
    481             return null;
    482         Rect hintIconPadding = new Rect(0, 0, 0, 0);
    483         hintIcon.getPadding(hintIconPadding);
    484         final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    485         final Canvas canvas = new Canvas(buffer);
    486         canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR);
    487 
    488         // Draw main icon at the center of the key visual
    489         // Assuming the hintIcon shares the same padding with the key's background drawable
    490         final int drawableX = (width + hintIconPadding.left - hintIconPadding.right
    491                 - mainIcon.getIntrinsicWidth()) / 2;
    492         final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom
    493                 - mainIcon.getIntrinsicHeight()) / 2;
    494         setDefaultBounds(mainIcon);
    495         canvas.translate(drawableX, drawableY);
    496         mainIcon.draw(canvas);
    497         canvas.translate(-drawableX, -drawableY);
    498 
    499         // Draw hint icon fully in the key
    500         hintIcon.setBounds(0, 0, width, height);
    501         hintIcon.draw(canvas);
    502         return buffer;
    503     }
    504 
    505     // Layout local language name and left and right arrow on space bar.
    506     private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow,
    507             Drawable rArrow, int width, int height, float origTextSize,
    508             boolean allowVariableTextSize) {
    509         final float arrowWidth = lArrow.getIntrinsicWidth();
    510         final float arrowHeight = lArrow.getIntrinsicHeight();
    511         final float maxTextWidth = width - (arrowWidth + arrowWidth);
    512         final Rect bounds = new Rect();
    513 
    514         // Estimate appropriate language name text size to fit in maxTextWidth.
    515         String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale));
    516         int textWidth = getTextWidth(paint, language, origTextSize, bounds);
    517         // Assuming text width and text size are proportional to each other.
    518         float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
    519 
    520         final boolean useShortName;
    521         if (allowVariableTextSize) {
    522             textWidth = getTextWidth(paint, language, textSize, bounds);
    523             // If text size goes too small or text does not fit, use short name
    524             useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME
    525                     || textWidth > maxTextWidth;
    526         } else {
    527             useShortName = textWidth > maxTextWidth;
    528             textSize = origTextSize;
    529         }
    530         if (useShortName) {
    531             language = LanguageSwitcher.toTitleCase(locale.getLanguage());
    532             textWidth = getTextWidth(paint, language, origTextSize, bounds);
    533             textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
    534         }
    535         paint.setTextSize(textSize);
    536 
    537         // Place left and right arrow just before and after language text.
    538         final float baseline = height * SPACEBAR_LANGUAGE_BASELINE;
    539         final int top = (int)(baseline - arrowHeight);
    540         final float remains = (width - textWidth) / 2;
    541         lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline);
    542         rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth),
    543                 (int)baseline);
    544 
    545         return language;
    546     }
    547 
    548     private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack) {
    549         final int width = mSpaceKey.width;
    550         final int height = mSpaceIcon.getIntrinsicHeight();
    551         final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    552         final Canvas canvas = new Canvas(buffer);
    553         canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR);
    554 
    555         // If application locales are explicitly selected.
    556         if (mLocale != null) {
    557             final Paint paint = new Paint();
    558             paint.setAlpha(opacity);
    559             paint.setAntiAlias(true);
    560             paint.setTextAlign(Align.CENTER);
    561 
    562             final boolean allowVariableTextSize = true;
    563             final String language = layoutSpaceBar(paint, mLanguageSwitcher.getInputLocale(),
    564                     mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height,
    565                     getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14),
    566                     allowVariableTextSize);
    567 
    568             // Draw language text with shadow
    569             final int shadowColor = mRes.getColor(isBlack
    570                     ? R.color.latinkeyboard_bar_language_shadow_black
    571                     : R.color.latinkeyboard_bar_language_shadow_white);
    572             final float baseline = height * SPACEBAR_LANGUAGE_BASELINE;
    573             final float descent = paint.descent();
    574             paint.setColor(shadowColor);
    575             canvas.drawText(language, width / 2, baseline - descent - 1, paint);
    576             paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text));
    577             canvas.drawText(language, width / 2, baseline - descent, paint);
    578 
    579             // Put arrows that are already layed out on either side of the text
    580             if (mLanguageSwitcher.getLocaleCount() > 1) {
    581                 mButtonArrowLeftIcon.draw(canvas);
    582                 mButtonArrowRightIcon.draw(canvas);
    583             }
    584         }
    585 
    586         // Draw the spacebar icon at the bottom
    587         if (isAutoCompletion) {
    588             final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
    589             final int iconHeight = mSpaceAutoCompletionIndicator.getIntrinsicHeight();
    590             int x = (width - iconWidth) / 2;
    591             int y = height - iconHeight;
    592             mSpaceAutoCompletionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight);
    593             mSpaceAutoCompletionIndicator.draw(canvas);
    594         } else {
    595             final int iconWidth = mSpaceIcon.getIntrinsicWidth();
    596             final int iconHeight = mSpaceIcon.getIntrinsicHeight();
    597             int x = (width - iconWidth) / 2;
    598             int y = height - iconHeight;
    599             mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
    600             mSpaceIcon.draw(canvas);
    601         }
    602         return buffer;
    603     }
    604 
    605     private void updateLocaleDrag(int diff) {
    606         if (mSlidingLocaleIcon == null) {
    607             final int width = Math.max(mSpaceKey.width,
    608                     (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO));
    609             final int height = mSpacePreviewIcon.getIntrinsicHeight();
    610             mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, width, height);
    611             mSlidingLocaleIcon.setBounds(0, 0, width, height);
    612             mSpaceKey.iconPreview = mSlidingLocaleIcon;
    613         }
    614         mSlidingLocaleIcon.setDiff(diff);
    615         if (Math.abs(diff) == Integer.MAX_VALUE) {
    616             mSpaceKey.iconPreview = mSpacePreviewIcon;
    617         } else {
    618             mSpaceKey.iconPreview = mSlidingLocaleIcon;
    619         }
    620         mSpaceKey.iconPreview.invalidateSelf();
    621     }
    622 
    623     public int getLanguageChangeDirection() {
    624         if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2
    625                 || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) {
    626             return 0; // No change
    627         }
    628         return mSpaceDragLastDiff > 0 ? 1 : -1;
    629     }
    630 
    631     public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion,
    632             boolean isBlackSym) {
    633         mLanguageSwitcher = switcher;
    634         Locale locale = mLanguageSwitcher.getLocaleCount() > 0
    635                 ? mLanguageSwitcher.getInputLocale()
    636                 : null;
    637         // If the language count is 1 and is the same as the system language, don't show it.
    638         if (locale != null
    639                 && mLanguageSwitcher.getLocaleCount() == 1
    640                 && mLanguageSwitcher.getSystemLocale().getLanguage()
    641                    .equalsIgnoreCase(locale.getLanguage())) {
    642             locale = null;
    643         }
    644         mLocale = locale;
    645         setColorOfSymbolIcons(isAutoCompletion, isBlackSym);
    646     }
    647 
    648     boolean isCurrentlyInSpace() {
    649         return mCurrentlyInSpace;
    650     }
    651 
    652     void setPreferredLetters(int[] frequencies) {
    653         mPrefLetterFrequencies = frequencies;
    654         mPrefLetter = 0;
    655     }
    656 
    657     void keyReleased() {
    658         mCurrentlyInSpace = false;
    659         mSpaceDragLastDiff = 0;
    660         mPrefLetter = 0;
    661         mPrefLetterX = 0;
    662         mPrefLetterY = 0;
    663         mPrefDistance = Integer.MAX_VALUE;
    664         if (mSpaceKey != null) {
    665             updateLocaleDrag(Integer.MAX_VALUE);
    666         }
    667     }
    668 
    669     /**
    670      * Does the magic of locking the touch gesture into the spacebar when
    671      * switching input languages.
    672      */
    673     boolean isInside(LatinKey key, int x, int y) {
    674         final int code = key.codes[0];
    675         if (code == KEYCODE_SHIFT ||
    676                 code == KEYCODE_DELETE) {
    677             y -= key.height / 10;
    678             if (code == KEYCODE_SHIFT) x += key.width / 6;
    679             if (code == KEYCODE_DELETE) x -= key.width / 6;
    680         } else if (code == LatinIME.KEYCODE_SPACE) {
    681             y += LatinKeyboard.sSpacebarVerticalCorrection;
    682             if (mLanguageSwitcher.getLocaleCount() > 1) {
    683                 if (mCurrentlyInSpace) {
    684                     int diff = x - mSpaceDragStartX;
    685                     if (Math.abs(diff - mSpaceDragLastDiff) > 0) {
    686                         updateLocaleDrag(diff);
    687                     }
    688                     mSpaceDragLastDiff = diff;
    689                     return true;
    690                 } else {
    691                     boolean insideSpace = key.isInsideSuper(x, y);
    692                     if (insideSpace) {
    693                         mCurrentlyInSpace = true;
    694                         mSpaceDragStartX = x;
    695                         updateLocaleDrag(0);
    696                     }
    697                     return insideSpace;
    698                 }
    699             }
    700         } else if (mPrefLetterFrequencies != null) {
    701             // New coordinate? Reset
    702             if (mPrefLetterX != x || mPrefLetterY != y) {
    703                 mPrefLetter = 0;
    704                 mPrefDistance = Integer.MAX_VALUE;
    705             }
    706             // Handle preferred next letter
    707             final int[] pref = mPrefLetterFrequencies;
    708             if (mPrefLetter > 0) {
    709                 if (DEBUG_PREFERRED_LETTER) {
    710                     if (mPrefLetter == code && !key.isInsideSuper(x, y)) {
    711                         Log.d(TAG, "CORRECTED !!!!!!");
    712                     }
    713                 }
    714                 return mPrefLetter == code;
    715             } else {
    716                 final boolean inside = key.isInsideSuper(x, y);
    717                 int[] nearby = getNearestKeys(x, y);
    718                 List<Key> nearbyKeys = getKeys();
    719                 if (inside) {
    720                     // If it's a preferred letter
    721                     if (inPrefList(code, pref)) {
    722                         // Check if its frequency is much lower than a nearby key
    723                         mPrefLetter = code;
    724                         mPrefLetterX = x;
    725                         mPrefLetterY = y;
    726                         for (int i = 0; i < nearby.length; i++) {
    727                             Key k = nearbyKeys.get(nearby[i]);
    728                             if (k != key && inPrefList(k.codes[0], pref)) {
    729                                 final int dist = distanceFrom(k, x, y);
    730                                 if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) &&
    731                                         (pref[k.codes[0]] > pref[mPrefLetter] * 3))  {
    732                                     mPrefLetter = k.codes[0];
    733                                     mPrefDistance = dist;
    734                                     if (DEBUG_PREFERRED_LETTER) {
    735                                         Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!");
    736                                     }
    737                                     break;
    738                                 }
    739                             }
    740                         }
    741 
    742                         return mPrefLetter == code;
    743                     }
    744                 }
    745 
    746                 // Get the surrounding keys and intersect with the preferred list
    747                 // For all in the intersection
    748                 //   if distance from touch point is within a reasonable distance
    749                 //       make this the pref letter
    750                 // If no pref letter
    751                 //   return inside;
    752                 // else return thiskey == prefletter;
    753 
    754                 for (int i = 0; i < nearby.length; i++) {
    755                     Key k = nearbyKeys.get(nearby[i]);
    756                     if (inPrefList(k.codes[0], pref)) {
    757                         final int dist = distanceFrom(k, x, y);
    758                         if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB)
    759                                 && dist < mPrefDistance)  {
    760                             mPrefLetter = k.codes[0];
    761                             mPrefLetterX = x;
    762                             mPrefLetterY = y;
    763                             mPrefDistance = dist;
    764                         }
    765                     }
    766                 }
    767                 // Didn't find any
    768                 if (mPrefLetter == 0) {
    769                     return inside;
    770                 } else {
    771                     return mPrefLetter == code;
    772                 }
    773             }
    774         }
    775 
    776         // Lock into the spacebar
    777         if (mCurrentlyInSpace) return false;
    778 
    779         return key.isInsideSuper(x, y);
    780     }
    781 
    782     private boolean inPrefList(int code, int[] pref) {
    783         if (code < pref.length && code >= 0) return pref[code] > 0;
    784         return false;
    785     }
    786 
    787     private int distanceFrom(Key k, int x, int y) {
    788         if (y > k.y && y < k.y + k.height) {
    789             return Math.abs(k.x + k.width / 2 - x);
    790         } else {
    791             return Integer.MAX_VALUE;
    792         }
    793     }
    794 
    795     @Override
    796     public int[] getNearestKeys(int x, int y) {
    797         if (mCurrentlyInSpace) {
    798             return new int[] { mSpaceKeyIndex };
    799         } else {
    800             // Avoid dead pixels at edges of the keyboard
    801             return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)),
    802                     Math.max(0, Math.min(y, getHeight() - 1)));
    803         }
    804     }
    805 
    806     private int indexOf(int code) {
    807         List<Key> keys = getKeys();
    808         int count = keys.size();
    809         for (int i = 0; i < count; i++) {
    810             if (keys.get(i).codes[0] == code) return i;
    811         }
    812         return -1;
    813     }
    814 
    815     private int getTextSizeFromTheme(int style, int defValue) {
    816         TypedArray array = mContext.getTheme().obtainStyledAttributes(
    817                 style, new int[] { android.R.attr.textSize });
    818         int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
    819         return textSize;
    820     }
    821 
    822     // TODO LatinKey could be static class
    823     class LatinKey extends Keyboard.Key {
    824 
    825         // functional normal state (with properties)
    826         private final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
    827                 android.R.attr.state_single
    828         };
    829 
    830         // functional pressed state (with properties)
    831         private final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
    832                 android.R.attr.state_single,
    833                 android.R.attr.state_pressed
    834         };
    835 
    836         private boolean mShiftLockEnabled;
    837 
    838         public LatinKey(Resources res, Keyboard.Row parent, int x, int y,
    839                 XmlResourceParser parser) {
    840             super(res, parent, x, y, parser);
    841             if (popupCharacters != null && popupCharacters.length() == 0) {
    842                 // If there is a keyboard with no keys specified in popupCharacters
    843                 popupResId = 0;
    844             }
    845         }
    846 
    847         private void enableShiftLock() {
    848             mShiftLockEnabled = true;
    849         }
    850 
    851         // sticky is used for shift key.  If a key is not sticky and is modifier,
    852         // the key will be treated as functional.
    853         private boolean isFunctionalKey() {
    854             return !sticky && modifier;
    855         }
    856 
    857         @Override
    858         public void onReleased(boolean inside) {
    859             if (!mShiftLockEnabled) {
    860                 super.onReleased(inside);
    861             } else {
    862                 pressed = !pressed;
    863             }
    864         }
    865 
    866         /**
    867          * Overriding this method so that we can reduce the target area for certain keys.
    868          */
    869         @Override
    870         public boolean isInside(int x, int y) {
    871             // TODO This should be done by parent.isInside(this, x, y)
    872             // if Key.parent were protected.
    873             boolean result = LatinKeyboard.this.isInside(this, x, y);
    874             return result;
    875         }
    876 
    877         boolean isInsideSuper(int x, int y) {
    878             return super.isInside(x, y);
    879         }
    880 
    881         @Override
    882         public int[] getCurrentDrawableState() {
    883             if (isFunctionalKey()) {
    884                 if (pressed) {
    885                     return KEY_STATE_FUNCTIONAL_PRESSED;
    886                 } else {
    887                     return KEY_STATE_FUNCTIONAL_NORMAL;
    888                 }
    889             }
    890             return super.getCurrentDrawableState();
    891         }
    892 
    893         @Override
    894         public int squaredDistanceFrom(int x, int y) {
    895             // We should count vertical gap between rows to calculate the center of this Key.
    896             final int verticalGap = LatinKeyboard.this.mVerticalGap;
    897             final int xDist = this.x + width / 2 - x;
    898             final int yDist = this.y + (height + verticalGap) / 2 - y;
    899             return xDist * xDist + yDist * yDist;
    900         }
    901     }
    902 
    903     /**
    904      * Animation to be displayed on the spacebar preview popup when switching
    905      * languages by swiping the spacebar. It draws the current, previous and
    906      * next languages and moves them by the delta of touch movement on the spacebar.
    907      */
    908     class SlidingLocaleDrawable extends Drawable {
    909 
    910         private final int mWidth;
    911         private final int mHeight;
    912         private final Drawable mBackground;
    913         private final TextPaint mTextPaint;
    914         private final int mMiddleX;
    915         private final Drawable mLeftDrawable;
    916         private final Drawable mRightDrawable;
    917         private final int mThreshold;
    918         private int mDiff;
    919         private boolean mHitThreshold;
    920         private String mCurrentLanguage;
    921         private String mNextLanguage;
    922         private String mPrevLanguage;
    923 
    924         public SlidingLocaleDrawable(Drawable background, int width, int height) {
    925             mBackground = background;
    926             setDefaultBounds(mBackground);
    927             mWidth = width;
    928             mHeight = height;
    929             mTextPaint = new TextPaint();
    930             mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18));
    931             mTextPaint.setColor(R.color.latinkeyboard_transparent);
    932             mTextPaint.setTextAlign(Align.CENTER);
    933             mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE);
    934             mTextPaint.setAntiAlias(true);
    935             mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2;
    936             mLeftDrawable =
    937                     mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left);
    938             mRightDrawable =
    939                     mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right);
    940             mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop();
    941         }
    942 
    943         private void setDiff(int diff) {
    944             if (diff == Integer.MAX_VALUE) {
    945                 mHitThreshold = false;
    946                 mCurrentLanguage = null;
    947                 return;
    948             }
    949             mDiff = diff;
    950             if (mDiff > mWidth) mDiff = mWidth;
    951             if (mDiff < -mWidth) mDiff = -mWidth;
    952             if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
    953             invalidateSelf();
    954         }
    955 
    956         private String getLanguageName(Locale locale) {
    957             return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale));
    958         }
    959 
    960         @Override
    961         public void draw(Canvas canvas) {
    962             canvas.save();
    963             if (mHitThreshold) {
    964                 Paint paint = mTextPaint;
    965                 final int width = mWidth;
    966                 final int height = mHeight;
    967                 final int diff = mDiff;
    968                 final Drawable lArrow = mLeftDrawable;
    969                 final Drawable rArrow = mRightDrawable;
    970                 canvas.clipRect(0, 0, width, height);
    971                 if (mCurrentLanguage == null) {
    972                     final LanguageSwitcher languageSwitcher = mLanguageSwitcher;
    973                     mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale());
    974                     mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale());
    975                     mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale());
    976                 }
    977                 // Draw language text with shadow
    978                 final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent();
    979                 paint.setColor(mRes.getColor(R.color.latinkeyboard_feedback_language_text));
    980                 canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint);
    981                 canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint);
    982                 canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint);
    983 
    984                 setDefaultBounds(lArrow);
    985                 rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width,
    986                         rArrow.getIntrinsicHeight());
    987                 lArrow.draw(canvas);
    988                 rArrow.draw(canvas);
    989             }
    990             if (mBackground != null) {
    991                 canvas.translate(mMiddleX, 0);
    992                 mBackground.draw(canvas);
    993             }
    994             canvas.restore();
    995         }
    996 
    997         @Override
    998         public int getOpacity() {
    999             return PixelFormat.TRANSLUCENT;
   1000         }
   1001 
   1002         @Override
   1003         public void setAlpha(int alpha) {
   1004             // Ignore
   1005         }
   1006 
   1007         @Override
   1008         public void setColorFilter(ColorFilter cf) {
   1009             // Ignore
   1010         }
   1011 
   1012         @Override
   1013         public int getIntrinsicWidth() {
   1014             return mWidth;
   1015         }
   1016 
   1017         @Override
   1018         public int getIntrinsicHeight() {
   1019             return mHeight;
   1020         }
   1021     }
   1022 }
   1023