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