• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
LatinKeyboard(Context context, int xmlLayoutResId)118     public LatinKeyboard(Context context, int xmlLayoutResId) {
119         this(context, xmlLayoutResId, 0);
120     }
121 
LatinKeyboard(Context context, int xmlLayoutResId, int mode)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 
initializeNumberHintResources(Context context)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
createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser)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 
setImeOptions(Resources res, int mode, int options)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 
enableShiftLock()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 
setShiftLocked(boolean shiftLocked)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 
isShiftLocked()291     boolean isShiftLocked() {
292         return mShiftState == SHIFT_LOCKED;
293     }
294 
295     @Override
setShifted(boolean shiftState)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
isShifted()318     public boolean isShifted() {
319         if (mShiftKey != null) {
320             return mShiftState != SHIFT_OFF;
321         } else {
322             return super.isShifted();
323         }
324     }
325 
isAlphaKeyboard()326     /* package */ boolean isAlphaKeyboard() {
327         return mIsAlphaKeyboard;
328     }
329 
setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack)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 
setDefaultBounds(Drawable drawable)350     private void setDefaultBounds(Drawable drawable) {
351         drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
352     }
353 
setVoiceMode(boolean hasVoiceButton, boolean hasVoice)354     public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) {
355         mHasVoiceButton = hasVoiceButton;
356         mVoiceEnabled = hasVoice;
357         updateDynamicKeys();
358     }
359 
updateDynamicKeys()360     private void updateDynamicKeys() {
361         update123Key();
362         updateF1Key();
363     }
364 
update123Key()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 
updateF1Key()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 
setMicF1Key(Key key)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 
setNonMicF1Key(Key key, String label, int popupResId)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 
isF1Key(Key key)426     public boolean isF1Key(Key key) {
427         return key == mF1Key;
428     }
429 
hasPuncOrSmileysPopup(Key key)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      */
onAutoCompletionStateChanged(boolean isAutoCompletion)437     public Key onAutoCompletionStateChanged(boolean isAutoCompletion) {
438         updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym);
439         return mSpaceKey;
440     }
441 
updateNumberHintKeys()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 
isLanguageSwitchEnabled()450     public boolean isLanguageSwitchEnabled() {
451         return mLocale != null;
452     }
453 
updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack)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.
getTextWidth(Paint paint, String text, float textSize, Rect bounds)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.
drawSynthesizedSettingsHintImage( int width, int height, Drawable mainIcon, Drawable 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.
layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow, Drawable rArrow, int width, int height, float origTextSize, boolean allowVariableTextSize)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 
drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack)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 
updateLocaleDrag(int diff)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 
getLanguageChangeDirection()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 
setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion, boolean isBlackSym)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 
getInputLocale()649     public Locale getInputLocale() {
650         return (mLocale != null) ? mLocale : mLanguageSwitcher.getSystemLocale();
651     }
652 
isCurrentlyInSpace()653     boolean isCurrentlyInSpace() {
654         return mCurrentlyInSpace;
655     }
656 
setPreferredLetters(int[] frequencies)657     void setPreferredLetters(int[] frequencies) {
658         mPrefLetterFrequencies = frequencies;
659         mPrefLetter = 0;
660     }
661 
keyReleased()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      */
isInside(LatinKey key, int x, int y)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 
inPrefList(int code, int[] pref)787     private boolean inPrefList(int code, int[] pref) {
788         if (code < pref.length && code >= 0) return pref[code] > 0;
789         return false;
790     }
791 
distanceFrom(Key k, int x, int y)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
getNearestKeys(int x, int y)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 
indexOf(int code)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 
getTextSizeFromTheme(int style, int defValue)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 
LatinKey(Resources res, Keyboard.Row parent, int x, int y, XmlResourceParser parser)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 
enableShiftLock()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.
isFunctionalKey()858         private boolean isFunctionalKey() {
859             return !sticky && modifier;
860         }
861 
862         @Override
onReleased(boolean inside)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
isInside(int x, int y)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 
isInsideSuper(int x, int y)882         boolean isInsideSuper(int x, int y) {
883             return super.isInside(x, y);
884         }
885 
886         @Override
getCurrentDrawableState()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
squaredDistanceFrom(int x, int y)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 
SlidingLocaleDrawable(Drawable background, int width, int height)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 
setDiff(int diff)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 
getLanguageName(Locale locale)961         private String getLanguageName(Locale locale) {
962             return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale), locale);
963         }
964 
965         @Override
draw(Canvas canvas)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
getOpacity()1003         public int getOpacity() {
1004             return PixelFormat.TRANSLUCENT;
1005         }
1006 
1007         @Override
setAlpha(int alpha)1008         public void setAlpha(int alpha) {
1009             // Ignore
1010         }
1011 
1012         @Override
setColorFilter(ColorFilter cf)1013         public void setColorFilter(ColorFilter cf) {
1014             // Ignore
1015         }
1016 
1017         @Override
getIntrinsicWidth()1018         public int getIntrinsicWidth() {
1019             return mWidth;
1020         }
1021 
1022         @Override
getIntrinsicHeight()1023         public int getIntrinsicHeight() {
1024             return mHeight;
1025         }
1026     }
1027 }
1028