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