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(android.support.car.ui.R.color.car_700);
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             Log.d("Yao", "Keyboard height " + mKeyboard.getHeight() + " " + getPaddingTop() + " " + getPaddingBottom());
676         }
677     }
678 
679     /**
680      * Compute the average distance between adjacent keys (horizontally and vertically)
681      * and square it to get the proximity threshold. We use a square here and in computing
682      * the touch distance from a key's center to avoid taking a square root.
683      * @param keyboard
684      */
computeProximityThreshold(Keyboard keyboard)685     private void computeProximityThreshold(Keyboard keyboard) {
686         if (keyboard == null) return;
687         final Key[] keys = mKeys;
688         if (keys == null) return;
689         int length = keys.length;
690         int dimensionSum = 0;
691         for (int i = 0; i < length; i++) {
692             Key key = keys[i];
693             dimensionSum += Math.min(key.width, key.height) + key.gap;
694         }
695         if (dimensionSum < 0 || length == 0) return;
696         mProximityThreshold = (int) (dimensionSum * 1.4f / length);
697         mProximityThreshold *= mProximityThreshold; // Square it
698     }
699 
700     @Override
onSizeChanged(int w, int h, int oldw, int oldh)701     public void onSizeChanged(int w, int h, int oldw, int oldh) {
702         super.onSizeChanged(w, h, oldw, oldh);
703         if (mKeyboard != null) {
704             //TODO(ftamp): mKeyboard.resize(w, h);
705         }
706         // Release the buffer, if any and it will be reallocated on the next draw
707         mBuffer = null;
708     }
709 
710     @Override
onDraw(Canvas canvas)711     public void onDraw(Canvas canvas) {
712         super.onDraw(canvas);
713         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
714             onBufferDraw();
715         }
716         canvas.drawBitmap(mBuffer, 0, 0, null);
717     }
718 
719     @SuppressWarnings("unused")
onBufferDraw()720     private void onBufferDraw() {
721         if (mBuffer == null || mKeyboardChanged) {
722             if (mBuffer == null || mKeyboardChanged &&
723                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
724                 // Make sure our bitmap is at least 1x1
725                 final int width = Math.max(1, getWidth());
726                 final int height = Math.max(1, getHeight());
727                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
728                 mCanvas = new Canvas(mBuffer);
729             }
730             invalidateAllKeys();
731             mKeyboardChanged = false;
732         }
733         final Canvas canvas = mCanvas;
734         canvas.clipRect(mDirtyRect, Op.REPLACE);
735 
736         if (mKeyboard == null) return;
737 
738         final Paint paint = mPaint;
739         final Drawable keyBackground = mKeyBackground;
740         final Rect clipRegion = mClipRegion;
741         final Rect padding = mPadding;
742         final int kbdPaddingLeft = getPaddingLeft();
743         final int kbdPaddingTop = getPaddingTop();
744         final Key[] keys = mKeys;
745         final Key invalidKey = mInvalidatedKey;
746 
747         paint.setColor(mKeyTextColorPrimary);
748         boolean drawSingleKey = false;
749         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
750             // Is clipRegion completely contained within the invalidated key?
751             if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
752                     invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
753                     invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
754                     invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
755                 drawSingleKey = true;
756             }
757         }
758         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
759 
760         // Animate in translation. No overall translation for animating out.
761         float offset = getWidth()/2 * (1 - mAnimateInValue);
762         canvas.translate(offset, 0);
763 
764         // Draw background.
765         float backgroundWidth = getWidth() * (mAnimateInValue - mAnimateOutValue);
766         paint.setColor(mBackgroundColor);
767         int center;
768         if (mLastSentIndex != NOT_A_KEY && mLastSentIndex < keys.length) {
769             center = keys[mLastSentIndex].x + keys[mLastSentIndex].width / 2;
770         } else {
771             center = getWidth()/2;
772         }
773         canvas.drawRect(mAnimateOutValue * center, 0, mAnimateOutValue * center + backgroundWidth,
774                 getHeight(), paint);
775         final int keyCount = keys.length;
776         for (int i = 0; i < keyCount; i++) {
777             final Key key = keys[i];
778             if (drawSingleKey && invalidKey != key) {
779                 continue;
780             }
781             int[] drawableState = key.getCurrentDrawableState();
782             keyBackground.setState(drawableState);
783             if (key.icon != null) {
784                 key.icon.setState(drawableState);
785             }
786 
787 
788             // Switch the character to uppercase if shift is pressed
789             String label = key.label == null? null : adjustCase(key.label).toString();
790 
791             final Rect bounds = keyBackground.getBounds();
792             if (key.width != bounds.right ||
793                     key.height != bounds.bottom) {
794                 keyBackground.setBounds(0, 0, key.width, key.height);
795             }
796             float animateOutOffset = 0;
797             if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
798                 animateOutOffset = (center - key.x - key.width/2) * mAnimateOutValue;
799             }
800             canvas.translate(key.x + kbdPaddingLeft + animateOutOffset, key.y + kbdPaddingTop);
801             keyBackground.draw(canvas);
802 
803             if (label != null && label.length() > 0) {
804                 // Use primary color for letters and digits, secondary color for everything else
805                 if (Character.isLetterOrDigit(label.charAt(0))) {
806                     paint.setColor(mKeyTextColorPrimary);
807                 } else if (mUseSecondaryColor) {
808                     paint.setColor(mKeyTextColorSecondary);
809                 }
810                 // For characters, use large font. For labels like "Done", use small font.
811                 if (label.length() > 1 && key.codes.length < 2) {
812                     paint.setTextSize(mLabelTextSize);
813                 } else if (isPunctuation(label)) {
814                     paint.setTextSize(mKeyPunctuationSize);
815                 } else {
816                     paint.setTextSize(mKeyTextSize);
817                 }
818                 if (mAnimateInValue != MAX_ANIMATION_VALUE) {
819                     int alpha =
820                             (int) ((backgroundWidth - key.x - key.width) / key.width * MAX_ALPHA);
821                     if (alpha > MAX_ALPHA) {
822                         alpha = MAX_ALPHA;
823                     } else if (alpha < 0) {
824                         alpha = 0;
825                     }
826                     paint.setAlpha(alpha);
827                 } else if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
828                     int alpha;
829                     if (i == mLastSentIndex) {
830                         // Fade out selected character from 1/2 to 3/4 of animate out.
831                         alpha = (int) ((3 - mAnimateOutValue * 4) * MAX_ALPHA);
832                     } else {
833                         // Fade out the rest from start to 1/2 of animate out.
834                         alpha = (int) ((1 - mAnimateOutValue * 2) * MAX_ALPHA);
835                     }
836                     if (alpha > MAX_ALPHA) {
837                         alpha = MAX_ALPHA;
838                     } else if (alpha < 0) {
839                         alpha = 0;
840                     }
841                     paint.setAlpha(alpha);
842                 }
843                 // Draw the text
844                 canvas.drawText(label,
845                         (key.width - padding.left - padding.right) / 2
846                                 + padding.left,
847                         (key.height - padding.top - padding.bottom) / 2
848                                 + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
849                         paint);
850                 // Turn off drop shadow
851                 paint.setShadowLayer(0, 0, 0, 0);
852             } else if (key.icon != null) {
853                 final int drawableX = (key.width - padding.left - padding.right
854                         - key.icon.getIntrinsicWidth()) / 2 + padding.left;
855                 final int drawableY = (key.height - padding.top - padding.bottom
856                         - key.icon.getIntrinsicHeight()) / 2 + padding.top;
857                 canvas.translate(drawableX, drawableY);
858                 key.icon.setBounds(0, 0,
859                         key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
860                 key.icon.draw(canvas);
861                 canvas.translate(-drawableX, -drawableY);
862             }
863             canvas.translate(-key.x - kbdPaddingLeft - animateOutOffset, -key.y - kbdPaddingTop);
864         }
865         mInvalidatedKey = null;
866         // Overlay a dark rectangle to dim the keyboard
867         paint.setColor(mPopupScrimColor);
868         paint.setAlpha(mScrimAlpha);
869         canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
870         canvas.translate(-offset, 0);
871 
872         if (DEBUG && mShowTouchPoints) {
873             paint.setAlpha(128);
874             paint.setColor(0xFFFF0000);
875             canvas.drawCircle(mStartX, mStartY, 3, paint);
876             canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
877             paint.setColor(0xFF0000FF);
878             canvas.drawCircle(mLastX, mLastY, 3, paint);
879             paint.setColor(0xFF00FF00);
880             canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
881         }
882 
883         mDrawPending = false;
884         mDirtyRect.setEmpty();
885     }
886 
isPunctuation(String label)887     private boolean isPunctuation(String label) {
888         return PUNCTUATION_PATTERN.matcher(label).matches();
889     }
890 
getKeyIndices(int x, int y, int[] allKeys)891     private int getKeyIndices(int x, int y, int[] allKeys) {
892         final Key[] keys = mKeys;
893         int primaryIndex = NOT_A_KEY;
894         int closestKey = NOT_A_KEY;
895         int closestKeyDist = mProximityThreshold + 1;
896         Arrays.fill(mDistances, Integer.MAX_VALUE);
897         int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
898         final int keyCount = nearestKeyIndices.length;
899         for (int i = 0; i < keyCount; i++) {
900             final Key key = keys[nearestKeyIndices[i]];
901             int dist = 0;
902             boolean isInside = key.isInside(x,y);
903             if (isInside) {
904                 primaryIndex = nearestKeyIndices[i];
905             }
906 
907             if (((mProximityCorrectOn
908                     && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
909                     || isInside)
910                     && key.codes[0] > 32) {
911                 // Find insertion point
912                 final int nCodes = key.codes.length;
913                 if (dist < closestKeyDist) {
914                     closestKeyDist = dist;
915                     closestKey = nearestKeyIndices[i];
916                 }
917 
918                 if (allKeys == null) continue;
919 
920                 for (int j = 0; j < mDistances.length; j++) {
921                     if (mDistances[j] > dist) {
922                         // Make space for nCodes codes
923                         System.arraycopy(mDistances, j, mDistances, j + nCodes,
924                                 mDistances.length - j - nCodes);
925                         System.arraycopy(allKeys, j, allKeys, j + nCodes,
926                                 allKeys.length - j - nCodes);
927                         for (int c = 0; c < nCodes; c++) {
928                             allKeys[j + c] = key.codes[c];
929                             mDistances[j + c] = dist;
930                         }
931                         break;
932                     }
933                 }
934             }
935         }
936         if (primaryIndex == NOT_A_KEY) {
937             primaryIndex = closestKey;
938         }
939         return primaryIndex;
940     }
941 
detectAndSendKey(int index, int x, int y, long eventTime)942     private void detectAndSendKey(int index, int x, int y, long eventTime) {
943         if (index != NOT_A_KEY && index < mKeys.length) {
944             final Key key = mKeys[index];
945             if (key.text != null) {
946                 mKeyboardActionListener.onText(key.text);
947                 mKeyboardActionListener.onRelease(NOT_A_KEY);
948             } else {
949                 int code = key.codes[0];
950                 //TextEntryState.keyPressedAt(key, x, y);
951                 int[] codes = new int[MAX_NEARBY_KEYS];
952                 Arrays.fill(codes, NOT_A_KEY);
953                 getKeyIndices(x, y, codes);
954                 // Multi-tap
955                 if (mInMultiTap) {
956                     if (mTapCount != -1) {
957                         mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
958                     } else {
959                         mTapCount = 0;
960                     }
961                     code = key.codes[mTapCount];
962                 }
963                 mKeyboardActionListener.onKey(code, codes);
964                 mKeyboardActionListener.onRelease(code);
965             }
966             mLastSentIndex = index;
967             mLastTapTime = eventTime;
968         }
969     }
970 
971     /**
972      * Handle multi-tap keys by producing the key label for the current multi-tap state.
973      */
getPreviewText(Key key)974     private CharSequence getPreviewText(Key key) {
975         if (mInMultiTap) {
976             // Multi-tap
977             mPreviewLabel.setLength(0);
978             mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
979             return adjustCase(mPreviewLabel);
980         } else {
981             return adjustCase(key.label);
982         }
983     }
984 
showPreview(int keyIndex)985     private void showPreview(int keyIndex) {
986         int oldKeyIndex = mCurrentKeyIndex;
987         final PopupWindow previewPopup = mPreviewPopup;
988 
989         mCurrentKeyIndex = keyIndex;
990         // Release the old key and press the new key
991         final Key[] keys = mKeys;
992         if (oldKeyIndex != mCurrentKeyIndex) {
993             if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
994                 Key oldKey = keys[oldKeyIndex];
995                 // Keyboard.Key.onReleased(boolean) should be called here, but due to b/21446448,
996                 // onReleased doesn't check the input param and it always toggles the on state.
997                 // Therefore we need to just set the pressed state to false here.
998                 oldKey.pressed = false;
999                 invalidateKey(oldKeyIndex);
1000             }
1001             if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
1002                 Key newKey = keys[mCurrentKeyIndex];
1003                 newKey.onPressed();
1004                 invalidateKey(mCurrentKeyIndex);
1005             }
1006         }
1007         // If key changed and preview is on ...
1008         if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
1009             mHandler.removeMessages(MSG_SHOW_PREVIEW);
1010             if (previewPopup.isShowing()) {
1011                 if (keyIndex == NOT_A_KEY) {
1012                     mHandler.sendMessageDelayed(mHandler
1013                                     .obtainMessage(MSG_REMOVE_PREVIEW),
1014                             DELAY_AFTER_PREVIEW);
1015                 }
1016             }
1017             if (keyIndex != NOT_A_KEY) {
1018                 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
1019                     // Show right away, if it's already visible and finger is moving around
1020                     showKey(keyIndex);
1021                 } else {
1022                     mHandler.sendMessageDelayed(
1023                             mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
1024                             DELAY_BEFORE_PREVIEW);
1025                 }
1026             }
1027         }
1028     }
1029 
showKey(final int keyIndex)1030     private void showKey(final int keyIndex) {
1031         final PopupWindow previewPopup = mPreviewPopup;
1032         final Key[] keys = mKeys;
1033         if (keyIndex < 0 || keyIndex >= mKeys.length) return;
1034         Key key = keys[keyIndex];
1035         if (key.icon != null) {
1036             mPreviewText.setCompoundDrawables(null, null, null,
1037                     key.iconPreview != null ? key.iconPreview : key.icon);
1038             mPreviewText.setText(null);
1039         } else {
1040             mPreviewText.setCompoundDrawables(null, null, null, null);
1041             mPreviewText.setText(getPreviewText(key));
1042             if (key.label.length() > 1 && key.codes.length < 2) {
1043                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
1044                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
1045             } else {
1046                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
1047                 mPreviewText.setTypeface(Typeface.DEFAULT);
1048             }
1049         }
1050         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1051                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
1052         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
1053                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
1054         final int popupHeight = mPreviewHeight;
1055         LayoutParams lp = mPreviewText.getLayoutParams();
1056         if (lp != null) {
1057             lp.width = popupWidth;
1058             lp.height = popupHeight;
1059         }
1060         if (!mPreviewCentered) {
1061             mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + getPaddingLeft();
1062             mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
1063         } else {
1064             // TODO: Fix this if centering is brought back
1065             mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
1066             mPopupPreviewY = - mPreviewText.getMeasuredHeight();
1067         }
1068         mHandler.removeMessages(MSG_REMOVE_PREVIEW);
1069         getLocationInWindow(mCoordinates);
1070         mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
1071         mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
1072 
1073         // Set the preview background state
1074         /*
1075         mPreviewText.getBackground().setState(
1076                  key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
1077         */
1078         mPopupPreviewX += mCoordinates[0];
1079         mPopupPreviewY += mCoordinates[1];
1080 
1081         // If the popup cannot be shown above the key, put it on the side
1082         getLocationOnScreen(mCoordinates);
1083         if (mPopupPreviewY + mCoordinates[1] < 0) {
1084             // If the key you're pressing is on the left side of the keyboard, show the popup on
1085             // the right, offset by enough to see at least one key to the left/right.
1086             if (key.x + key.width <= getWidth() / 2) {
1087                 mPopupPreviewX += (int) (key.width * 2.5);
1088             } else {
1089                 mPopupPreviewX -= (int) (key.width * 2.5);
1090             }
1091             mPopupPreviewY += popupHeight;
1092         }
1093 
1094         if (previewPopup.isShowing()) {
1095             previewPopup.update(mPopupPreviewX, mPopupPreviewY,
1096                     popupWidth, popupHeight);
1097         } else {
1098             previewPopup.setWidth(popupWidth);
1099             previewPopup.setHeight(popupHeight);
1100             previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
1101                     mPopupPreviewX, mPopupPreviewY);
1102         }
1103         mPreviewText.setVisibility(VISIBLE);
1104     }
1105 
1106     /**
1107      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1108      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1109      * draws the cached buffer.
1110      * @see #invalidateKey(int)
1111      */
invalidateAllKeys()1112     public void invalidateAllKeys() {
1113         mDirtyRect.union(0, 0, getWidth(), getHeight());
1114         mDrawPending = true;
1115         invalidate();
1116     }
1117 
1118     /**
1119      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1120      * one key is changing it's content. Any changes that affect the position or size of the key
1121      * may not be honored.
1122      * @param keyIndex the index of the key in the attached {@link android.inputmethodservice.Keyboard}.
1123      * @see #invalidateAllKeys
1124      */
invalidateKey(int keyIndex)1125     public void invalidateKey(int keyIndex) {
1126         if (mKeys == null) return;
1127         if (keyIndex < 0 || keyIndex >= mKeys.length) {
1128             return;
1129         }
1130         final Key key = mKeys[keyIndex];
1131         mInvalidatedKey = key;
1132         mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1133                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1134         onBufferDraw();
1135         invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1136                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1137     }
1138 
openPopupIfRequired(MotionEvent unused)1139     private void openPopupIfRequired(MotionEvent unused) {
1140         // Check if we have a popup keyboard first.
1141         if (mPopupKeyboardView == null || mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
1142             return;
1143         }
1144 
1145         Key popupKey = mKeys[mCurrentKey];
1146         boolean result = onLongPress(popupKey);
1147         if (result) {
1148             mAbortKey = true;
1149             showPreview(NOT_A_KEY);
1150         }
1151     }
1152 
1153     /**
1154      * Called when a key is long pressed. By default this will open any popup keyboard associated
1155      * with this key through the attributes popupLayout and popupCharacters.
1156      *
1157      * @param popupKey the key that was long pressed
1158      * @return true if the long press is handled, false otherwise. Subclasses should call the
1159      * method on the base class if the subclass doesn't wish to handle the call.
1160      */
onLongPress(Key popupKey)1161     protected boolean onLongPress(Key popupKey) {
1162         int popupKeyboardId = popupKey.popupResId;
1163 
1164         if (popupKeyboardId != 0) {
1165             Keyboard keyboard;
1166             if (popupKey.popupCharacters != null) {
1167                 keyboard = new Keyboard(getContext(), popupKeyboardId,
1168                         popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
1169             } else {
1170                 keyboard = new Keyboard(getContext(), popupKeyboardId);
1171             }
1172             mPopupKeyboardView.setKeyboard(keyboard, mLocale);
1173             mPopupKeyboardView.setVisibility(VISIBLE);
1174             mPopupKeyboardView.setShifted(isShifted());
1175             mPopupKeyboardView.mAnimateInAnimator.start();
1176             mPopupKeyboardView.mLastSentIndex = NOT_A_KEY;
1177             mScrimAlphaAnimator.setStartDelay(0);
1178             mScrimAlphaAnimator.start();
1179             mPopupKeyboardView.invalidate();
1180             mPopupKeyboardOnScreen = true;
1181             invalidateAllKeys();
1182             return true;
1183         }
1184         return false;
1185     }
1186 
1187     @Override
onHoverEvent(MotionEvent event)1188     public boolean onHoverEvent(MotionEvent event) {
1189         if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
1190             final int action = event.getAction();
1191             switch (action) {
1192                 case MotionEvent.ACTION_HOVER_ENTER: {
1193                     event.setAction(MotionEvent.ACTION_DOWN);
1194                 } break;
1195                 case MotionEvent.ACTION_HOVER_MOVE: {
1196                     event.setAction(MotionEvent.ACTION_MOVE);
1197                 } break;
1198                 case MotionEvent.ACTION_HOVER_EXIT: {
1199                     event.setAction(MotionEvent.ACTION_UP);
1200                 } break;
1201             }
1202             return onTouchEvent(event);
1203         }
1204         return true;
1205     }
1206 
1207     @SuppressLint("ClickableViewAccessibility")
1208     @Override
onTouchEvent(MotionEvent me)1209     public boolean onTouchEvent(MotionEvent me) {
1210         // Don't process touches while animating.
1211         if (mAnimateInValue != MAX_ANIMATION_VALUE || mAnimateOutValue != MIN_ANIMATION_VALUE) {
1212             return false;
1213         }
1214 
1215         // Convert multi-pointer up/down events to single up/down events to
1216         // deal with the typical multi-pointer behavior of two-thumb typing
1217         final int pointerCount = me.getPointerCount();
1218         final int action = me.getAction();
1219         boolean result = false;
1220         final long now = me.getEventTime();
1221 
1222         if (pointerCount != mOldPointerCount) {
1223             if (pointerCount == 1) {
1224                 // Send a down event for the latest pointer
1225                 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1226                         me.getX(), me.getY(), me.getMetaState());
1227                 result = onModifiedTouchEvent(down, false);
1228                 down.recycle();
1229                 // If it's an up action, then deliver the up as well.
1230                 if (action == MotionEvent.ACTION_UP) {
1231                     result = onModifiedTouchEvent(me, true);
1232                 }
1233             } else {
1234                 // Send an up event for the last pointer
1235                 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1236                         mOldPointerX, mOldPointerY, me.getMetaState());
1237                 result = onModifiedTouchEvent(up, true);
1238                 up.recycle();
1239             }
1240         } else {
1241             if (pointerCount == 1) {
1242                 result = onModifiedTouchEvent(me, false);
1243                 mOldPointerX = me.getX();
1244                 mOldPointerY = me.getY();
1245             } else {
1246                 // Don't do anything when 2 pointers are down and moving.
1247                 result = true;
1248             }
1249         }
1250         mOldPointerCount = pointerCount;
1251 
1252         return result;
1253     }
1254 
onModifiedTouchEvent(MotionEvent me, boolean possiblePoly)1255     private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
1256         int touchX = (int) me.getX() - getPaddingLeft();
1257         int touchY = (int) me.getY() - getPaddingTop();
1258         if (touchY >= -mVerticalCorrection)
1259             touchY += mVerticalCorrection;
1260         final int action = me.getAction();
1261         final long eventTime = me.getEventTime();
1262         int keyIndex = getKeyIndices(touchX, touchY, null);
1263         mPossiblePoly = possiblePoly;
1264 
1265         // Track the last few movements to look for spurious swipes.
1266         if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
1267         mSwipeTracker.addMovement(me);
1268 
1269         // Ignore all motion events until a DOWN.
1270         if (mAbortKey
1271                 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
1272             return true;
1273         }
1274 
1275         if (mGestureDetector.onTouchEvent(me)) {
1276             showPreview(NOT_A_KEY);
1277             mHandler.removeMessages(MSG_REPEAT);
1278             mHandler.removeMessages(MSG_LONGPRESS);
1279             return true;
1280         }
1281 
1282         if (mPopupKeyboardOnScreen) {
1283             dismissPopupKeyboard();
1284             return true;
1285         }
1286 
1287         switch (action) {
1288             case MotionEvent.ACTION_DOWN:
1289                 mAbortKey = false;
1290                 mStartX = touchX;
1291                 mStartY = touchY;
1292                 mLastCodeX = touchX;
1293                 mLastCodeY = touchY;
1294                 mLastKeyTime = 0;
1295                 mCurrentKeyTime = 0;
1296                 mLastKey = NOT_A_KEY;
1297                 mCurrentKey = keyIndex;
1298                 mDownKey = keyIndex;
1299                 mDownTime = me.getEventTime();
1300                 mLastMoveTime = mDownTime;
1301                 checkMultiTap(eventTime, keyIndex);
1302                 if (keyIndex != NOT_A_KEY) {
1303                     mKeyboardActionListener.onPress(mKeys[keyIndex].codes[0]);
1304                 }
1305                 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1306                     mRepeatKeyIndex = mCurrentKey;
1307                     Message msg = mHandler.obtainMessage(MSG_REPEAT);
1308                     mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1309                     repeatKey();
1310                     // Delivering the key could have caused an abort
1311                     if (mAbortKey) {
1312                         mRepeatKeyIndex = NOT_A_KEY;
1313                         break;
1314                     }
1315                 }
1316                 if (mCurrentKey != NOT_A_KEY) {
1317                     Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1318                     mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1319                 }
1320                 showPreview(keyIndex);
1321                 break;
1322 
1323             case MotionEvent.ACTION_MOVE:
1324                 boolean continueLongPress = false;
1325                 if (keyIndex != NOT_A_KEY) {
1326                     if (mCurrentKey == NOT_A_KEY) {
1327                         mCurrentKey = keyIndex;
1328                         mCurrentKeyTime = eventTime - mDownTime;
1329                     } else {
1330                         if (keyIndex == mCurrentKey) {
1331                             mCurrentKeyTime += eventTime - mLastMoveTime;
1332                             continueLongPress = true;
1333                         } else if (mRepeatKeyIndex == NOT_A_KEY) {
1334                             resetMultiTap();
1335                             mLastKey = mCurrentKey;
1336                             mLastCodeX = mLastX;
1337                             mLastCodeY = mLastY;
1338                             mLastKeyTime =
1339                                     mCurrentKeyTime + eventTime - mLastMoveTime;
1340                             mCurrentKey = keyIndex;
1341                             mCurrentKeyTime = 0;
1342                         }
1343                     }
1344                 }
1345                 if (!continueLongPress) {
1346                     // Cancel old longpress
1347                     mHandler.removeMessages(MSG_LONGPRESS);
1348                     // Start new longpress if key has changed
1349                     if (keyIndex != NOT_A_KEY) {
1350                         Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1351                         mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1352                     }
1353                 }
1354                 showPreview(mCurrentKey);
1355                 mLastMoveTime = eventTime;
1356                 break;
1357 
1358             case MotionEvent.ACTION_UP:
1359                 removeMessages();
1360                 if (keyIndex == mCurrentKey) {
1361                     mCurrentKeyTime += eventTime - mLastMoveTime;
1362                 } else {
1363                     resetMultiTap();
1364                     mLastKey = mCurrentKey;
1365                     mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1366                     mCurrentKey = keyIndex;
1367                     mCurrentKeyTime = 0;
1368                 }
1369                 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
1370                         && mLastKey != NOT_A_KEY) {
1371                     mCurrentKey = mLastKey;
1372                     touchX = mLastCodeX;
1373                     touchY = mLastCodeY;
1374                 }
1375                 showPreview(NOT_A_KEY);
1376                 Arrays.fill(mKeyIndices, NOT_A_KEY);
1377                 // If we're not on a repeating key (which sends on a DOWN event)
1378                 if (mRepeatKeyIndex == NOT_A_KEY && !mPopupKeyboardOnScreen && !mAbortKey) {
1379                     detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
1380                 }
1381                 invalidateKey(keyIndex);
1382                 mRepeatKeyIndex = NOT_A_KEY;
1383                 break;
1384             case MotionEvent.ACTION_CANCEL:
1385                 removeMessages();
1386                 dismissPopupKeyboard();
1387                 mAbortKey = true;
1388                 showPreview(NOT_A_KEY);
1389                 invalidateKey(mCurrentKey);
1390                 break;
1391         }
1392         mLastX = touchX;
1393         mLastY = touchY;
1394         return true;
1395     }
1396 
repeatKey()1397     private boolean repeatKey() {
1398         Key key = mKeys[mRepeatKeyIndex];
1399         detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
1400         return true;
1401     }
1402 
swipeRight()1403     protected void swipeRight() {
1404         mKeyboardActionListener.swipeRight();
1405     }
1406 
swipeLeft()1407     protected void swipeLeft() {
1408         mKeyboardActionListener.swipeLeft();
1409     }
1410 
swipeUp()1411     protected void swipeUp() {
1412         mKeyboardActionListener.swipeUp();
1413     }
1414 
swipeDown()1415     protected void swipeDown() {
1416         mKeyboardActionListener.swipeDown();
1417     }
1418 
closing()1419     public void closing() {
1420         if (mPreviewPopup.isShowing()) {
1421             mPreviewPopup.dismiss();
1422         }
1423         removeMessages();
1424 
1425         dismissPopupKeyboard();
1426         mBuffer = null;
1427         mCanvas = null;
1428         mMiniKeyboardCache.clear();
1429     }
1430 
removeMessages()1431     private void removeMessages() {
1432         mHandler.removeMessages(MSG_REPEAT);
1433         mHandler.removeMessages(MSG_LONGPRESS);
1434         mHandler.removeMessages(MSG_SHOW_PREVIEW);
1435     }
1436 
1437     @Override
onDetachedFromWindow()1438     public void onDetachedFromWindow() {
1439         super.onDetachedFromWindow();
1440         closing();
1441     }
1442 
dismissPopupKeyboard()1443     public void dismissPopupKeyboard() {
1444         mPopupKeyboardOnScreen = false;
1445         if (mPopupKeyboardView != null && mPopupKeyboardView.getVisibility() == View.VISIBLE) {
1446             if (mPopupKeyboardView.mAnimateInValue == MAX_ANIMATION_VALUE) {
1447                 mPopupKeyboardView.mAnimateOutAnimator.start();
1448                 mScrimAlphaAnimator.setStartDelay(ANIMATE_SCRIM_DELAY_MS);
1449                 mScrimAlphaAnimator.reverse();
1450             } else {
1451                 mPopupKeyboardView.mAnimateInAnimator.reverse();
1452                 mScrimAlphaAnimator.setStartDelay(0);
1453                 mScrimAlphaAnimator.reverse();
1454             }
1455         }
1456 
1457         invalidateAllKeys();
1458     }
1459 
setPopupKeyboardView(KeyboardView popupKeyboardView)1460     public void setPopupKeyboardView(KeyboardView popupKeyboardView) {
1461         mPopupKeyboardView = popupKeyboardView;
1462         mPopupKeyboardView.mBackgroundColor = getResources().getColor(R.color.car_teal_700);
1463     }
1464 
resetMultiTap()1465     private void resetMultiTap() {
1466         mLastSentIndex = NOT_A_KEY;
1467         mTapCount = 0;
1468         mLastTapTime = -1;
1469         mInMultiTap = false;
1470     }
1471 
checkMultiTap(long eventTime, int keyIndex)1472     private void checkMultiTap(long eventTime, int keyIndex) {
1473         if (keyIndex == NOT_A_KEY) return;
1474         Key key = mKeys[keyIndex];
1475         if (key.codes.length > 1) {
1476             mInMultiTap = true;
1477             if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1478                     && keyIndex == mLastSentIndex) {
1479                 mTapCount = (mTapCount + 1) % key.codes.length;
1480                 return;
1481             } else {
1482                 mTapCount = -1;
1483                 return;
1484             }
1485         }
1486         if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1487             resetMultiTap();
1488         }
1489     }
1490 
1491     @Override
onGenericMotionEvent(MotionEvent event)1492     public boolean onGenericMotionEvent(MotionEvent event) {
1493         // Close the touch keyboard when the user scrolls.
1494         if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
1495             mKeyboardActionListener.stopInput();
1496             return true;
1497         }
1498         return false;
1499     }
1500 
1501     private static class SwipeTracker {
1502 
1503         static final int NUM_PAST = 4;
1504         static final int LONGEST_PAST_TIME = 200;
1505 
1506         final float mPastX[] = new float[NUM_PAST];
1507         final float mPastY[] = new float[NUM_PAST];
1508         final long mPastTime[] = new long[NUM_PAST];
1509 
1510         float mYVelocity;
1511         float mXVelocity;
1512 
clear()1513         public void clear() {
1514             mPastTime[0] = 0;
1515         }
1516 
addMovement(MotionEvent ev)1517         public void addMovement(MotionEvent ev) {
1518             long time = ev.getEventTime();
1519             final int N = ev.getHistorySize();
1520             for (int i=0; i<N; i++) {
1521                 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
1522                         ev.getHistoricalEventTime(i));
1523             }
1524             addPoint(ev.getX(), ev.getY(), time);
1525         }
1526 
addPoint(float x, float y, long time)1527         private void addPoint(float x, float y, long time) {
1528             int drop = -1;
1529             int i;
1530             final long[] pastTime = mPastTime;
1531             for (i=0; i<NUM_PAST; i++) {
1532                 if (pastTime[i] == 0) {
1533                     break;
1534                 } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
1535                     drop = i;
1536                 }
1537             }
1538             if (i == NUM_PAST && drop < 0) {
1539                 drop = 0;
1540             }
1541             if (drop == i) drop--;
1542             final float[] pastX = mPastX;
1543             final float[] pastY = mPastY;
1544             if (drop >= 0) {
1545                 final int start = drop+1;
1546                 final int count = NUM_PAST-drop-1;
1547                 System.arraycopy(pastX, start, pastX, 0, count);
1548                 System.arraycopy(pastY, start, pastY, 0, count);
1549                 System.arraycopy(pastTime, start, pastTime, 0, count);
1550                 i -= (drop+1);
1551             }
1552             pastX[i] = x;
1553             pastY[i] = y;
1554             pastTime[i] = time;
1555             i++;
1556             if (i < NUM_PAST) {
1557                 pastTime[i] = 0;
1558             }
1559         }
1560 
computeCurrentVelocity(int units)1561         public void computeCurrentVelocity(int units) {
1562             computeCurrentVelocity(units, Float.MAX_VALUE);
1563         }
1564 
computeCurrentVelocity(int units, float maxVelocity)1565         public void computeCurrentVelocity(int units, float maxVelocity) {
1566             final float[] pastX = mPastX;
1567             final float[] pastY = mPastY;
1568             final long[] pastTime = mPastTime;
1569 
1570             final float oldestX = pastX[0];
1571             final float oldestY = pastY[0];
1572             final long oldestTime = pastTime[0];
1573             float accumX = 0;
1574             float accumY = 0;
1575             int N=0;
1576             while (N < NUM_PAST) {
1577                 if (pastTime[N] == 0) {
1578                     break;
1579                 }
1580                 N++;
1581             }
1582 
1583             for (int i=1; i < N; i++) {
1584                 final int dur = (int)(pastTime[i] - oldestTime);
1585                 if (dur == 0) continue;
1586                 float dist = pastX[i] - oldestX;
1587                 float vel = (dist/dur) * units;   // pixels/frame.
1588                 if (accumX == 0) accumX = vel;
1589                 else accumX = (accumX + vel) * .5f;
1590 
1591                 dist = pastY[i] - oldestY;
1592                 vel = (dist/dur) * units;   // pixels/frame.
1593                 if (accumY == 0) accumY = vel;
1594                 else accumY = (accumY + vel) * .5f;
1595             }
1596             mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
1597                     : Math.min(accumX, maxVelocity);
1598             mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
1599                     : Math.min(accumY, maxVelocity);
1600         }
1601 
1602         public float getXVelocity() {
1603             return mXVelocity;
1604         }
1605 
1606         public float getYVelocity() {
1607             return mYVelocity;
1608         }
1609     }
1610 
1611     private static class KeyboardHander extends Handler {
1612         private final WeakReference<KeyboardView> mKeyboardView;
1613 
1614         public KeyboardHander(KeyboardView keyboardView) {
1615             mKeyboardView = new WeakReference<KeyboardView>(keyboardView);
1616         }
1617 
1618         @Override
1619         public void handleMessage(Message msg) {
1620             KeyboardView keyboardView = mKeyboardView.get();
1621             if (keyboardView != null) {
1622                 switch (msg.what) {
1623                     case MSG_SHOW_PREVIEW:
1624                         keyboardView.showKey(msg.arg1);
1625                         break;
1626                     case MSG_REMOVE_PREVIEW:
1627                         keyboardView.mPreviewText.setVisibility(INVISIBLE);
1628                         break;
1629                     case MSG_REPEAT:
1630                         if (keyboardView.repeatKey()) {
1631                             Message repeat = Message.obtain(this, MSG_REPEAT);
1632                             sendMessageDelayed(repeat, REPEAT_INTERVAL);
1633                         }
1634                         break;
1635                     case MSG_LONGPRESS:
1636                         keyboardView.openPopupIfRequired((MotionEvent) msg.obj);
1637                         break;
1638                 }
1639             }
1640         }
1641     }
1642 }
1643