LatinKeyboard.java revision 4d3b9d709c36a4c5ea0705ccc3d58c28fbf873d0
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
17package com.android.inputmethod.keyboard;
18
19import com.android.inputmethod.latin.R;
20import com.android.inputmethod.latin.SubtypeSwitcher;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Paint;
29import android.graphics.Paint.Align;
30import android.graphics.PorterDuff;
31import android.graphics.Rect;
32import android.graphics.drawable.BitmapDrawable;
33import android.graphics.drawable.Drawable;
34import android.util.Log;
35
36import java.util.List;
37import java.util.Locale;
38
39// TODO: We should remove this class
40public class LatinKeyboard extends Keyboard {
41
42    private static final boolean DEBUG_PREFERRED_LETTER = false;
43    private static final String TAG = "LatinKeyboard";
44
45    public static final int OPACITY_FULLY_OPAQUE = 255;
46    private static final int SPACE_LED_LENGTH_PERCENT = 80;
47
48    private final Context mContext;
49
50    /* Space key and its icons, drawables and colors. */
51    private final Key mSpaceKey;
52    private final Drawable mSpaceIcon;
53    private final Drawable mSpacePreviewIcon;
54    private final int[] mSpaceKeyIndexArray;
55    private final Drawable mSpaceAutoCorrectionIndicator;
56    private final Drawable mButtonArrowLeftIcon;
57    private final Drawable mButtonArrowRightIcon;
58    private final int mSpacebarTextColor;
59    private final int mSpacebarTextShadowColor;
60    private final int mSpacebarVerticalCorrection;
61    private float mSpacebarTextFadeFactor = 0.0f;
62    private int mSpaceDragStartX;
63    private int mSpaceDragLastDiff;
64    private boolean mCurrentlyInSpace;
65    private SlidingLocaleDrawable mSlidingLocaleIcon;
66
67    /* Shortcut key and its icons if available */
68    private final Key mShortcutKey;
69    private final Drawable mEnabledShortcutIcon;
70    private final Drawable mDisabledShortcutIcon;
71
72    private int[] mPrefLetterFrequencies;
73    private int mPrefLetter;
74    private int mPrefLetterX;
75    private int mPrefLetterY;
76    private int mPrefDistance;
77
78    private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
79    private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
80    private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f;
81    // Minimum width of space key preview (proportional to keyboard width)
82    private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f;
83    // Height in space key the language name will be drawn. (proportional to space key height)
84    public static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
85    // If the full language name needs to be smaller than this value to be drawn on space key,
86    // its short language name will be used instead.
87    private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
88
89    private static final String SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "small";
90    private static final String MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "medium";
91
92    public LatinKeyboard(Context context, KeyboardId id) {
93        super(context, id.getXmlId(), id);
94        final Resources res = context.getResources();
95        mContext = context;
96
97        final List<Key> keys = getKeys();
98        int spaceKeyIndex = -1;
99        int shortcutKeyIndex = -1;
100        final int keyCount = keys.size();
101        for (int index = 0; index < keyCount; index++) {
102            // For now, assuming there are up to one space key and one shortcut key respectively.
103            switch (keys.get(index).mCode) {
104            case CODE_SPACE:
105                spaceKeyIndex = index;
106                break;
107            case CODE_VOICE:
108                shortcutKeyIndex = index;
109                break;
110            }
111        }
112
113        // The index of space key is available only after Keyboard constructor has finished.
114        mSpaceKey = (spaceKeyIndex >= 0) ? keys.get(spaceKeyIndex) : null;
115        mSpaceIcon = (mSpaceKey != null) ? mSpaceKey.getIcon() : null;
116        mSpacePreviewIcon = (mSpaceKey != null) ? mSpaceKey.getPreviewIcon() : null;
117        mSpaceKeyIndexArray = new int[] { spaceKeyIndex };
118
119        mShortcutKey = (shortcutKeyIndex >= 0) ? keys.get(shortcutKeyIndex) : null;
120        mEnabledShortcutIcon = (mShortcutKey != null) ? mShortcutKey.getIcon() : null;
121
122        mSpacebarTextColor = res.getColor(R.color.latinkeyboard_bar_language_text);
123        if (id.mColorScheme == KeyboardView.COLOR_SCHEME_BLACK) {
124            mSpacebarTextShadowColor = res.getColor(
125                    R.color.latinkeyboard_bar_language_shadow_black);
126            mDisabledShortcutIcon = res.getDrawable(R.drawable.sym_bkeyboard_voice_off);
127        } else { // default color scheme is KeyboardView.COLOR_SCHEME_WHITE
128            mSpacebarTextShadowColor = res.getColor(
129                    R.color.latinkeyboard_bar_language_shadow_white);
130            mDisabledShortcutIcon = res.getDrawable(R.drawable.sym_keyboard_voice_off_holo);
131        }
132        mSpaceAutoCorrectionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led);
133        mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left);
134        mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right);
135        mSpacebarVerticalCorrection = res.getDimensionPixelOffset(
136                R.dimen.spacebar_vertical_correction);
137    }
138
139    public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboardView view) {
140        mSpacebarTextFadeFactor = fadeFactor;
141        updateSpacebarForLocale(false);
142        if (view != null)
143            view.invalidateKey(mSpaceKey);
144    }
145
146    private static int getSpacebarTextColor(int color, float fadeFactor) {
147        final int newColor = Color.argb((int)(Color.alpha(color) * fadeFactor),
148                Color.red(color), Color.green(color), Color.blue(color));
149        return newColor;
150    }
151
152    public void updateShortcutKey(boolean available, LatinKeyboardView view) {
153        if (mShortcutKey == null)
154            return;
155        mShortcutKey.mEnabled = available;
156        mShortcutKey.setIcon(available ? mEnabledShortcutIcon : mDisabledShortcutIcon);
157        if (view != null)
158            view.invalidateKey(mShortcutKey);
159    }
160
161    /**
162     * @return a key which should be invalidated.
163     */
164    public Key onAutoCorrectionStateChanged(boolean isAutoCorrection) {
165        updateSpacebarForLocale(isAutoCorrection);
166        return mSpaceKey;
167    }
168
169    private void updateSpacebarForLocale(boolean isAutoCorrection) {
170        final Resources res = mContext.getResources();
171        // If application locales are explicitly selected.
172        if (SubtypeSwitcher.getInstance().needsToDisplayLanguage()) {
173            mSpaceKey.setIcon(new BitmapDrawable(res,
174                    drawSpacebar(OPACITY_FULLY_OPAQUE, isAutoCorrection)));
175        } else {
176            // sym_keyboard_space_led can be shared with Black and White symbol themes.
177            if (isAutoCorrection) {
178                mSpaceKey.setIcon(new BitmapDrawable(res,
179                        drawSpacebar(OPACITY_FULLY_OPAQUE, isAutoCorrection)));
180            } else {
181                mSpaceKey.setIcon(mSpaceIcon);
182            }
183        }
184    }
185
186    // Compute width of text with specified text size using paint.
187    private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
188        paint.setTextSize(textSize);
189        paint.getTextBounds(text, 0, text.length(), bounds);
190        return bounds.width();
191    }
192
193    // Layout local language name and left and right arrow on spacebar.
194    private static String layoutSpacebar(Paint paint, Locale locale, Drawable lArrow,
195            Drawable rArrow, int width, int height, float origTextSize,
196            boolean allowVariableTextSize) {
197        final float arrowWidth = lArrow.getIntrinsicWidth();
198        final float arrowHeight = lArrow.getIntrinsicHeight();
199        final float maxTextWidth = width - (arrowWidth + arrowWidth);
200        final Rect bounds = new Rect();
201
202        // Estimate appropriate language name text size to fit in maxTextWidth.
203        String language = SubtypeSwitcher.getFullDisplayName(locale, true);
204        int textWidth = getTextWidth(paint, language, origTextSize, bounds);
205        // Assuming text width and text size are proportional to each other.
206        float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
207
208        final boolean useShortName;
209        if (allowVariableTextSize) {
210            textWidth = getTextWidth(paint, language, textSize, bounds);
211            // If text size goes too small or text does not fit, use short name
212            useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME
213                    || textWidth > maxTextWidth;
214        } else {
215            useShortName = textWidth > maxTextWidth;
216            textSize = origTextSize;
217        }
218        if (useShortName) {
219            language = SubtypeSwitcher.getShortDisplayLanguage(locale);
220            textWidth = getTextWidth(paint, language, origTextSize, bounds);
221            textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
222        }
223        paint.setTextSize(textSize);
224
225        // Place left and right arrow just before and after language text.
226        final float baseline = height * SPACEBAR_LANGUAGE_BASELINE;
227        final int top = (int)(baseline - arrowHeight);
228        final float remains = (width - textWidth) / 2;
229        lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline);
230        rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth),
231                (int)baseline);
232
233        return language;
234    }
235
236    private Bitmap drawSpacebar(int opacity, boolean isAutoCorrection) {
237        final int width = mSpaceKey.mWidth;
238        final int height = mSpaceIcon != null ? mSpaceIcon.getIntrinsicHeight() : mSpaceKey.mHeight;
239        final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
240        final Canvas canvas = new Canvas(buffer);
241        final Resources res = mContext.getResources();
242        canvas.drawColor(res.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR);
243
244        SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance();
245        // If application locales are explicitly selected.
246        if (subtypeSwitcher.needsToDisplayLanguage()) {
247            final Paint paint = new Paint();
248            paint.setAlpha(opacity);
249            paint.setAntiAlias(true);
250            paint.setTextAlign(Align.CENTER);
251
252            final String textSizeOfLanguageOnSpacebar = res.getString(
253                    R.string.config_text_size_of_language_on_spacebar,
254                    SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR);
255            final int textStyle;
256            final int defaultTextSize;
257            if (MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR.equals(textSizeOfLanguageOnSpacebar)) {
258                textStyle = android.R.style.TextAppearance_Medium;
259                defaultTextSize = 18;
260            } else {
261                textStyle = android.R.style.TextAppearance_Small;
262                defaultTextSize = 14;
263            }
264
265            final boolean allowVariableTextSize = true;
266            final String language = layoutSpacebar(paint, subtypeSwitcher.getInputLocale(),
267                    mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height,
268                    getTextSizeFromTheme(textStyle, defaultTextSize),
269                    allowVariableTextSize);
270
271            // Draw language text with shadow
272            // In case there is no space icon, we will place the language text at the center of
273            // spacebar.
274            final float descent = paint.descent();
275            final float textHeight = -paint.ascent() + descent;
276            final float baseline = (mSpaceIcon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
277                    : height / 2 + textHeight / 2;
278            paint.setColor(getSpacebarTextColor(mSpacebarTextShadowColor, mSpacebarTextFadeFactor));
279            canvas.drawText(language, width / 2, baseline - descent - 1, paint);
280            paint.setColor(getSpacebarTextColor(mSpacebarTextColor, mSpacebarTextFadeFactor));
281            canvas.drawText(language, width / 2, baseline - descent, paint);
282
283            // Put arrows that are already layed out on either side of the text
284            if (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher()
285                    && subtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) {
286                mButtonArrowLeftIcon.draw(canvas);
287                mButtonArrowRightIcon.draw(canvas);
288            }
289        }
290
291        // Draw the spacebar icon at the bottom
292        if (isAutoCorrection) {
293            final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
294            final int iconHeight = mSpaceAutoCorrectionIndicator.getIntrinsicHeight();
295            int x = (width - iconWidth) / 2;
296            int y = height - iconHeight;
297            mSpaceAutoCorrectionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight);
298            mSpaceAutoCorrectionIndicator.draw(canvas);
299        } else if (mSpaceIcon != null) {
300            final int iconWidth = mSpaceIcon.getIntrinsicWidth();
301            final int iconHeight = mSpaceIcon.getIntrinsicHeight();
302            int x = (width - iconWidth) / 2;
303            int y = height - iconHeight;
304            mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
305            mSpaceIcon.draw(canvas);
306        }
307        return buffer;
308    }
309
310    private void updateLocaleDrag(int diff) {
311        if (mSlidingLocaleIcon == null) {
312            final int width = Math.max(mSpaceKey.mWidth,
313                    (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO));
314            final int height = mSpacePreviewIcon.getIntrinsicHeight();
315            mSlidingLocaleIcon =
316                    new SlidingLocaleDrawable(mContext, mSpacePreviewIcon, width, height);
317            mSlidingLocaleIcon.setBounds(0, 0, width, height);
318            mSpaceKey.setPreviewIcon(mSlidingLocaleIcon);
319        }
320        mSlidingLocaleIcon.setDiff(diff);
321        if (Math.abs(diff) == Integer.MAX_VALUE) {
322            mSpaceKey.setPreviewIcon(mSpacePreviewIcon);
323        } else {
324            mSpaceKey.setPreviewIcon(mSlidingLocaleIcon);
325        }
326        mSpaceKey.getPreviewIcon().invalidateSelf();
327    }
328
329    public int getLanguageChangeDirection() {
330        if (mSpaceKey == null || SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() <= 1
331                || Math.abs(mSpaceDragLastDiff) < mSpaceKey.mWidth * SPACEBAR_DRAG_THRESHOLD) {
332            return 0; // No change
333        }
334        return mSpaceDragLastDiff > 0 ? 1 : -1;
335    }
336
337    public void setPreferredLetters(int[] frequencies) {
338        mPrefLetterFrequencies = frequencies;
339        mPrefLetter = 0;
340    }
341
342    public void keyReleased() {
343        mCurrentlyInSpace = false;
344        mSpaceDragLastDiff = 0;
345        mPrefLetter = 0;
346        mPrefLetterX = 0;
347        mPrefLetterY = 0;
348        mPrefDistance = Integer.MAX_VALUE;
349        if (mSpaceKey != null) {
350            updateLocaleDrag(Integer.MAX_VALUE);
351        }
352    }
353
354    /**
355     * Does the magic of locking the touch gesture into the spacebar when
356     * switching input languages.
357     */
358    @Override
359    public boolean isInside(Key key, int pointX, int pointY) {
360        int x = pointX;
361        int y = pointY;
362        final int code = key.mCode;
363        if (code == CODE_SPACE) {
364            y += mSpacebarVerticalCorrection;
365            if (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher()
366                    && SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() > 1) {
367                if (mCurrentlyInSpace) {
368                    int diff = x - mSpaceDragStartX;
369                    if (Math.abs(diff - mSpaceDragLastDiff) > 0) {
370                        updateLocaleDrag(diff);
371                    }
372                    mSpaceDragLastDiff = diff;
373                    return true;
374                } else {
375                    boolean isOnSpace = key.isOnKey(x, y);
376                    if (isOnSpace) {
377                        mCurrentlyInSpace = true;
378                        mSpaceDragStartX = x;
379                        updateLocaleDrag(0);
380                    }
381                    return isOnSpace;
382                }
383            }
384        } else if (mPrefLetterFrequencies != null) {
385            // New coordinate? Reset
386            if (mPrefLetterX != x || mPrefLetterY != y) {
387                mPrefLetter = 0;
388                mPrefDistance = Integer.MAX_VALUE;
389            }
390            // Handle preferred next letter
391            final int[] pref = mPrefLetterFrequencies;
392            if (mPrefLetter > 0) {
393                if (DEBUG_PREFERRED_LETTER) {
394                    if (mPrefLetter == code && !key.isOnKey(x, y)) {
395                        Log.d(TAG, "CORRECTED !!!!!!");
396                    }
397                }
398                return mPrefLetter == code;
399            } else {
400                final boolean isOnKey = key.isOnKey(x, y);
401                int[] nearby = getNearestKeys(x, y);
402                List<Key> nearbyKeys = getKeys();
403                if (isOnKey) {
404                    // If it's a preferred letter
405                    if (inPrefList(code, pref)) {
406                        // Check if its frequency is much lower than a nearby key
407                        mPrefLetter = code;
408                        mPrefLetterX = x;
409                        mPrefLetterY = y;
410                        for (int i = 0; i < nearby.length; i++) {
411                            Key k = nearbyKeys.get(nearby[i]);
412                            if (k != key && inPrefList(k.mCode, pref)) {
413                                final int dist = distanceFrom(k, x, y);
414                                if (dist < (int) (k.mWidth * OVERLAP_PERCENTAGE_LOW_PROB) &&
415                                        (pref[k.mCode] > pref[mPrefLetter] * 3))  {
416                                    mPrefLetter = k.mCode;
417                                    mPrefDistance = dist;
418                                    if (DEBUG_PREFERRED_LETTER) {
419                                        Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!");
420                                    }
421                                    break;
422                                }
423                            }
424                        }
425
426                        return mPrefLetter == code;
427                    }
428                }
429
430                // Get the surrounding keys and intersect with the preferred list
431                // For all in the intersection
432                //   if distance from touch point is within a reasonable distance
433                //       make this the pref letter
434                // If no pref letter
435                //   return inside;
436                // else return thiskey == prefletter;
437
438                for (int i = 0; i < nearby.length; i++) {
439                    Key k = nearbyKeys.get(nearby[i]);
440                    if (inPrefList(k.mCode, pref)) {
441                        final int dist = distanceFrom(k, x, y);
442                        if (dist < (int) (k.mWidth * OVERLAP_PERCENTAGE_HIGH_PROB)
443                                && dist < mPrefDistance)  {
444                            mPrefLetter = k.mCode;
445                            mPrefLetterX = x;
446                            mPrefLetterY = y;
447                            mPrefDistance = dist;
448                        }
449                    }
450                }
451                // Didn't find any
452                if (mPrefLetter == 0) {
453                    return isOnKey;
454                } else {
455                    return mPrefLetter == code;
456                }
457            }
458        }
459
460        // Lock into the spacebar
461        if (mCurrentlyInSpace) return false;
462
463        return key.isOnKey(x, y);
464    }
465
466    private boolean inPrefList(int code, int[] pref) {
467        if (code < pref.length && code >= 0) return pref[code] > 0;
468        return false;
469    }
470
471    private int distanceFrom(Key k, int x, int y) {
472        if (y > k.mY && y < k.mY + k.mHeight) {
473            return Math.abs(k.mX + k.mWidth / 2 - x);
474        } else {
475            return Integer.MAX_VALUE;
476        }
477    }
478
479    @Override
480    public int[] getNearestKeys(int x, int y) {
481        if (mCurrentlyInSpace) {
482            return mSpaceKeyIndexArray;
483        } else {
484            // Avoid dead pixels at edges of the keyboard
485            return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)),
486                    Math.max(0, Math.min(y, getHeight() - 1)));
487        }
488    }
489
490    private int getTextSizeFromTheme(int style, int defValue) {
491        TypedArray array = mContext.getTheme().obtainStyledAttributes(
492                style, new int[] { android.R.attr.textSize });
493        int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
494        return textSize;
495    }
496}
497