• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.latin.car;
18 
19 import android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.annotation.SuppressLint;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.Paint.Align;
29 import android.graphics.PorterDuff;
30 import android.graphics.Rect;
31 import android.graphics.Region.Op;
32 import android.graphics.Typeface;
33 import android.graphics.drawable.Drawable;
34 import android.inputmethodservice.Keyboard;
35 import android.inputmethodservice.Keyboard.Key;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.util.AttributeSet;
39 import android.util.Log;
40 import android.util.TypedValue;
41 import android.view.GestureDetector;
42 import android.view.Gravity;
43 import android.view.LayoutInflater;
44 import android.view.MotionEvent;
45 import android.view.View;
46 import android.view.ViewConfiguration;
47 import android.view.ViewGroup.LayoutParams;
48 import android.view.accessibility.AccessibilityManager;
49 import android.view.animation.AccelerateDecelerateInterpolator;
50 import android.view.animation.LinearInterpolator;
51 import android.widget.PopupWindow;
52 import android.widget.TextView;
53 
54 import com.android.inputmethod.latin.R;
55 
56 import java.lang.ref.WeakReference;
57 import java.util.Arrays;
58 import java.util.HashMap;
59 import java.util.List;
60 import java.util.Locale;
61 import java.util.Map;
62 import java.util.regex.Pattern;
63 
64 /**
65  * A view that renders a virtual {@link android.inputmethodservice.Keyboard}. It handles rendering of keys and
66  * detecting key presses and touch movements.
67  *
68  * @attr ref android.R.styleable#KeyboardView_keyBackground
69  * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
70  * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
71  * @attr ref android.R.styleable#KeyboardView_labelTextSize
72  * @attr ref android.R.styleable#KeyboardView_keyTextSize
73  * @attr ref android.R.styleable#KeyboardView_keyTextColor
74  * @attr ref android.R.styleable#KeyboardView_verticalCorrection
75  * @attr ref android.R.styleable#KeyboardView_popupLayout
76  */
77 public class KeyboardView extends View implements View.OnClickListener {
78 
79     /**
80      * Listener for virtual keyboard events.
81      */
82     public interface OnKeyboardActionListener {
83 
84         /**
85          * Called when the user presses a key. This is sent before the {@link #onKey} is called.
86          * For keys that repeat, this is only called once.
87          * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
88          * key, the value will be zero.
89          */
onPress(int primaryCode)90         void onPress(int primaryCode);
91 
92         /**
93          * Called when the user releases a key. This is sent after the {@link #onKey} is called.
94          * For keys that repeat, this is only called once.
95          * @param primaryCode the code of the key that was released
96          */
onRelease(int primaryCode)97         void onRelease(int primaryCode);
98 
99         /**
100          * Send a key press to the listener.
101          * @param primaryCode this is the key that was pressed
102          * @param keyCodes the codes for all the possible alternative keys
103          * with the primary code being the first. If the primary key code is
104          * a single character such as an alphabet or number or symbol, the alternatives
105          * will include other characters that may be on the same key or adjacent keys.
106          * These codes are useful to correct for accidental presses of a key adjacent to
107          * the intended key.
108          */
onKey(int primaryCode, int[] keyCodes)109         void onKey(int primaryCode, int[] keyCodes);
110 
111         /**
112          * Sends a sequence of characters to the listener.
113          * @param text the sequence of characters to be displayed.
114          */
onText(CharSequence text)115         void onText(CharSequence text);
116 
117         /**
118          * Called when the user quickly moves the finger from right to left.
119          */
swipeLeft()120         void swipeLeft();
121 
122         /**
123          * Called when the user quickly moves the finger from left to right.
124          */
swipeRight()125         void swipeRight();
126 
127         /**
128          * Called when the user quickly moves the finger from up to down.
129          */
swipeDown()130         void swipeDown();
131 
132         /**
133          * Called when the user quickly moves the finger from down to up.
134          */
swipeUp()135         void swipeUp();
136 
137         /**
138          * Called when we want to stop keyboard input.
139          */
stopInput()140         void stopInput();
141     }
142 
143     private static final boolean DEBUG = false;
144     private static final int NOT_A_KEY = -1;
145     private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
146     private static final int SCRIM_ALPHA = 242;  // 95% opacity.
147     private static final int MAX_ALPHA = 255;
148     private static final float MIN_ANIMATION_VALUE = 0.0f;
149     private static final float MAX_ANIMATION_VALUE = 1.0f;
150     private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("_|-|,|\\.");
151     //private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
152     private final Context mContext;
153 
154     private Keyboard mKeyboard;
155     private int mCurrentKeyIndex = NOT_A_KEY;
156     private int mLabelTextSize;
157     private int mKeyTextSize;
158     private int mKeyPunctuationSize;
159     private int mKeyTextColorPrimary;
160     private int mKeyTextColorSecondary;
161     private String mFontFamily;
162     private int mTextStyle = Typeface.BOLD;
163 
164     private TextView mPreviewText;
165     private final PopupWindow mPreviewPopup;
166     private int mPreviewTextSizeLarge;
167     private int mPreviewOffset;
168     private int mPreviewHeight;
169     // Working variable
170     private final int[] mCoordinates = new int[2];
171 
172     private KeyboardView mPopupKeyboardView;
173     private boolean mPopupKeyboardOnScreen;
174     private View mPopupParent;
175     private int mMiniKeyboardOffsetX;
176     private int mMiniKeyboardOffsetY;
177     private final Map<Key,View> mMiniKeyboardCache;
178     private Key[] mKeys;
179 
180     /** Listener for {@link OnKeyboardActionListener}. */
181     private OnKeyboardActionListener mKeyboardActionListener;
182 
183     private static final int MSG_SHOW_PREVIEW = 1;
184     private static final int MSG_REMOVE_PREVIEW = 2;
185     private static final int MSG_REPEAT = 3;
186     private static final int MSG_LONGPRESS = 4;
187 
188     private static final int DELAY_BEFORE_PREVIEW = 0;
189     private static final int DELAY_AFTER_PREVIEW = 70;
190     private static final int DEBOUNCE_TIME = 70;
191 
192     private static final int ANIMATE_IN_OUT_DURATION_MS = 300;
193     private static final int ANIMATE_SCRIM_DURATION_MS = 150;
194     private static final int ANIMATE_SCRIM_DELAY_MS = 225;
195 
196     private int mVerticalCorrection;
197     private int mProximityThreshold;
198 
199     private final boolean mPreviewCentered = false;
200     private boolean mShowPreview = true;
201     private final boolean mShowTouchPoints = true;
202     private int mPopupPreviewX;
203     private int mPopupPreviewY;
204     private int mPopupScrimColor;
205     private int mBackgroundColor;
206 
207     private int mLastX;
208     private int mLastY;
209     private int mStartX;
210     private int mStartY;
211 
212     private boolean mProximityCorrectOn;
213 
214     private final Paint mPaint;
215     private final Rect mPadding;
216 
217     private long mDownTime;
218     private long mLastMoveTime;
219     private int mLastKey;
220     private int mLastCodeX;
221     private int mLastCodeY;
222     private int mCurrentKey = NOT_A_KEY;
223     private int mDownKey = NOT_A_KEY;
224     private long mLastKeyTime;
225     private long mCurrentKeyTime;
226     private final int[] mKeyIndices = new int[12];
227     private GestureDetector mGestureDetector;
228     private int mRepeatKeyIndex = NOT_A_KEY;
229     private boolean mAbortKey;
230     private Key mInvalidatedKey;
231     private final Rect mClipRegion = new Rect(0, 0, 0, 0);
232     private boolean mPossiblePoly;
233     private final SwipeTracker mSwipeTracker = new SwipeTracker();
234     private final int mSwipeThreshold;
235     private final boolean mDisambiguateSwipe;
236 
237     // Variables for dealing with multiple pointers
238     private int mOldPointerCount = 1;
239     private float mOldPointerX;
240     private float mOldPointerY;
241 
242     private Drawable mKeyBackground;
243 
244     private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
245     private static final int REPEAT_START_DELAY = 400;
246     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
247 
248     private static int MAX_NEARBY_KEYS = 12;
249     private final int[] mDistances = new int[MAX_NEARBY_KEYS];
250 
251     // For multi-tap
252     private int mLastSentIndex;
253     private int mTapCount;
254     private long mLastTapTime;
255     private boolean mInMultiTap;
256     private static final int MULTITAP_INTERVAL = 800; // milliseconds
257     private final StringBuilder mPreviewLabel = new StringBuilder(1);
258 
259     /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
260     private boolean mDrawPending;
261     /** The dirty region in the keyboard bitmap */
262     private final Rect mDirtyRect = new Rect();
263     /** The keyboard bitmap for faster updates */
264     private Bitmap mBuffer;
265     /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
266     private boolean mKeyboardChanged;
267     /** The canvas for the above mutable keyboard bitmap */
268     private Canvas mCanvas;
269     /** The accessibility manager for accessibility support */
270     private AccessibilityManager mAccessibilityManager;
271 
272     private boolean mUseSecondaryColor = true;
273     private Locale mLocale;
274 
275     Handler mHandler = new KeyboardHander(this);
276 
277     private float mAnimateInValue;
278     private ValueAnimator mAnimateInAnimator;
279     private float mAnimateOutValue;
280     private ValueAnimator mAnimateOutAnimator;
281     private int mScrimAlpha;
282     private ValueAnimator mScrimAlphaAnimator;
283     private int mAnimateCenter;
284 
KeyboardView(Context context, AttributeSet attrs)285     public KeyboardView(Context context, AttributeSet attrs) {
286         this(context, attrs, 0);
287     }
288 
KeyboardView(Context context, AttributeSet attrs, int defStyle)289     public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
290         super(context, attrs, defStyle);
291         mContext = context;
292 
293         TypedArray a =
294                 context.obtainStyledAttributes(
295                         attrs, R.styleable.KeyboardView, defStyle, 0);
296 
297         LayoutInflater inflate =
298                 (LayoutInflater) context
299                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
300 
301         int previewLayout = 0;
302 
303         int n = a.getIndexCount();
304 
305         for (int i = 0; i < n; i++) {
306             int attr = a.getIndex(i);
307 
308             switch (attr) {
309                 case R.styleable.KeyboardView_keyBackground:
310                     mKeyBackground = a.getDrawable(attr);
311                     break;
312 /*                case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
313                     mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
314                     break;
315                 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
316                     previewLayout = a.getResourceId(attr, 0);
317                     break;
318                 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
319                     mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
320                     break;
321                 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
322                     mPreviewHeight = a.getDimensionPixelSize(attr, 80);
323                     break; */
324                 case R.styleable.KeyboardView_keyTextSize:
325                     mKeyTextSize = a.getDimensionPixelSize(attr, 18);
326                     break;
327                 case R.styleable.KeyboardView_keyTextColorPrimary:
328                     mKeyTextColorPrimary = a.getColor(attr, 0xFF000000);
329                     break;
330                 case R.styleable.KeyboardView_keyTextColorSecondary:
331                     mKeyTextColorSecondary = a.getColor(attr, 0x99000000);
332                     break;
333                 case R.styleable.KeyboardView_labelTextSize:
334                     mLabelTextSize = a.getDimensionPixelSize(attr, 14);
335                     break;
336                 case R.styleable.KeyboardView_fontFamily:
337                     mFontFamily = a.getString(attr);
338                     break;
339                 case R.styleable.KeyboardView_textStyle:
340                     mTextStyle = a.getInt(attr, 0);
341                     break;
342             }
343         }
344 
345         a.recycle();
346         a = mContext.obtainStyledAttributes(R.styleable.KeyboardView);
347         a.recycle();
348 
349         mPreviewPopup = new PopupWindow(context);
350         if (previewLayout != 0) {
351             mPreviewText = (TextView) inflate.inflate(previewLayout, null);
352             mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
353             mPreviewPopup.setContentView(mPreviewText);
354             mPreviewPopup.setBackgroundDrawable(null);
355         } else {
356             mShowPreview = false;
357         }
358 
359         mPreviewPopup.setTouchable(false);
360 
361         mPopupParent = this;
362         //mPredicting = true;
363 
364         mPaint = new Paint();
365         mPaint.setAntiAlias(true);
366         mPaint.setTextSize(mKeyTextSize);
367         mPaint.setTextAlign(Align.CENTER);
368         mPaint.setAlpha(MAX_ALPHA);
369 
370         if (mFontFamily != null) {
371             Typeface typeface = Typeface.create(mFontFamily, mTextStyle);
372             mPaint.setTypeface(typeface);
373         }
374         else {
375             mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, mTextStyle));
376         }
377 
378         mPadding = new Rect(0, 0, 0, 0);
379         mMiniKeyboardCache = new HashMap<Key,View>();
380         mKeyBackground.getPadding(mPadding);
381 
382         mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
383         mDisambiguateSwipe = true;
384 
385         int color = getResources().getColor(R.color.ime_background_letters);
386         mPopupScrimColor = Color.argb(
387                 SCRIM_ALPHA, Color.red(color), Color.green(color), Color.blue(color));
388         mBackgroundColor = Color.TRANSPARENT;
389 
390         mKeyPunctuationSize =
391                 (int) getResources().getDimension(R.dimen.keyboard_key_punctuation_height);
392 
393         resetMultiTap();
394         initGestureDetector();
395 
396         mAnimateInValue = MAX_ANIMATION_VALUE;
397         mAnimateInAnimator = ValueAnimator.ofFloat(MIN_ANIMATION_VALUE, MAX_ANIMATION_VALUE);
398         AccelerateDecelerateInterpolator accInterpolator = new AccelerateDecelerateInterpolator();
399         mAnimateInAnimator.setInterpolator(accInterpolator);
400         mAnimateInAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
401             @Override
402             public void onAnimationUpdate(ValueAnimator valueAnimator) {
403                 mAnimateInValue = (float) valueAnimator.getAnimatedValue();
404                 invalidateAllKeys();
405             }
406         });
407         mAnimateInAnimator.setDuration(ANIMATE_IN_OUT_DURATION_MS);
408 
409         mScrimAlpha = 0;
410         mScrimAlphaAnimator = ValueAnimator.ofInt(0, SCRIM_ALPHA);
411         LinearInterpolator linearInterpolator = new LinearInterpolator();
412         mScrimAlphaAnimator.setInterpolator(linearInterpolator);
413         mScrimAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
414             @Override
415             public void onAnimationUpdate(ValueAnimator valueAnimator) {
416                 mScrimAlpha = (int) valueAnimator.getAnimatedValue();
417                 invalidateAllKeys();
418             }
419         });
420         mScrimAlphaAnimator.setDuration(ANIMATE_SCRIM_DURATION_MS);
421 
422         mAnimateOutValue = MIN_ANIMATION_VALUE;
423         mAnimateOutAnimator = ValueAnimator.ofFloat(MIN_ANIMATION_VALUE, MAX_ANIMATION_VALUE);
424         mAnimateOutAnimator.setInterpolator(accInterpolator);
425         mAnimateOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
426             @Override
427             public void onAnimationUpdate(ValueAnimator valueAnimator) {
428                 mAnimateOutValue = (float) valueAnimator.getAnimatedValue();
429                 invalidateAllKeys();
430             }
431         });
432         mAnimateOutAnimator.setDuration(ANIMATE_IN_OUT_DURATION_MS);
433         mAnimateOutAnimator.addListener(new Animator.AnimatorListener() {
434             @Override
435             public void onAnimationEnd(Animator animation) {
436                 setVisibility(View.GONE);
437                 mAnimateOutValue = MIN_ANIMATION_VALUE;
438             }
439 
440             @Override
441             public void onAnimationCancel(Animator animator) {
442             }
443 
444             @Override
445             public void onAnimationRepeat(Animator animator) {
446             }
447 
448             @Override
449             public void onAnimationStart(Animator animator) {
450             }
451         });
452     }
453 
454 
initGestureDetector()455     private void initGestureDetector() {
456         mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
457             @Override
458             public boolean onFling(MotionEvent me1, MotionEvent me2,
459                     float velocityX, float velocityY) {
460                 if (mPossiblePoly) return false;
461                 final float absX = Math.abs(velocityX);
462                 final float absY = Math.abs(velocityY);
463                 float deltaX = me2.getX() - me1.getX();
464                 float deltaY = me2.getY() - me1.getY();
465                 int travelX = getWidth() / 2; // Half the keyboard width
466                 int travelY = getHeight() / 2; // Half the keyboard height
467                 mSwipeTracker.computeCurrentVelocity(1000);
468                 final float endingVelocityX = mSwipeTracker.getXVelocity();
469                 final float endingVelocityY = mSwipeTracker.getYVelocity();
470                 boolean sendDownKey = false;
471                 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
472                     if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) {
473                         sendDownKey = true;
474                     } else {
475                         swipeRight();
476                         return true;
477                     }
478                 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
479                     if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) {
480                         sendDownKey = true;
481                     } else {
482                         swipeLeft();
483                         return true;
484                     }
485                 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
486                     if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) {
487                         sendDownKey = true;
488                     } else {
489                         swipeUp();
490                         return true;
491                     }
492                 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
493                     if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) {
494                         sendDownKey = true;
495                     } else {
496                         swipeDown();
497                         return true;
498                     }
499                 }
500 
501                 if (sendDownKey) {
502                     detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime());
503                 }
504                 return false;
505             }
506         });
507 
508         mGestureDetector.setIsLongpressEnabled(false);
509     }
510 
setOnKeyboardActionListener(OnKeyboardActionListener listener)511     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
512         mKeyboardActionListener = listener;
513     }
514 
515     /**
516      * Returns the {@link OnKeyboardActionListener} object.
517      * @return the listener attached to this keyboard
518      */
getOnKeyboardActionListener()519     protected OnKeyboardActionListener getOnKeyboardActionListener() {
520         return mKeyboardActionListener;
521     }
522 
523     /**
524      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
525      * view will re-layout itself to accommodate the keyboard.
526      * @see android.inputmethodservice.Keyboard
527      * @see #getKeyboard()
528      * @param keyboard the keyboard to display in this view
529      */
setKeyboard(Keyboard keyboard, Locale locale)530     public void setKeyboard(Keyboard keyboard, Locale locale) {
531         if (mKeyboard != null) {
532             showPreview(NOT_A_KEY);
533         }
534         // Remove any pending messages
535         removeMessages();
536         mKeyboard = keyboard;
537         List<Key> keys = mKeyboard.getKeys();
538         mKeys = keys.toArray(new Key[keys.size()]);
539         requestLayout();
540         // Hint to reallocate the buffer if the size changed
541         mKeyboardChanged = true;
542         invalidateAllKeys();
543         computeProximityThreshold(keyboard);
544         mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
545         // Switching to a different keyboard should abort any pending keys so that the key up
546         // doesn't get delivered to the old or new keyboard
547         mAbortKey = true; // Until the next ACTION_DOWN
548         mLocale = locale;
549     }
550 
551     /**
552      * Returns the current keyboard being displayed by this view.
553      * @return the currently attached keyboard
554      * @see #setKeyboard(android.inputmethodservice.Keyboard, java.util.Locale)
555      */
getKeyboard()556     public Keyboard getKeyboard() {
557         return mKeyboard;
558     }
559 
560     /**
561      * Sets the state of the shift key of the keyboard, if any.
562      * @param shifted whether or not to enable the state of the shift key
563      * @return true if the shift key state changed, false if there was no change
564      * @see KeyboardView#isShifted()
565      */
setShifted(boolean shifted)566     public boolean setShifted(boolean shifted) {
567         if (mKeyboard != null) {
568             if (mKeyboard.setShifted(shifted)) {
569                 // The whole keyboard probably needs to be redrawn
570                 invalidateAllKeys();
571                 return true;
572             }
573         }
574         return false;
575     }
576 
577     /**
578      * Returns the state of the shift key of the keyboard, if any.
579      * @return true if the shift is in a pressed state, false otherwise. If there is
580      * no shift key on the keyboard or there is no keyboard attached, it returns false.
581      * @see KeyboardView#setShifted(boolean)
582      */
isShifted()583     public boolean isShifted() {
584         if (mKeyboard != null) {
585             return mKeyboard.isShifted();
586         }
587         return false;
588     }
589 
590     /**
591      * Enables or disables the key feedback popup. This is a popup that shows a magnified
592      * version of the depressed key. By default the preview is enabled.
593      * @param previewEnabled whether or not to enable the key feedback popup
594      * @see #isPreviewEnabled()
595      */
setPreviewEnabled(boolean previewEnabled)596     public void setPreviewEnabled(boolean previewEnabled) {
597         mShowPreview = previewEnabled;
598     }
599 
600     /**
601      * Returns the enabled state of the key feedback popup.
602      * @return whether or not the key feedback popup is enabled
603      * @see #setPreviewEnabled(boolean)
604      */
isPreviewEnabled()605     public boolean isPreviewEnabled() {
606         return mShowPreview;
607     }
608 
setPopupParent(View v)609     public void setPopupParent(View v) {
610         mPopupParent = v;
611     }
612 
setPopupOffset(int x, int y)613     public void setPopupOffset(int x, int y) {
614         mMiniKeyboardOffsetX = x;
615         mMiniKeyboardOffsetY = y;
616         if (mPreviewPopup.isShowing()) {
617             mPreviewPopup.dismiss();
618         }
619     }
620 
621     /**
622      * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
623      * codes for adjacent keys.  When disabled, only the primary key code will be
624      * reported.
625      * @param enabled whether or not the proximity correction is enabled
626      */
setProximityCorrectionEnabled(boolean enabled)627     public void setProximityCorrectionEnabled(boolean enabled) {
628         mProximityCorrectOn = enabled;
629     }
630 
631     /**
632      * Returns true if proximity correction is enabled.
633      */
isProximityCorrectionEnabled()634     public boolean isProximityCorrectionEnabled() {
635         return mProximityCorrectOn;
636     }
637 
getUseSecondaryColor()638     public boolean getUseSecondaryColor() {
639         return mUseSecondaryColor;
640     }
641 
setUseSecondaryColor(boolean useSecondaryColor)642     public void setUseSecondaryColor(boolean useSecondaryColor) {
643         mUseSecondaryColor = useSecondaryColor;
644     }
645 
646     /**
647      * Popup keyboard close button clicked.
648      * @hide
649      */
650     @Override
onClick(View v)651     public void onClick(View v) {
652         dismissPopupKeyboard();
653     }
654 
adjustCase(CharSequence label)655     private CharSequence adjustCase(CharSequence label) {
656         if (mKeyboard.isShifted() && label != null && label.length() < 3
657                 && Character.isLowerCase(label.charAt(0))) {
658             label = label.toString().toUpperCase(mLocale);
659         }
660         return label;
661     }
662 
663     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)664     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
665         // Round up a little
666         if (mKeyboard == null) {
667             setMeasuredDimension(getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
668         } else {
669             int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
670             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
671                 width = MeasureSpec.getSize(widthMeasureSpec);
672             }
673             setMeasuredDimension(width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
674         }
675     }
676 
677     /**
678      * Compute the average distance between adjacent keys (horizontally and vertically)
679      * and square it to get the proximity threshold. We use a square here and in computing
680      * the touch distance from a key's center to avoid taking a square root.
681      * @param keyboard
682      */
computeProximityThreshold(Keyboard keyboard)683     private void computeProximityThreshold(Keyboard keyboard) {
684         if (keyboard == null) return;
685         final Key[] keys = mKeys;
686         if (keys == null) return;
687         int length = keys.length;
688         int dimensionSum = 0;
689         for (int i = 0; i < length; i++) {
690             Key key = keys[i];
691             dimensionSum += Math.min(key.width, key.height) + key.gap;
692         }
693         if (dimensionSum < 0 || length == 0) return;
694         mProximityThreshold = (int) (dimensionSum * 1.4f / length);
695         mProximityThreshold *= mProximityThreshold; // Square it
696     }
697 
698     @Override
onSizeChanged(int w, int h, int oldw, int oldh)699     public void onSizeChanged(int w, int h, int oldw, int oldh) {
700         super.onSizeChanged(w, h, oldw, oldh);
701         if (mKeyboard != null) {
702             //TODO(ftamp): mKeyboard.resize(w, h);
703         }
704         // Release the buffer, if any and it will be reallocated on the next draw
705         mBuffer = null;
706     }
707 
708     @Override
onDraw(Canvas canvas)709     public void onDraw(Canvas canvas) {
710         super.onDraw(canvas);
711         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
712             onBufferDraw();
713         }
714         canvas.drawBitmap(mBuffer, 0, 0, null);
715     }
716 
717     @SuppressWarnings("unused")
onBufferDraw()718     private void onBufferDraw() {
719         if (mBuffer == null || mKeyboardChanged) {
720             if (mBuffer == null || mKeyboardChanged &&
721                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
722                 // Make sure our bitmap is at least 1x1
723                 final int width = Math.max(1, getWidth());
724                 final int height = Math.max(1, getHeight());
725                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
726                 mCanvas = new Canvas(mBuffer);
727             }
728             invalidateAllKeys();
729             mKeyboardChanged = false;
730         }
731         final Canvas canvas = mCanvas;
732         canvas.clipRect(mDirtyRect, Op.REPLACE);
733 
734         if (mKeyboard == null) return;
735 
736         final Paint paint = mPaint;
737         final Drawable keyBackground = mKeyBackground;
738         final Rect clipRegion = mClipRegion;
739         final Rect padding = mPadding;
740         final int kbdPaddingLeft = getPaddingLeft();
741         final int kbdPaddingTop = getPaddingTop();
742         final Key[] keys = mKeys;
743         final Key invalidKey = mInvalidatedKey;
744 
745         paint.setColor(mKeyTextColorPrimary);
746         boolean drawSingleKey = false;
747         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
748             // Is clipRegion completely contained within the invalidated key?
749             if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
750                     invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
751                     invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
752                     invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
753                 drawSingleKey = true;
754             }
755         }
756         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
757 
758         // Animate in translation. No overall translation for animating out.
759         float offset = getWidth()/2 * (1 - mAnimateInValue);
760         canvas.translate(offset, 0);
761 
762         // Draw background.
763         float backgroundWidth = getWidth() * (mAnimateInValue - mAnimateOutValue);
764         paint.setColor(mBackgroundColor);
765         int center;
766         if (mLastSentIndex != NOT_A_KEY && mLastSentIndex < keys.length) {
767             center = keys[mLastSentIndex].x + keys[mLastSentIndex].width / 2;
768         } else {
769             center = getWidth()/2;
770         }
771         canvas.drawRect(mAnimateOutValue * center, 0, mAnimateOutValue * center + backgroundWidth,
772                 getHeight(), paint);
773         final int keyCount = keys.length;
774         for (int i = 0; i < keyCount; i++) {
775             final Key key = keys[i];
776             if (drawSingleKey && invalidKey != key) {
777                 continue;
778             }
779             int[] drawableState = key.getCurrentDrawableState();
780             keyBackground.setState(drawableState);
781             if (key.icon != null) {
782                 key.icon.setState(drawableState);
783             }
784 
785 
786             // Switch the character to uppercase if shift is pressed
787             String label = key.label == null? null : adjustCase(key.label).toString();
788 
789             final Rect bounds = keyBackground.getBounds();
790             if (key.width != bounds.right ||
791                     key.height != bounds.bottom) {
792                 keyBackground.setBounds(0, 0, key.width, key.height);
793             }
794             float animateOutOffset = 0;
795             if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
796                 animateOutOffset = (center - key.x - key.width/2) * mAnimateOutValue;
797             }
798             canvas.translate(key.x + kbdPaddingLeft + animateOutOffset, key.y + kbdPaddingTop);
799             keyBackground.draw(canvas);
800 
801             if (label != null && label.length() > 0) {
802                 // Use primary color for letters and digits, secondary color for everything else
803                 if (Character.isLetterOrDigit(label.charAt(0))) {
804                     paint.setColor(mKeyTextColorPrimary);
805                 } else if (mUseSecondaryColor) {
806                     paint.setColor(mKeyTextColorSecondary);
807                 }
808                 // For characters, use large font. For labels like "Done", use small font.
809                 if (label.length() > 1 && key.codes.length < 2) {
810                     paint.setTextSize(mLabelTextSize);
811                 } else if (isPunctuation(label)) {
812                     paint.setTextSize(mKeyPunctuationSize);
813                 } else {
814                     paint.setTextSize(mKeyTextSize);
815                 }
816                 if (mAnimateInValue != MAX_ANIMATION_VALUE) {
817                     int alpha =
818                             (int) ((backgroundWidth - key.x - key.width) / key.width * MAX_ALPHA);
819                     if (alpha > MAX_ALPHA) {
820                         alpha = MAX_ALPHA;
821                     } else if (alpha < 0) {
822                         alpha = 0;
823                     }
824                     paint.setAlpha(alpha);
825                 } else if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
826                     int alpha;
827                     if (i == mLastSentIndex) {
828                         // Fade out selected character from 1/2 to 3/4 of animate out.
829                         alpha = (int) ((3 - mAnimateOutValue * 4) * MAX_ALPHA);
830                     } else {
831                         // Fade out the rest from start to 1/2 of animate out.
832                         alpha = (int) ((1 - mAnimateOutValue * 2) * MAX_ALPHA);
833                     }
834                     if (alpha > MAX_ALPHA) {
835                         alpha = MAX_ALPHA;
836                     } else if (alpha < 0) {
837                         alpha = 0;
838                     }
839                     paint.setAlpha(alpha);
840                 }
841                 // Draw the text
842                 canvas.drawText(label,
843                         (key.width - padding.left - padding.right) / 2
844                                 + padding.left,
845                         (key.height - padding.top - padding.bottom) / 2
846                                 + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
847                         paint);
848                 // Turn off drop shadow
849                 paint.setShadowLayer(0, 0, 0, 0);
850             } else if (key.icon != null) {
851                 final int drawableX = (key.width - padding.left - padding.right
852                         - key.icon.getIntrinsicWidth()) / 2 + padding.left;
853                 final int drawableY = (key.height - padding.top - padding.bottom
854                         - key.icon.getIntrinsicHeight()) / 2 + padding.top;
855                 canvas.translate(drawableX, drawableY);
856                 key.icon.setBounds(0, 0,
857                         key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
858                 key.icon.draw(canvas);
859                 canvas.translate(-drawableX, -drawableY);
860             }
861             canvas.translate(-key.x - kbdPaddingLeft - animateOutOffset, -key.y - kbdPaddingTop);
862         }
863         mInvalidatedKey = null;
864         // Overlay a dark rectangle to dim the keyboard
865         paint.setColor(mPopupScrimColor);
866         paint.setAlpha(mScrimAlpha);
867         canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
868         canvas.translate(-offset, 0);
869 
870         if (DEBUG && mShowTouchPoints) {
871             paint.setAlpha(128);
872             paint.setColor(0xFFFF0000);
873             canvas.drawCircle(mStartX, mStartY, 3, paint);
874             canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
875             paint.setColor(0xFF0000FF);
876             canvas.drawCircle(mLastX, mLastY, 3, paint);
877             paint.setColor(0xFF00FF00);
878             canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
879         }
880 
881         mDrawPending = false;
882         mDirtyRect.setEmpty();
883     }
884 
isPunctuation(String label)885     private boolean isPunctuation(String label) {
886         return PUNCTUATION_PATTERN.matcher(label).matches();
887     }
888 
getKeyIndices(int x, int y, int[] allKeys)889     private int getKeyIndices(int x, int y, int[] allKeys) {
890         final Key[] keys = mKeys;
891         int primaryIndex = NOT_A_KEY;
892         int closestKey = NOT_A_KEY;
893         int closestKeyDist = mProximityThreshold + 1;
894         Arrays.fill(mDistances, Integer.MAX_VALUE);
895         int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
896         final int keyCount = nearestKeyIndices.length;
897         for (int i = 0; i < keyCount; i++) {
898             final Key key = keys[nearestKeyIndices[i]];
899             int dist = 0;
900             boolean isInside = key.isInside(x,y);
901             if (isInside) {
902                 primaryIndex = nearestKeyIndices[i];
903             }
904 
905             if (((mProximityCorrectOn
906                     && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
907                     || isInside)
908                     && key.codes[0] > 32) {
909                 // Find insertion point
910                 final int nCodes = key.codes.length;
911                 if (dist < closestKeyDist) {
912                     closestKeyDist = dist;
913                     closestKey = nearestKeyIndices[i];
914                 }
915 
916                 if (allKeys == null) continue;
917 
918                 for (int j = 0; j < mDistances.length; j++) {
919                     if (mDistances[j] > dist) {
920                         // Make space for nCodes codes
921                         System.arraycopy(mDistances, j, mDistances, j + nCodes,
922                                 mDistances.length - j - nCodes);
923                         System.arraycopy(allKeys, j, allKeys, j + nCodes,
924                                 allKeys.length - j - nCodes);
925                         for (int c = 0; c < nCodes; c++) {
926                             allKeys[j + c] = key.codes[c];
927                             mDistances[j + c] = dist;
928                         }
929                         break;
930                     }
931                 }
932             }
933         }
934         if (primaryIndex == NOT_A_KEY) {
935             primaryIndex = closestKey;
936         }
937         return primaryIndex;
938     }
939 
detectAndSendKey(int index, int x, int y, long eventTime)940     private void detectAndSendKey(int index, int x, int y, long eventTime) {
941         if (index != NOT_A_KEY && index < mKeys.length) {
942             final Key key = mKeys[index];
943             if (key.text != null) {
944                 mKeyboardActionListener.onText(key.text);
945                 mKeyboardActionListener.onRelease(NOT_A_KEY);
946             } else {
947                 int code = key.codes[0];
948                 //TextEntryState.keyPressedAt(key, x, y);
949                 int[] codes = new int[MAX_NEARBY_KEYS];
950                 Arrays.fill(codes, NOT_A_KEY);
951                 getKeyIndices(x, y, codes);
952                 // Multi-tap
953                 if (mInMultiTap) {
954                     if (mTapCount != -1) {
955                         mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
956                     } else {
957                         mTapCount = 0;
958                     }
959                     code = key.codes[mTapCount];
960                 }
961                 mKeyboardActionListener.onKey(code, codes);
962                 mKeyboardActionListener.onRelease(code);
963             }
964             mLastSentIndex = index;
965             mLastTapTime = eventTime;
966         }
967     }
968 
969     /**
970      * Handle multi-tap keys by producing the key label for the current multi-tap state.
971      */
getPreviewText(Key key)972     private CharSequence getPreviewText(Key key) {
973         if (mInMultiTap) {
974             // Multi-tap
975             mPreviewLabel.setLength(0);
976             mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
977             return adjustCase(mPreviewLabel);
978         } else {
979             return adjustCase(key.label);
980         }
981     }
982 
showPreview(int keyIndex)983     private void showPreview(int keyIndex) {
984         int oldKeyIndex = mCurrentKeyIndex;
985         final PopupWindow previewPopup = mPreviewPopup;
986 
987         mCurrentKeyIndex = keyIndex;
988         // Release the old key and press the new key
989         final Key[] keys = mKeys;
990         if (oldKeyIndex != mCurrentKeyIndex) {
991             if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
992                 Key oldKey = keys[oldKeyIndex];
993                 // Keyboard.Key.onReleased(boolean) should be called here, but due to b/21446448,
994                 // onReleased doesn't check the input param and it always toggles the on state.
995                 // Therefore we need to just set the pressed state to false here.
996                 oldKey.pressed = false;
997                 invalidateKey(oldKeyIndex);
998             }
999             if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
1000                 Key newKey = keys[mCurrentKeyIndex];
1001                 newKey.onPressed();
1002                 invalidateKey(mCurrentKeyIndex);
1003             }
1004         }
1005         // If key changed and preview is on ...
1006         if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
1007             mHandler.removeMessages(MSG_SHOW_PREVIEW);
1008             if (previewPopup.isShowing()) {
1009                 if (keyIndex == NOT_A_KEY) {
1010                     mHandler.sendMessageDelayed(mHandler
1011                                     .obtainMessage(MSG_REMOVE_PREVIEW),
1012                             DELAY_AFTER_PREVIEW);
1013                 }
1014             }
1015             if (keyIndex != NOT_A_KEY) {
1016                 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
1017                     // Show right away, if it's already visible and finger is moving around
1018                     showKey(keyIndex);
1019                 } else {
1020                     mHandler.sendMessageDelayed(
1021                             mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
1022                             DELAY_BEFORE_PREVIEW);
1023                 }
1024             }
1025         }
1026     }
1027 
showKey(final int keyIndex)1028     private void showKey(final int keyIndex) {
1029         final PopupWindow previewPopup = mPreviewPopup;
1030         final Key[] keys = mKeys;
1031         if (keyIndex < 0 || keyIndex >= mKeys.length) return;
1032         Key key = keys[keyIndex];
1033         if (key.icon != null) {
1034             mPreviewText.setCompoundDrawables(null, null, null,
1035                     key.iconPreview != null ? key.iconPreview : key.icon);
1036             mPreviewText.setText(null);
1037         } else {
1038             mPreviewText.setCompoundDrawables(null, null, null, null);
1039             mPreviewText.setText(getPreviewText(key));
1040             if (key.label.length() > 1 && key.codes.length < 2) {
1041                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
1042                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
1043             } else {
1044                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
1045                 mPreviewText.setTypeface(Typeface.DEFAULT);
1046             }
1047         }
1048         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1049                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
1050         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
1051                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
1052         final int popupHeight = mPreviewHeight;
1053         LayoutParams lp = mPreviewText.getLayoutParams();
1054         if (lp != null) {
1055             lp.width = popupWidth;
1056             lp.height = popupHeight;
1057         }
1058         if (!mPreviewCentered) {
1059             mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + getPaddingLeft();
1060             mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
1061         } else {
1062             // TODO: Fix this if centering is brought back
1063             mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
1064             mPopupPreviewY = - mPreviewText.getMeasuredHeight();
1065         }
1066         mHandler.removeMessages(MSG_REMOVE_PREVIEW);
1067         getLocationInWindow(mCoordinates);
1068         mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
1069         mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
1070 
1071         // Set the preview background state
1072         /*
1073         mPreviewText.getBackground().setState(
1074                  key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
1075         */
1076         mPopupPreviewX += mCoordinates[0];
1077         mPopupPreviewY += mCoordinates[1];
1078 
1079         // If the popup cannot be shown above the key, put it on the side
1080         getLocationOnScreen(mCoordinates);
1081         if (mPopupPreviewY + mCoordinates[1] < 0) {
1082             // If the key you're pressing is on the left side of the keyboard, show the popup on
1083             // the right, offset by enough to see at least one key to the left/right.
1084             if (key.x + key.width <= getWidth() / 2) {
1085                 mPopupPreviewX += (int) (key.width * 2.5);
1086             } else {
1087                 mPopupPreviewX -= (int) (key.width * 2.5);
1088             }
1089             mPopupPreviewY += popupHeight;
1090         }
1091 
1092         if (previewPopup.isShowing()) {
1093             previewPopup.update(mPopupPreviewX, mPopupPreviewY,
1094                     popupWidth, popupHeight);
1095         } else {
1096             previewPopup.setWidth(popupWidth);
1097             previewPopup.setHeight(popupHeight);
1098             previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
1099                     mPopupPreviewX, mPopupPreviewY);
1100         }
1101         mPreviewText.setVisibility(VISIBLE);
1102     }
1103 
1104     /**
1105      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1106      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1107      * draws the cached buffer.
1108      * @see #invalidateKey(int)
1109      */
invalidateAllKeys()1110     public void invalidateAllKeys() {
1111         mDirtyRect.union(0, 0, getWidth(), getHeight());
1112         mDrawPending = true;
1113         invalidate();
1114     }
1115 
1116     /**
1117      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1118      * one key is changing it's content. Any changes that affect the position or size of the key
1119      * may not be honored.
1120      * @param keyIndex the index of the key in the attached {@link android.inputmethodservice.Keyboard}.
1121      * @see #invalidateAllKeys
1122      */
invalidateKey(int keyIndex)1123     public void invalidateKey(int keyIndex) {
1124         if (mKeys == null) return;
1125         if (keyIndex < 0 || keyIndex >= mKeys.length) {
1126             return;
1127         }
1128         final Key key = mKeys[keyIndex];
1129         mInvalidatedKey = key;
1130         mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1131                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1132         onBufferDraw();
1133         invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1134                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1135     }
1136 
openPopupIfRequired(MotionEvent unused)1137     private void openPopupIfRequired(MotionEvent unused) {
1138         // Check if we have a popup keyboard first.
1139         if (mPopupKeyboardView == null || mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
1140             return;
1141         }
1142 
1143         Key popupKey = mKeys[mCurrentKey];
1144         boolean result = onLongPress(popupKey);
1145         if (result) {
1146             mAbortKey = true;
1147             showPreview(NOT_A_KEY);
1148         }
1149     }
1150 
1151     /**
1152      * Called when a key is long pressed. By default this will open any popup keyboard associated
1153      * with this key through the attributes popupLayout and popupCharacters.
1154      *
1155      * @param popupKey the key that was long pressed
1156      * @return true if the long press is handled, false otherwise. Subclasses should call the
1157      * method on the base class if the subclass doesn't wish to handle the call.
1158      */
onLongPress(Key popupKey)1159     protected boolean onLongPress(Key popupKey) {
1160         int popupKeyboardId = popupKey.popupResId;
1161 
1162         if (popupKeyboardId != 0) {
1163             Keyboard keyboard;
1164             if (popupKey.popupCharacters != null) {
1165                 keyboard = new Keyboard(getContext(), popupKeyboardId,
1166                         popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
1167             } else {
1168                 keyboard = new Keyboard(getContext(), popupKeyboardId);
1169             }
1170             mPopupKeyboardView.setKeyboard(keyboard, mLocale);
1171             mPopupKeyboardView.setVisibility(VISIBLE);
1172             mPopupKeyboardView.setShifted(isShifted());
1173             mPopupKeyboardView.mAnimateInAnimator.start();
1174             mPopupKeyboardView.mLastSentIndex = NOT_A_KEY;
1175             mScrimAlphaAnimator.setStartDelay(0);
1176             mScrimAlphaAnimator.start();
1177             mPopupKeyboardView.invalidate();
1178             mPopupKeyboardOnScreen = true;
1179             invalidateAllKeys();
1180             return true;
1181         }
1182         return false;
1183     }
1184 
1185     @Override
onHoverEvent(MotionEvent event)1186     public boolean onHoverEvent(MotionEvent event) {
1187         if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
1188             final int action = event.getAction();
1189             switch (action) {
1190                 case MotionEvent.ACTION_HOVER_ENTER: {
1191                     event.setAction(MotionEvent.ACTION_DOWN);
1192                 } break;
1193                 case MotionEvent.ACTION_HOVER_MOVE: {
1194                     event.setAction(MotionEvent.ACTION_MOVE);
1195                 } break;
1196                 case MotionEvent.ACTION_HOVER_EXIT: {
1197                     event.setAction(MotionEvent.ACTION_UP);
1198                 } break;
1199             }
1200             return onTouchEvent(event);
1201         }
1202         return true;
1203     }
1204 
1205     @SuppressLint("ClickableViewAccessibility")
1206     @Override
onTouchEvent(MotionEvent me)1207     public boolean onTouchEvent(MotionEvent me) {
1208         // Don't process touches while animating.
1209         if (mAnimateInValue != MAX_ANIMATION_VALUE || mAnimateOutValue != MIN_ANIMATION_VALUE) {
1210             return false;
1211         }
1212 
1213         // Convert multi-pointer up/down events to single up/down events to
1214         // deal with the typical multi-pointer behavior of two-thumb typing
1215         final int pointerCount = me.getPointerCount();
1216         final int action = me.getAction();
1217         boolean result = false;
1218         final long now = me.getEventTime();
1219 
1220         if (pointerCount != mOldPointerCount) {
1221             if (pointerCount == 1) {
1222                 // Send a down event for the latest pointer
1223                 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1224                         me.getX(), me.getY(), me.getMetaState());
1225                 result = onModifiedTouchEvent(down, false);
1226                 down.recycle();
1227                 // If it's an up action, then deliver the up as well.
1228                 if (action == MotionEvent.ACTION_UP) {
1229                     result = onModifiedTouchEvent(me, true);
1230                 }
1231             } else {
1232                 // Send an up event for the last pointer
1233                 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1234                         mOldPointerX, mOldPointerY, me.getMetaState());
1235                 result = onModifiedTouchEvent(up, true);
1236                 up.recycle();
1237             }
1238         } else {
1239             if (pointerCount == 1) {
1240                 result = onModifiedTouchEvent(me, false);
1241                 mOldPointerX = me.getX();
1242                 mOldPointerY = me.getY();
1243             } else {
1244                 // Don't do anything when 2 pointers are down and moving.
1245                 result = true;
1246             }
1247         }
1248         mOldPointerCount = pointerCount;
1249 
1250         return result;
1251     }
1252 
onModifiedTouchEvent(MotionEvent me, boolean possiblePoly)1253     private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
1254         int touchX = (int) me.getX() - getPaddingLeft();
1255         int touchY = (int) me.getY() - getPaddingTop();
1256         if (touchY >= -mVerticalCorrection)
1257             touchY += mVerticalCorrection;
1258         final int action = me.getAction();
1259         final long eventTime = me.getEventTime();
1260         int keyIndex = getKeyIndices(touchX, touchY, null);
1261         mPossiblePoly = possiblePoly;
1262 
1263         // Track the last few movements to look for spurious swipes.
1264         if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
1265         mSwipeTracker.addMovement(me);
1266 
1267         // Ignore all motion events until a DOWN.
1268         if (mAbortKey
1269                 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
1270             return true;
1271         }
1272 
1273         if (mGestureDetector.onTouchEvent(me)) {
1274             showPreview(NOT_A_KEY);
1275             mHandler.removeMessages(MSG_REPEAT);
1276             mHandler.removeMessages(MSG_LONGPRESS);
1277             return true;
1278         }
1279 
1280         if (mPopupKeyboardOnScreen) {
1281             dismissPopupKeyboard();
1282             return true;
1283         }
1284 
1285         switch (action) {
1286             case MotionEvent.ACTION_DOWN:
1287                 mAbortKey = false;
1288                 mStartX = touchX;
1289                 mStartY = touchY;
1290                 mLastCodeX = touchX;
1291                 mLastCodeY = touchY;
1292                 mLastKeyTime = 0;
1293                 mCurrentKeyTime = 0;
1294                 mLastKey = NOT_A_KEY;
1295                 mCurrentKey = keyIndex;
1296                 mDownKey = keyIndex;
1297                 mDownTime = me.getEventTime();
1298                 mLastMoveTime = mDownTime;
1299                 checkMultiTap(eventTime, keyIndex);
1300                 if (keyIndex != NOT_A_KEY) {
1301                     mKeyboardActionListener.onPress(mKeys[keyIndex].codes[0]);
1302                 }
1303                 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1304                     mRepeatKeyIndex = mCurrentKey;
1305                     Message msg = mHandler.obtainMessage(MSG_REPEAT);
1306                     mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1307                     repeatKey();
1308                     // Delivering the key could have caused an abort
1309                     if (mAbortKey) {
1310                         mRepeatKeyIndex = NOT_A_KEY;
1311                         break;
1312                     }
1313                 }
1314                 if (mCurrentKey != NOT_A_KEY) {
1315                     Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1316                     mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1317                 }
1318                 showPreview(keyIndex);
1319                 break;
1320 
1321             case MotionEvent.ACTION_MOVE:
1322                 boolean continueLongPress = false;
1323                 if (keyIndex != NOT_A_KEY) {
1324                     if (mCurrentKey == NOT_A_KEY) {
1325                         mCurrentKey = keyIndex;
1326                         mCurrentKeyTime = eventTime - mDownTime;
1327                     } else {
1328                         if (keyIndex == mCurrentKey) {
1329                             mCurrentKeyTime += eventTime - mLastMoveTime;
1330                             continueLongPress = true;
1331                         } else if (mRepeatKeyIndex == NOT_A_KEY) {
1332                             resetMultiTap();
1333                             mLastKey = mCurrentKey;
1334                             mLastCodeX = mLastX;
1335                             mLastCodeY = mLastY;
1336                             mLastKeyTime =
1337                                     mCurrentKeyTime + eventTime - mLastMoveTime;
1338                             mCurrentKey = keyIndex;
1339                             mCurrentKeyTime = 0;
1340                         }
1341                     }
1342                 }
1343                 if (!continueLongPress) {
1344                     // Cancel old longpress
1345                     mHandler.removeMessages(MSG_LONGPRESS);
1346                     // Start new longpress if key has changed
1347                     if (keyIndex != NOT_A_KEY) {
1348                         Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1349                         mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1350                     }
1351                 }
1352                 showPreview(mCurrentKey);
1353                 mLastMoveTime = eventTime;
1354                 break;
1355 
1356             case MotionEvent.ACTION_UP:
1357                 removeMessages();
1358                 if (keyIndex == mCurrentKey) {
1359                     mCurrentKeyTime += eventTime - mLastMoveTime;
1360                 } else {
1361                     resetMultiTap();
1362                     mLastKey = mCurrentKey;
1363                     mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1364                     mCurrentKey = keyIndex;
1365                     mCurrentKeyTime = 0;
1366                 }
1367                 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
1368                         && mLastKey != NOT_A_KEY) {
1369                     mCurrentKey = mLastKey;
1370                     touchX = mLastCodeX;
1371                     touchY = mLastCodeY;
1372                 }
1373                 showPreview(NOT_A_KEY);
1374                 Arrays.fill(mKeyIndices, NOT_A_KEY);
1375                 // If we're not on a repeating key (which sends on a DOWN event)
1376                 if (mRepeatKeyIndex == NOT_A_KEY && !mPopupKeyboardOnScreen && !mAbortKey) {
1377                     detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
1378                 }
1379                 invalidateKey(keyIndex);
1380                 mRepeatKeyIndex = NOT_A_KEY;
1381                 break;
1382             case MotionEvent.ACTION_CANCEL:
1383                 removeMessages();
1384                 dismissPopupKeyboard();
1385                 mAbortKey = true;
1386                 showPreview(NOT_A_KEY);
1387                 invalidateKey(mCurrentKey);
1388                 break;
1389         }
1390         mLastX = touchX;
1391         mLastY = touchY;
1392         return true;
1393     }
1394 
repeatKey()1395     private boolean repeatKey() {
1396         Key key = mKeys[mRepeatKeyIndex];
1397         detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
1398         return true;
1399     }
1400 
swipeRight()1401     protected void swipeRight() {
1402         mKeyboardActionListener.swipeRight();
1403     }
1404 
swipeLeft()1405     protected void swipeLeft() {
1406         mKeyboardActionListener.swipeLeft();
1407     }
1408 
swipeUp()1409     protected void swipeUp() {
1410         mKeyboardActionListener.swipeUp();
1411     }
1412 
swipeDown()1413     protected void swipeDown() {
1414         mKeyboardActionListener.swipeDown();
1415     }
1416 
closing()1417     public void closing() {
1418         if (mPreviewPopup.isShowing()) {
1419             mPreviewPopup.dismiss();
1420         }
1421         removeMessages();
1422 
1423         dismissPopupKeyboard();
1424         mBuffer = null;
1425         mCanvas = null;
1426         mMiniKeyboardCache.clear();
1427     }
1428 
removeMessages()1429     private void removeMessages() {
1430         mHandler.removeMessages(MSG_REPEAT);
1431         mHandler.removeMessages(MSG_LONGPRESS);
1432         mHandler.removeMessages(MSG_SHOW_PREVIEW);
1433     }
1434 
1435     @Override
onDetachedFromWindow()1436     public void onDetachedFromWindow() {
1437         super.onDetachedFromWindow();
1438         closing();
1439     }
1440 
dismissPopupKeyboard()1441     public void dismissPopupKeyboard() {
1442         mPopupKeyboardOnScreen = false;
1443         if (mPopupKeyboardView != null && mPopupKeyboardView.getVisibility() == View.VISIBLE) {
1444             if (mPopupKeyboardView.mAnimateInValue == MAX_ANIMATION_VALUE) {
1445                 mPopupKeyboardView.mAnimateOutAnimator.start();
1446                 mScrimAlphaAnimator.setStartDelay(ANIMATE_SCRIM_DELAY_MS);
1447                 mScrimAlphaAnimator.reverse();
1448             } else {
1449                 mPopupKeyboardView.mAnimateInAnimator.reverse();
1450                 mScrimAlphaAnimator.setStartDelay(0);
1451                 mScrimAlphaAnimator.reverse();
1452             }
1453         }
1454 
1455         invalidateAllKeys();
1456     }
1457 
setPopupKeyboardView(KeyboardView popupKeyboardView)1458     public void setPopupKeyboardView(KeyboardView popupKeyboardView) {
1459         mPopupKeyboardView = popupKeyboardView;
1460         mPopupKeyboardView.mBackgroundColor = getResources().getColor(R.color.car_teal_700);
1461     }
1462 
resetMultiTap()1463     private void resetMultiTap() {
1464         mLastSentIndex = NOT_A_KEY;
1465         mTapCount = 0;
1466         mLastTapTime = -1;
1467         mInMultiTap = false;
1468     }
1469 
checkMultiTap(long eventTime, int keyIndex)1470     private void checkMultiTap(long eventTime, int keyIndex) {
1471         if (keyIndex == NOT_A_KEY) return;
1472         Key key = mKeys[keyIndex];
1473         if (key.codes.length > 1) {
1474             mInMultiTap = true;
1475             if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1476                     && keyIndex == mLastSentIndex) {
1477                 mTapCount = (mTapCount + 1) % key.codes.length;
1478                 return;
1479             } else {
1480                 mTapCount = -1;
1481                 return;
1482             }
1483         }
1484         if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1485             resetMultiTap();
1486         }
1487     }
1488 
1489     @Override
onGenericMotionEvent(MotionEvent event)1490     public boolean onGenericMotionEvent(MotionEvent event) {
1491         // Close the touch keyboard when the user scrolls.
1492         if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
1493             mKeyboardActionListener.stopInput();
1494             return true;
1495         }
1496         return false;
1497     }
1498 
1499     private static class SwipeTracker {
1500 
1501         static final int NUM_PAST = 4;
1502         static final int LONGEST_PAST_TIME = 200;
1503 
1504         final float mPastX[] = new float[NUM_PAST];
1505         final float mPastY[] = new float[NUM_PAST];
1506         final long mPastTime[] = new long[NUM_PAST];
1507 
1508         float mYVelocity;
1509         float mXVelocity;
1510 
clear()1511         public void clear() {
1512             mPastTime[0] = 0;
1513         }
1514 
addMovement(MotionEvent ev)1515         public void addMovement(MotionEvent ev) {
1516             long time = ev.getEventTime();
1517             final int N = ev.getHistorySize();
1518             for (int i=0; i<N; i++) {
1519                 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
1520                         ev.getHistoricalEventTime(i));
1521             }
1522             addPoint(ev.getX(), ev.getY(), time);
1523         }
1524 
addPoint(float x, float y, long time)1525         private void addPoint(float x, float y, long time) {
1526             int drop = -1;
1527             int i;
1528             final long[] pastTime = mPastTime;
1529             for (i=0; i<NUM_PAST; i++) {
1530                 if (pastTime[i] == 0) {
1531                     break;
1532                 } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
1533                     drop = i;
1534                 }
1535             }
1536             if (i == NUM_PAST && drop < 0) {
1537                 drop = 0;
1538             }
1539             if (drop == i) drop--;
1540             final float[] pastX = mPastX;
1541             final float[] pastY = mPastY;
1542             if (drop >= 0) {
1543                 final int start = drop+1;
1544                 final int count = NUM_PAST-drop-1;
1545                 System.arraycopy(pastX, start, pastX, 0, count);
1546                 System.arraycopy(pastY, start, pastY, 0, count);
1547                 System.arraycopy(pastTime, start, pastTime, 0, count);
1548                 i -= (drop+1);
1549             }
1550             pastX[i] = x;
1551             pastY[i] = y;
1552             pastTime[i] = time;
1553             i++;
1554             if (i < NUM_PAST) {
1555                 pastTime[i] = 0;
1556             }
1557         }
1558 
computeCurrentVelocity(int units)1559         public void computeCurrentVelocity(int units) {
1560             computeCurrentVelocity(units, Float.MAX_VALUE);
1561         }
1562 
computeCurrentVelocity(int units, float maxVelocity)1563         public void computeCurrentVelocity(int units, float maxVelocity) {
1564             final float[] pastX = mPastX;
1565             final float[] pastY = mPastY;
1566             final long[] pastTime = mPastTime;
1567 
1568             final float oldestX = pastX[0];
1569             final float oldestY = pastY[0];
1570             final long oldestTime = pastTime[0];
1571             float accumX = 0;
1572             float accumY = 0;
1573             int N=0;
1574             while (N < NUM_PAST) {
1575                 if (pastTime[N] == 0) {
1576                     break;
1577                 }
1578                 N++;
1579             }
1580 
1581             for (int i=1; i < N; i++) {
1582                 final int dur = (int)(pastTime[i] - oldestTime);
1583                 if (dur == 0) continue;
1584                 float dist = pastX[i] - oldestX;
1585                 float vel = (dist/dur) * units;   // pixels/frame.
1586                 if (accumX == 0) accumX = vel;
1587                 else accumX = (accumX + vel) * .5f;
1588 
1589                 dist = pastY[i] - oldestY;
1590                 vel = (dist/dur) * units;   // pixels/frame.
1591                 if (accumY == 0) accumY = vel;
1592                 else accumY = (accumY + vel) * .5f;
1593             }
1594             mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
1595                     : Math.min(accumX, maxVelocity);
1596             mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
1597                     : Math.min(accumY, maxVelocity);
1598         }
1599 
1600         public float getXVelocity() {
1601             return mXVelocity;
1602         }
1603 
1604         public float getYVelocity() {
1605             return mYVelocity;
1606         }
1607     }
1608 
1609     private static class KeyboardHander extends Handler {
1610         private final WeakReference<KeyboardView> mKeyboardView;
1611 
1612         public KeyboardHander(KeyboardView keyboardView) {
1613             mKeyboardView = new WeakReference<KeyboardView>(keyboardView);
1614         }
1615 
1616         @Override
1617         public void handleMessage(Message msg) {
1618             KeyboardView keyboardView = mKeyboardView.get();
1619             if (keyboardView != null) {
1620                 switch (msg.what) {
1621                     case MSG_SHOW_PREVIEW:
1622                         keyboardView.showKey(msg.arg1);
1623                         break;
1624                     case MSG_REMOVE_PREVIEW:
1625                         keyboardView.mPreviewText.setVisibility(INVISIBLE);
1626                         break;
1627                     case MSG_REPEAT:
1628                         if (keyboardView.repeatKey()) {
1629                             Message repeat = Message.obtain(this, MSG_REPEAT);
1630                             sendMessageDelayed(repeat, REPEAT_INTERVAL);
1631                         }
1632                         break;
1633                     case MSG_LONGPRESS:
1634                         keyboardView.openPopupIfRequired((MotionEvent) msg.obj);
1635                         break;
1636                 }
1637             }
1638         }
1639     }
1640 }
1641