• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Bitmap;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Align;
27 import android.graphics.PorterDuff;
28 import android.graphics.Rect;
29 import android.graphics.Region.Op;
30 import android.graphics.Typeface;
31 import android.graphics.drawable.Drawable;
32 import android.inputmethodservice.Keyboard;
33 import android.inputmethodservice.Keyboard.Key;
34 import android.os.Handler;
35 import android.os.Message;
36 import android.os.SystemClock;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.view.GestureDetector;
41 import android.view.Gravity;
42 import android.view.LayoutInflater;
43 import android.view.MotionEvent;
44 import android.view.View;
45 import android.view.ViewGroup.LayoutParams;
46 import android.widget.PopupWindow;
47 import android.widget.TextView;
48 
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.LinkedList;
52 import java.util.List;
53 import java.util.Locale;
54 import java.util.WeakHashMap;
55 
56 /**
57  * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and
58  * detecting key presses and touch movements.
59  *
60  * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class.
61  *
62  * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground
63  * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout
64  * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset
65  * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize
66  * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize
67  * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor
68  * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection
69  * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout
70  */
71 public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy {
72     private static final String TAG = "LatinKeyboardBaseView";
73     private static final boolean DEBUG = false;
74 
75     public static final int NOT_A_TOUCH_COORDINATE = -1;
76 
77     public interface OnKeyboardActionListener {
78 
79         /**
80          * Called when the user presses a key. This is sent before the
81          * {@link #onKey} is called. For keys that repeat, this is only
82          * called once.
83          *
84          * @param primaryCode
85          *            the unicode of the key being pressed. If the touch is
86          *            not on a valid key, the value will be zero.
87          */
onPress(int primaryCode)88         void onPress(int primaryCode);
89 
90         /**
91          * Called when the user releases a key. This is sent after the
92          * {@link #onKey} is called. For keys that repeat, this is only
93          * called once.
94          *
95          * @param primaryCode
96          *            the code of the key that was released
97          */
onRelease(int primaryCode)98         void onRelease(int primaryCode);
99 
100         /**
101          * Send a key press to the listener.
102          *
103          * @param primaryCode
104          *            this is the key that was pressed
105          * @param keyCodes
106          *            the codes for all the possible alternative keys with
107          *            the primary code being the first. If the primary key
108          *            code is a single character such as an alphabet or
109          *            number or symbol, the alternatives will include other
110          *            characters that may be on the same key or adjacent
111          *            keys. These codes are useful to correct for
112          *            accidental presses of a key adjacent to the intended
113          *            key.
114          * @param x
115          *            x-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
116          *            the value should be NOT_A_TOUCH_COORDINATE.
117          * @param y
118          *            y-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
119          *            the value should be NOT_A_TOUCH_COORDINATE.
120          */
onKey(int primaryCode, int[] keyCodes, int x, int y)121         void onKey(int primaryCode, int[] keyCodes, int x, int y);
122 
123         /**
124          * Sends a sequence of characters to the listener.
125          *
126          * @param text
127          *            the sequence of characters to be displayed.
128          */
onText(CharSequence text)129         void onText(CharSequence text);
130 
131         /**
132          * Called when user released a finger outside any key.
133          */
onCancel()134         void onCancel();
135 
136         /**
137          * Called when the user quickly moves the finger from right to
138          * left.
139          */
swipeLeft()140         void swipeLeft();
141 
142         /**
143          * Called when the user quickly moves the finger from left to
144          * right.
145          */
swipeRight()146         void swipeRight();
147 
148         /**
149          * Called when the user quickly moves the finger from up to down.
150          */
swipeDown()151         void swipeDown();
152 
153         /**
154          * Called when the user quickly moves the finger from down to up.
155          */
swipeUp()156         void swipeUp();
157     }
158 
159     // Timing constants
160     private final int mKeyRepeatInterval;
161 
162     // Miscellaneous constants
163     /* package */ static final int NOT_A_KEY = -1;
164     private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
165     private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1;
166 
167     // XML attribute
168     private int mKeyTextSize;
169     private int mKeyTextColor;
170     private Typeface mKeyTextStyle = Typeface.DEFAULT;
171     private int mLabelTextSize;
172     private int mSymbolColorScheme = 0;
173     private int mShadowColor;
174     private float mShadowRadius;
175     private Drawable mKeyBackground;
176     private float mBackgroundDimAmount;
177     private float mKeyHysteresisDistance;
178     private float mVerticalCorrection;
179     private int mPreviewOffset;
180     private int mPreviewHeight;
181     private int mPopupLayout;
182 
183     // Main keyboard
184     private Keyboard mKeyboard;
185     private Key[] mKeys;
186     // TODO this attribute should be gotten from Keyboard.
187     private int mKeyboardVerticalGap;
188 
189     // Key preview popup
190     private TextView mPreviewText;
191     private PopupWindow mPreviewPopup;
192     private int mPreviewTextSizeLarge;
193     private int[] mOffsetInWindow;
194     private int mOldPreviewKeyIndex = NOT_A_KEY;
195     private boolean mShowPreview = true;
196     private boolean mShowTouchPoints = true;
197     private int mPopupPreviewOffsetX;
198     private int mPopupPreviewOffsetY;
199     private int mWindowY;
200     private int mPopupPreviewDisplayedY;
201     private final int mDelayBeforePreview;
202     private final int mDelayAfterPreview;
203 
204     // Popup mini keyboard
205     private PopupWindow mMiniKeyboardPopup;
206     private LatinKeyboardBaseView mMiniKeyboard;
207     private View mMiniKeyboardParent;
208     private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>();
209     private int mMiniKeyboardOriginX;
210     private int mMiniKeyboardOriginY;
211     private long mMiniKeyboardPopupTime;
212     private int[] mWindowOffset;
213     private final float mMiniKeyboardSlideAllowance;
214     private int mMiniKeyboardTrackerId;
215 
216     /** Listener for {@link OnKeyboardActionListener}. */
217     private OnKeyboardActionListener mKeyboardActionListener;
218 
219     private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
220 
221     // TODO: Let the PointerTracker class manage this pointer queue
222     private final PointerQueue mPointerQueue = new PointerQueue();
223 
224     private final boolean mHasDistinctMultitouch;
225     private int mOldPointerCount = 1;
226 
227     protected KeyDetector mKeyDetector = new ProximityKeyDetector();
228 
229     // Swipe gesture detector
230     private GestureDetector mGestureDetector;
231     private final SwipeTracker mSwipeTracker = new SwipeTracker();
232     private final int mSwipeThreshold;
233     private final boolean mDisambiguateSwipe;
234 
235     // Drawing
236     /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
237     private boolean mDrawPending;
238     /** The dirty region in the keyboard bitmap */
239     private final Rect mDirtyRect = new Rect();
240     /** The keyboard bitmap for faster updates */
241     private Bitmap mBuffer;
242     /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
243     private boolean mKeyboardChanged;
244     private Key mInvalidatedKey;
245     /** The canvas for the above mutable keyboard bitmap */
246     private Canvas mCanvas;
247     private final Paint mPaint;
248     private final Rect mPadding;
249     private final Rect mClipRegion = new Rect(0, 0, 0, 0);
250     // This map caches key label text height in pixel as value and key label text size as map key.
251     private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
252     // Distance from horizontal center of the key, proportional to key label text height.
253     private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f;
254     private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H";
255 
256     private final UIHandler mHandler = new UIHandler();
257 
258     class UIHandler extends Handler {
259         private static final int MSG_POPUP_PREVIEW = 1;
260         private static final int MSG_DISMISS_PREVIEW = 2;
261         private static final int MSG_REPEAT_KEY = 3;
262         private static final int MSG_LONGPRESS_KEY = 4;
263 
264         private boolean mInKeyRepeat;
265 
266         @Override
handleMessage(Message msg)267         public void handleMessage(Message msg) {
268             switch (msg.what) {
269                 case MSG_POPUP_PREVIEW:
270                     showKey(msg.arg1, (PointerTracker)msg.obj);
271                     break;
272                 case MSG_DISMISS_PREVIEW:
273                     mPreviewPopup.dismiss();
274                     break;
275                 case MSG_REPEAT_KEY: {
276                     final PointerTracker tracker = (PointerTracker)msg.obj;
277                     tracker.repeatKey(msg.arg1);
278                     startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
279                     break;
280                 }
281                 case MSG_LONGPRESS_KEY: {
282                     final PointerTracker tracker = (PointerTracker)msg.obj;
283                     openPopupIfRequired(msg.arg1, tracker);
284                     break;
285                 }
286             }
287         }
288 
popupPreview(long delay, int keyIndex, PointerTracker tracker)289         public void popupPreview(long delay, int keyIndex, PointerTracker tracker) {
290             removeMessages(MSG_POPUP_PREVIEW);
291             if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
292                 // Show right away, if it's already visible and finger is moving around
293                 showKey(keyIndex, tracker);
294             } else {
295                 sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker),
296                         delay);
297             }
298         }
299 
cancelPopupPreview()300         public void cancelPopupPreview() {
301             removeMessages(MSG_POPUP_PREVIEW);
302         }
303 
dismissPreview(long delay)304         public void dismissPreview(long delay) {
305             if (mPreviewPopup.isShowing()) {
306                 sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay);
307             }
308         }
309 
cancelDismissPreview()310         public void cancelDismissPreview() {
311             removeMessages(MSG_DISMISS_PREVIEW);
312         }
313 
startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker)314         public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
315             mInKeyRepeat = true;
316             sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
317         }
318 
cancelKeyRepeatTimer()319         public void cancelKeyRepeatTimer() {
320             mInKeyRepeat = false;
321             removeMessages(MSG_REPEAT_KEY);
322         }
323 
isInKeyRepeat()324         public boolean isInKeyRepeat() {
325             return mInKeyRepeat;
326         }
327 
startLongPressTimer(long delay, int keyIndex, PointerTracker tracker)328         public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
329             removeMessages(MSG_LONGPRESS_KEY);
330             sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
331         }
332 
cancelLongPressTimer()333         public void cancelLongPressTimer() {
334             removeMessages(MSG_LONGPRESS_KEY);
335         }
336 
cancelKeyTimers()337         public void cancelKeyTimers() {
338             cancelKeyRepeatTimer();
339             cancelLongPressTimer();
340         }
341 
cancelAllMessages()342         public void cancelAllMessages() {
343             cancelKeyTimers();
344             cancelPopupPreview();
345             cancelDismissPreview();
346         }
347     }
348 
349     static class PointerQueue {
350         private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
351 
add(PointerTracker tracker)352         public void add(PointerTracker tracker) {
353             mQueue.add(tracker);
354         }
355 
lastIndexOf(PointerTracker tracker)356         public int lastIndexOf(PointerTracker tracker) {
357             LinkedList<PointerTracker> queue = mQueue;
358             for (int index = queue.size() - 1; index >= 0; index--) {
359                 PointerTracker t = queue.get(index);
360                 if (t == tracker)
361                     return index;
362             }
363             return -1;
364         }
365 
releaseAllPointersOlderThan(PointerTracker tracker, long eventTime)366         public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) {
367             LinkedList<PointerTracker> queue = mQueue;
368             int oldestPos = 0;
369             for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) {
370                 if (t.isModifier()) {
371                     oldestPos++;
372                 } else {
373                     t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
374                     t.setAlreadyProcessed();
375                     queue.remove(oldestPos);
376                 }
377             }
378         }
379 
releaseAllPointersExcept(PointerTracker tracker, long eventTime)380         public void releaseAllPointersExcept(PointerTracker tracker, long eventTime) {
381             for (PointerTracker t : mQueue) {
382                 if (t == tracker)
383                     continue;
384                 t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
385                 t.setAlreadyProcessed();
386             }
387             mQueue.clear();
388             if (tracker != null)
389                 mQueue.add(tracker);
390         }
391 
remove(PointerTracker tracker)392         public void remove(PointerTracker tracker) {
393             mQueue.remove(tracker);
394         }
395 
isInSlidingKeyInput()396         public boolean isInSlidingKeyInput() {
397             for (final PointerTracker tracker : mQueue) {
398                 if (tracker.isInSlidingKeyInput())
399                     return true;
400             }
401             return false;
402         }
403     }
404 
LatinKeyboardBaseView(Context context, AttributeSet attrs)405     public LatinKeyboardBaseView(Context context, AttributeSet attrs) {
406         this(context, attrs, R.attr.keyboardViewStyle);
407     }
408 
LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle)409     public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) {
410         super(context, attrs, defStyle);
411 
412         TypedArray a = context.obtainStyledAttributes(
413                 attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView);
414         LayoutInflater inflate =
415                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
416         int previewLayout = 0;
417         int keyTextSize = 0;
418 
419         int n = a.getIndexCount();
420 
421         for (int i = 0; i < n; i++) {
422             int attr = a.getIndex(i);
423 
424             switch (attr) {
425             case R.styleable.LatinKeyboardBaseView_keyBackground:
426                 mKeyBackground = a.getDrawable(attr);
427                 break;
428             case R.styleable.LatinKeyboardBaseView_keyHysteresisDistance:
429                 mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0);
430                 break;
431             case R.styleable.LatinKeyboardBaseView_verticalCorrection:
432                 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
433                 break;
434             case R.styleable.LatinKeyboardBaseView_keyPreviewLayout:
435                 previewLayout = a.getResourceId(attr, 0);
436                 break;
437             case R.styleable.LatinKeyboardBaseView_keyPreviewOffset:
438                 mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
439                 break;
440             case R.styleable.LatinKeyboardBaseView_keyPreviewHeight:
441                 mPreviewHeight = a.getDimensionPixelSize(attr, 80);
442                 break;
443             case R.styleable.LatinKeyboardBaseView_keyTextSize:
444                 mKeyTextSize = a.getDimensionPixelSize(attr, 18);
445                 break;
446             case R.styleable.LatinKeyboardBaseView_keyTextColor:
447                 mKeyTextColor = a.getColor(attr, 0xFF000000);
448                 break;
449             case R.styleable.LatinKeyboardBaseView_labelTextSize:
450                 mLabelTextSize = a.getDimensionPixelSize(attr, 14);
451                 break;
452             case R.styleable.LatinKeyboardBaseView_popupLayout:
453                 mPopupLayout = a.getResourceId(attr, 0);
454                 break;
455             case R.styleable.LatinKeyboardBaseView_shadowColor:
456                 mShadowColor = a.getColor(attr, 0);
457                 break;
458             case R.styleable.LatinKeyboardBaseView_shadowRadius:
459                 mShadowRadius = a.getFloat(attr, 0f);
460                 break;
461             // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
462             case R.styleable.LatinKeyboardBaseView_backgroundDimAmount:
463                 mBackgroundDimAmount = a.getFloat(attr, 0.5f);
464                 break;
465             //case android.R.styleable.
466             case R.styleable.LatinKeyboardBaseView_keyTextStyle:
467                 int textStyle = a.getInt(attr, 0);
468                 switch (textStyle) {
469                     case 0:
470                         mKeyTextStyle = Typeface.DEFAULT;
471                         break;
472                     case 1:
473                         mKeyTextStyle = Typeface.DEFAULT_BOLD;
474                         break;
475                     default:
476                         mKeyTextStyle = Typeface.defaultFromStyle(textStyle);
477                         break;
478                 }
479                 break;
480             case R.styleable.LatinKeyboardBaseView_symbolColorScheme:
481                 mSymbolColorScheme = a.getInt(attr, 0);
482                 break;
483             }
484         }
485 
486         final Resources res = getResources();
487 
488         mPreviewPopup = new PopupWindow(context);
489         if (previewLayout != 0) {
490             mPreviewText = (TextView) inflate.inflate(previewLayout, null);
491             mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large);
492             mPreviewPopup.setContentView(mPreviewText);
493             mPreviewPopup.setBackgroundDrawable(null);
494         } else {
495             mShowPreview = false;
496         }
497         mPreviewPopup.setTouchable(false);
498         mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
499         mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
500         mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
501 
502         mMiniKeyboardParent = this;
503         mMiniKeyboardPopup = new PopupWindow(context);
504         mMiniKeyboardPopup.setBackgroundDrawable(null);
505         mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation);
506 
507         mPaint = new Paint();
508         mPaint.setAntiAlias(true);
509         mPaint.setTextSize(keyTextSize);
510         mPaint.setTextAlign(Align.CENTER);
511         mPaint.setAlpha(255);
512 
513         mPadding = new Rect(0, 0, 0, 0);
514         mKeyBackground.getPadding(mPadding);
515 
516         mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
517         // TODO: Refer frameworks/base/core/res/res/values/config.xml
518         mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
519         mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance);
520 
521         GestureDetector.SimpleOnGestureListener listener =
522                 new GestureDetector.SimpleOnGestureListener() {
523             @Override
524             public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
525                     float velocityY) {
526                 final float absX = Math.abs(velocityX);
527                 final float absY = Math.abs(velocityY);
528                 float deltaX = me2.getX() - me1.getX();
529                 float deltaY = me2.getY() - me1.getY();
530                 int travelX = getWidth() / 2; // Half the keyboard width
531                 int travelY = getHeight() / 2; // Half the keyboard height
532                 mSwipeTracker.computeCurrentVelocity(1000);
533                 final float endingVelocityX = mSwipeTracker.getXVelocity();
534                 final float endingVelocityY = mSwipeTracker.getYVelocity();
535                 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
536                     if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) {
537                         swipeRight();
538                         return true;
539                     }
540                 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
541                     if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) {
542                         swipeLeft();
543                         return true;
544                     }
545                 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
546                     if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) {
547                         swipeUp();
548                         return true;
549                     }
550                 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
551                     if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
552                         swipeDown();
553                         return true;
554                     }
555                 }
556                 return false;
557             }
558         };
559 
560         final boolean ignoreMultitouch = true;
561         mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
562         mGestureDetector.setIsLongpressEnabled(false);
563 
564         mHasDistinctMultitouch = context.getPackageManager()
565                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
566         mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
567     }
568 
setOnKeyboardActionListener(OnKeyboardActionListener listener)569     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
570         mKeyboardActionListener = listener;
571         for (PointerTracker tracker : mPointerTrackers) {
572             tracker.setOnKeyboardActionListener(listener);
573         }
574     }
575 
576     /**
577      * Returns the {@link OnKeyboardActionListener} object.
578      * @return the listener attached to this keyboard
579      */
getOnKeyboardActionListener()580     protected OnKeyboardActionListener getOnKeyboardActionListener() {
581         return mKeyboardActionListener;
582     }
583 
584     /**
585      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
586      * view will re-layout itself to accommodate the keyboard.
587      * @see Keyboard
588      * @see #getKeyboard()
589      * @param keyboard the keyboard to display in this view
590      */
setKeyboard(Keyboard keyboard)591     public void setKeyboard(Keyboard keyboard) {
592         if (mKeyboard != null) {
593             dismissKeyPreview();
594         }
595         // Remove any pending messages, except dismissing preview
596         mHandler.cancelKeyTimers();
597         mHandler.cancelPopupPreview();
598         mKeyboard = keyboard;
599         LatinImeLogger.onSetKeyboard(keyboard);
600         mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
601                 -getPaddingTop() + mVerticalCorrection);
602         mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap);
603         for (PointerTracker tracker : mPointerTrackers) {
604             tracker.setKeyboard(mKeys, mKeyHysteresisDistance);
605         }
606         requestLayout();
607         // Hint to reallocate the buffer if the size changed
608         mKeyboardChanged = true;
609         invalidateAllKeys();
610         computeProximityThreshold(keyboard);
611         mMiniKeyboardCache.clear();
612     }
613 
614     /**
615      * Returns the current keyboard being displayed by this view.
616      * @return the currently attached keyboard
617      * @see #setKeyboard(Keyboard)
618      */
getKeyboard()619     public Keyboard getKeyboard() {
620         return mKeyboard;
621     }
622 
623     /**
624      * Return whether the device has distinct multi-touch panel.
625      * @return true if the device has distinct multi-touch panel.
626      */
hasDistinctMultitouch()627     public boolean hasDistinctMultitouch() {
628         return mHasDistinctMultitouch;
629     }
630 
631     /**
632      * Sets the state of the shift key of the keyboard, if any.
633      * @param shifted whether or not to enable the state of the shift key
634      * @return true if the shift key state changed, false if there was no change
635      */
setShifted(boolean shifted)636     public boolean setShifted(boolean shifted) {
637         if (mKeyboard != null) {
638             if (mKeyboard.setShifted(shifted)) {
639                 // The whole keyboard probably needs to be redrawn
640                 invalidateAllKeys();
641                 return true;
642             }
643         }
644         return false;
645     }
646 
647     /**
648      * Returns the state of the shift key of the keyboard, if any.
649      * @return true if the shift is in a pressed state, false otherwise. If there is
650      * no shift key on the keyboard or there is no keyboard attached, it returns false.
651      */
isShifted()652     public boolean isShifted() {
653         if (mKeyboard != null) {
654             return mKeyboard.isShifted();
655         }
656         return false;
657     }
658 
659     /**
660      * Enables or disables the key feedback popup. This is a popup that shows a magnified
661      * version of the depressed key. By default the preview is enabled.
662      * @param previewEnabled whether or not to enable the key feedback popup
663      * @see #isPreviewEnabled()
664      */
setPreviewEnabled(boolean previewEnabled)665     public void setPreviewEnabled(boolean previewEnabled) {
666         mShowPreview = previewEnabled;
667     }
668 
669     /**
670      * Returns the enabled state of the key feedback popup.
671      * @return whether or not the key feedback popup is enabled
672      * @see #setPreviewEnabled(boolean)
673      */
isPreviewEnabled()674     public boolean isPreviewEnabled() {
675         return mShowPreview;
676     }
677 
getSymbolColorScheme()678     public int getSymbolColorScheme() {
679         return mSymbolColorScheme;
680     }
681 
setPopupParent(View v)682     public void setPopupParent(View v) {
683         mMiniKeyboardParent = v;
684     }
685 
setPopupOffset(int x, int y)686     public void setPopupOffset(int x, int y) {
687         mPopupPreviewOffsetX = x;
688         mPopupPreviewOffsetY = y;
689         mPreviewPopup.dismiss();
690     }
691 
692     /**
693      * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
694      * codes for adjacent keys.  When disabled, only the primary key code will be
695      * reported.
696      * @param enabled whether or not the proximity correction is enabled
697      */
setProximityCorrectionEnabled(boolean enabled)698     public void setProximityCorrectionEnabled(boolean enabled) {
699         mKeyDetector.setProximityCorrectionEnabled(enabled);
700     }
701 
702     /**
703      * Returns true if proximity correction is enabled.
704      */
isProximityCorrectionEnabled()705     public boolean isProximityCorrectionEnabled() {
706         return mKeyDetector.isProximityCorrectionEnabled();
707     }
708 
getKeyboardLocale()709     protected Locale getKeyboardLocale() {
710         if (mKeyboard instanceof LatinKeyboard) {
711             return ((LatinKeyboard)mKeyboard).getInputLocale();
712         } else {
713             return getContext().getResources().getConfiguration().locale;
714         }
715     }
716 
adjustCase(CharSequence label)717     protected CharSequence adjustCase(CharSequence label) {
718         if (mKeyboard.isShifted() && label != null && label.length() < 3
719                 && Character.isLowerCase(label.charAt(0))) {
720             return label.toString().toUpperCase(getKeyboardLocale());
721         }
722         return label;
723     }
724 
725     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)726     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
727         // Round up a little
728         if (mKeyboard == null) {
729             setMeasuredDimension(
730                     getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
731         } else {
732             int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
733             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
734                 width = MeasureSpec.getSize(widthMeasureSpec);
735             }
736             setMeasuredDimension(
737                     width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
738         }
739     }
740 
741     /**
742      * Compute the average distance between adjacent keys (horizontally and vertically)
743      * and square it to get the proximity threshold. We use a square here and in computing
744      * the touch distance from a key's center to avoid taking a square root.
745      * @param keyboard
746      */
computeProximityThreshold(Keyboard keyboard)747     private void computeProximityThreshold(Keyboard keyboard) {
748         if (keyboard == null) return;
749         final Key[] keys = mKeys;
750         if (keys == null) return;
751         int length = keys.length;
752         int dimensionSum = 0;
753         for (int i = 0; i < length; i++) {
754             Key key = keys[i];
755             dimensionSum += Math.min(key.width, key.height + mKeyboardVerticalGap) + key.gap;
756         }
757         if (dimensionSum < 0 || length == 0) return;
758         mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length));
759     }
760 
761     @Override
onSizeChanged(int w, int h, int oldw, int oldh)762     public void onSizeChanged(int w, int h, int oldw, int oldh) {
763         super.onSizeChanged(w, h, oldw, oldh);
764         // Release the buffer, if any and it will be reallocated on the next draw
765         mBuffer = null;
766     }
767 
768     @Override
onDraw(Canvas canvas)769     public void onDraw(Canvas canvas) {
770         super.onDraw(canvas);
771         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
772             onBufferDraw();
773         }
774         canvas.drawBitmap(mBuffer, 0, 0, null);
775     }
776 
onBufferDraw()777     private void onBufferDraw() {
778         if (mBuffer == null || mKeyboardChanged) {
779             if (mBuffer == null || mKeyboardChanged &&
780                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
781                 // Make sure our bitmap is at least 1x1
782                 final int width = Math.max(1, getWidth());
783                 final int height = Math.max(1, getHeight());
784                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
785                 mCanvas = new Canvas(mBuffer);
786             }
787             invalidateAllKeys();
788             mKeyboardChanged = false;
789         }
790         final Canvas canvas = mCanvas;
791         canvas.clipRect(mDirtyRect, Op.REPLACE);
792 
793         if (mKeyboard == null) return;
794 
795         final Paint paint = mPaint;
796         final Drawable keyBackground = mKeyBackground;
797         final Rect clipRegion = mClipRegion;
798         final Rect padding = mPadding;
799         final int kbdPaddingLeft = getPaddingLeft();
800         final int kbdPaddingTop = getPaddingTop();
801         final Key[] keys = mKeys;
802         final Key invalidKey = mInvalidatedKey;
803 
804         paint.setColor(mKeyTextColor);
805         boolean drawSingleKey = false;
806         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
807             // TODO we should use Rect.inset and Rect.contains here.
808             // Is clipRegion completely contained within the invalidated key?
809             if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
810                     invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
811                     invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
812                     invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
813                 drawSingleKey = true;
814             }
815         }
816         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
817         final int keyCount = keys.length;
818         for (int i = 0; i < keyCount; i++) {
819             final Key key = keys[i];
820             if (drawSingleKey && invalidKey != key) {
821                 continue;
822             }
823             int[] drawableState = key.getCurrentDrawableState();
824             keyBackground.setState(drawableState);
825 
826             // Switch the character to uppercase if shift is pressed
827             String label = key.label == null? null : adjustCase(key.label).toString();
828 
829             final Rect bounds = keyBackground.getBounds();
830             if (key.width != bounds.right || key.height != bounds.bottom) {
831                 keyBackground.setBounds(0, 0, key.width, key.height);
832             }
833             canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
834             keyBackground.draw(canvas);
835 
836             boolean shouldDrawIcon = true;
837             if (label != null) {
838                 // For characters, use large font. For labels like "Done", use small font.
839                 final int labelSize;
840                 if (label.length() > 1 && key.codes.length < 2) {
841                     labelSize = mLabelTextSize;
842                     paint.setTypeface(Typeface.DEFAULT_BOLD);
843                 } else {
844                     labelSize = mKeyTextSize;
845                     paint.setTypeface(mKeyTextStyle);
846                 }
847                 paint.setTextSize(labelSize);
848 
849                 Integer labelHeightValue = mTextHeightCache.get(labelSize);
850                 final int labelHeight;
851                 if (labelHeightValue != null) {
852                     labelHeight = labelHeightValue;
853                 } else {
854                     Rect textBounds = new Rect();
855                     paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds);
856                     labelHeight = textBounds.height();
857                     mTextHeightCache.put(labelSize, labelHeight);
858                 }
859 
860                 // Draw a drop shadow for the text
861                 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
862                 final int centerX = (key.width + padding.left - padding.right) / 2;
863                 final int centerY = (key.height + padding.top - padding.bottom) / 2;
864                 final float baseline = centerY
865                         + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR;
866                 canvas.drawText(label, centerX, baseline, paint);
867                 // Turn off drop shadow
868                 paint.setShadowLayer(0, 0, 0, 0);
869 
870                 // Usually don't draw icon if label is not null, but we draw icon for the number
871                 // hint and popup hint.
872                 shouldDrawIcon = shouldDrawLabelAndIcon(key);
873             }
874             if (key.icon != null && shouldDrawIcon) {
875                 // Special handing for the upper-right number hint icons
876                 final int drawableWidth;
877                 final int drawableHeight;
878                 final int drawableX;
879                 final int drawableY;
880                 if (shouldDrawIconFully(key)) {
881                     drawableWidth = key.width;
882                     drawableHeight = key.height;
883                     drawableX = 0;
884                     drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL;
885                 } else {
886                     drawableWidth = key.icon.getIntrinsicWidth();
887                     drawableHeight = key.icon.getIntrinsicHeight();
888                     drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2;
889                     drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2;
890                 }
891                 canvas.translate(drawableX, drawableY);
892                 key.icon.setBounds(0, 0, drawableWidth, drawableHeight);
893                 key.icon.draw(canvas);
894                 canvas.translate(-drawableX, -drawableY);
895             }
896             canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
897         }
898         mInvalidatedKey = null;
899         // Overlay a dark rectangle to dim the keyboard
900         if (mMiniKeyboard != null) {
901             paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
902             canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
903         }
904 
905         if (DEBUG) {
906             if (mShowTouchPoints) {
907                 for (PointerTracker tracker : mPointerTrackers) {
908                     int startX = tracker.getStartX();
909                     int startY = tracker.getStartY();
910                     int lastX = tracker.getLastX();
911                     int lastY = tracker.getLastY();
912                     paint.setAlpha(128);
913                     paint.setColor(0xFFFF0000);
914                     canvas.drawCircle(startX, startY, 3, paint);
915                     canvas.drawLine(startX, startY, lastX, lastY, paint);
916                     paint.setColor(0xFF0000FF);
917                     canvas.drawCircle(lastX, lastY, 3, paint);
918                     paint.setColor(0xFF00FF00);
919                     canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint);
920                 }
921             }
922         }
923 
924         mDrawPending = false;
925         mDirtyRect.setEmpty();
926     }
927 
928     // TODO: clean up this method.
dismissKeyPreview()929     private void dismissKeyPreview() {
930         for (PointerTracker tracker : mPointerTrackers)
931             tracker.updateKey(NOT_A_KEY);
932         showPreview(NOT_A_KEY, null);
933     }
934 
showPreview(int keyIndex, PointerTracker tracker)935     public void showPreview(int keyIndex, PointerTracker tracker) {
936         int oldKeyIndex = mOldPreviewKeyIndex;
937         mOldPreviewKeyIndex = keyIndex;
938         final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard)
939                 && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled();
940         // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show
941         // the space key preview and 3) pointer moves off the space key to other letter key, we
942         // should hide the preview of the previous key.
943         final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null)
944                 || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex);
945         // If key changed and preview is on or the key is space (language switch is enabled)
946         if (oldKeyIndex != keyIndex
947                 && (mShowPreview
948                         || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) {
949             if (keyIndex == NOT_A_KEY) {
950                 mHandler.cancelPopupPreview();
951                 mHandler.dismissPreview(mDelayAfterPreview);
952             } else if (tracker != null) {
953                 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker);
954             }
955         }
956     }
957 
showKey(final int keyIndex, PointerTracker tracker)958     private void showKey(final int keyIndex, PointerTracker tracker) {
959         Key key = tracker.getKey(keyIndex);
960         if (key == null)
961             return;
962         // Should not draw hint icon in key preview
963         if (key.icon != null && !shouldDrawLabelAndIcon(key)) {
964             mPreviewText.setCompoundDrawables(null, null, null,
965                     key.iconPreview != null ? key.iconPreview : key.icon);
966             mPreviewText.setText(null);
967         } else {
968             mPreviewText.setCompoundDrawables(null, null, null, null);
969             mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
970             if (key.label.length() > 1 && key.codes.length < 2) {
971                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
972                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
973             } else {
974                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
975                 mPreviewText.setTypeface(mKeyTextStyle);
976             }
977         }
978         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
979                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
980         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
981                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
982         final int popupHeight = mPreviewHeight;
983         LayoutParams lp = mPreviewText.getLayoutParams();
984         if (lp != null) {
985             lp.width = popupWidth;
986             lp.height = popupHeight;
987         }
988 
989         int popupPreviewX = key.x - (popupWidth - key.width) / 2;
990         int popupPreviewY = key.y - popupHeight + mPreviewOffset;
991 
992         mHandler.cancelDismissPreview();
993         if (mOffsetInWindow == null) {
994             mOffsetInWindow = new int[2];
995             getLocationInWindow(mOffsetInWindow);
996             mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero
997             mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero
998             int[] windowLocation = new int[2];
999             getLocationOnScreen(windowLocation);
1000             mWindowY = windowLocation[1];
1001         }
1002         // Set the preview background state
1003         mPreviewText.getBackground().setState(
1004                 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
1005         popupPreviewX += mOffsetInWindow[0];
1006         popupPreviewY += mOffsetInWindow[1];
1007 
1008         // If the popup cannot be shown above the key, put it on the side
1009         if (popupPreviewY + mWindowY < 0) {
1010             // If the key you're pressing is on the left side of the keyboard, show the popup on
1011             // the right, offset by enough to see at least one key to the left/right.
1012             if (key.x + key.width <= getWidth() / 2) {
1013                 popupPreviewX += (int) (key.width * 2.5);
1014             } else {
1015                 popupPreviewX -= (int) (key.width * 2.5);
1016             }
1017             popupPreviewY += popupHeight;
1018         }
1019 
1020         if (mPreviewPopup.isShowing()) {
1021             mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight);
1022         } else {
1023             mPreviewPopup.setWidth(popupWidth);
1024             mPreviewPopup.setHeight(popupHeight);
1025             mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY,
1026                     popupPreviewX, popupPreviewY);
1027         }
1028         // Record popup preview position to display mini-keyboard later at the same positon
1029         mPopupPreviewDisplayedY = popupPreviewY;
1030         mPreviewText.setVisibility(VISIBLE);
1031     }
1032 
1033     /**
1034      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1035      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1036      * draws the cached buffer.
1037      * @see #invalidateKey(Key)
1038      */
invalidateAllKeys()1039     public void invalidateAllKeys() {
1040         mDirtyRect.union(0, 0, getWidth(), getHeight());
1041         mDrawPending = true;
1042         invalidate();
1043     }
1044 
1045     /**
1046      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1047      * one key is changing it's content. Any changes that affect the position or size of the key
1048      * may not be honored.
1049      * @param key key in the attached {@link Keyboard}.
1050      * @see #invalidateAllKeys
1051      */
invalidateKey(Key key)1052     public void invalidateKey(Key key) {
1053         if (key == null)
1054             return;
1055         mInvalidatedKey = key;
1056         // TODO we should clean up this and record key's region to use in onBufferDraw.
1057         mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1058                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1059         onBufferDraw();
1060         invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
1061                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
1062     }
1063 
openPopupIfRequired(int keyIndex, PointerTracker tracker)1064     private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) {
1065         // Check if we have a popup layout specified first.
1066         if (mPopupLayout == 0) {
1067             return false;
1068         }
1069 
1070         Key popupKey = tracker.getKey(keyIndex);
1071         if (popupKey == null)
1072             return false;
1073         boolean result = onLongPress(popupKey);
1074         if (result) {
1075             dismissKeyPreview();
1076             mMiniKeyboardTrackerId = tracker.mPointerId;
1077             // Mark this tracker "already processed" and remove it from the pointer queue
1078             tracker.setAlreadyProcessed();
1079             mPointerQueue.remove(tracker);
1080         }
1081         return result;
1082     }
1083 
inflateMiniKeyboardContainer(Key popupKey)1084     private View inflateMiniKeyboardContainer(Key popupKey) {
1085         int popupKeyboardId = popupKey.popupResId;
1086         LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(
1087                 Context.LAYOUT_INFLATER_SERVICE);
1088         View container = inflater.inflate(mPopupLayout, null);
1089         if (container == null)
1090             throw new NullPointerException();
1091 
1092         LatinKeyboardBaseView miniKeyboard =
1093                 (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
1094         miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
1095             public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
1096                 mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y);
1097                 dismissPopupKeyboard();
1098             }
1099 
1100             public void onText(CharSequence text) {
1101                 mKeyboardActionListener.onText(text);
1102                 dismissPopupKeyboard();
1103             }
1104 
1105             public void onCancel() {
1106                 mKeyboardActionListener.onCancel();
1107                 dismissPopupKeyboard();
1108             }
1109 
1110             public void swipeLeft() {
1111             }
1112             public void swipeRight() {
1113             }
1114             public void swipeUp() {
1115             }
1116             public void swipeDown() {
1117             }
1118             public void onPress(int primaryCode) {
1119                 mKeyboardActionListener.onPress(primaryCode);
1120             }
1121             public void onRelease(int primaryCode) {
1122                 mKeyboardActionListener.onRelease(primaryCode);
1123             }
1124         });
1125         // Override default ProximityKeyDetector.
1126         miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance);
1127         // Remove gesture detector on mini-keyboard
1128         miniKeyboard.mGestureDetector = null;
1129 
1130         Keyboard keyboard;
1131         if (popupKey.popupCharacters != null) {
1132             keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters,
1133                     -1, getPaddingLeft() + getPaddingRight());
1134         } else {
1135             keyboard = new Keyboard(getContext(), popupKeyboardId);
1136         }
1137         miniKeyboard.setKeyboard(keyboard);
1138         miniKeyboard.setPopupParent(this);
1139 
1140         container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1141                 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1142 
1143         return container;
1144     }
1145 
isOneRowKeys(List<Key> keys)1146     private static boolean isOneRowKeys(List<Key> keys) {
1147         if (keys.size() == 0) return false;
1148         final int edgeFlags = keys.get(0).edgeFlags;
1149         // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
1150         // does not have both top and bottom edge flags on at the same time.  On the other hand,
1151         // the first key of mini keyboard that was created with popupCharacters must have both top
1152         // and bottom edge flags on.
1153         // When you want to use one row mini-keyboard from xml file, make sure that the row has
1154         // both top and bottom edge flags set.
1155         return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
1156     }
1157 
1158     /**
1159      * Called when a key is long pressed. By default this will open any popup keyboard associated
1160      * with this key through the attributes popupLayout and popupCharacters.
1161      * @param popupKey the key that was long pressed
1162      * @return true if the long press is handled, false otherwise. Subclasses should call the
1163      * method on the base class if the subclass doesn't wish to handle the call.
1164      */
onLongPress(Key popupKey)1165     protected boolean onLongPress(Key popupKey) {
1166         // TODO if popupKey.popupCharacters has only one letter, send it as key without opening
1167         // mini keyboard.
1168 
1169         if (popupKey.popupResId == 0)
1170             return false;
1171 
1172         View container = mMiniKeyboardCache.get(popupKey);
1173         if (container == null) {
1174             container = inflateMiniKeyboardContainer(popupKey);
1175             mMiniKeyboardCache.put(popupKey, container);
1176         }
1177         mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
1178         if (mWindowOffset == null) {
1179             mWindowOffset = new int[2];
1180             getLocationInWindow(mWindowOffset);
1181         }
1182 
1183         // Get width of a key in the mini popup keyboard = "miniKeyWidth".
1184         // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard.
1185         // We adjust the position of mini popup keyboard with the edge key in it:
1186         //  a) When we have the leftmost key in popup keyboard directly above the pressed key
1187         //     Right edges of both keys should be aligned for consistent default selection
1188         //  b) When we have the rightmost key in popup keyboard directly above the pressed key
1189         //     Left edges of both keys should be aligned for consistent default selection
1190         final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys();
1191         final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).width : 0;
1192 
1193         // HACK: Have the leftmost number in the popup characters right above the key
1194         boolean isNumberAtLeftmost =
1195                 hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey);
1196         int popupX = popupKey.x + mWindowOffset[0];
1197         popupX += getPaddingLeft();
1198         if (isNumberAtLeftmost) {
1199             popupX += popupKey.width - miniKeyWidth;  // adjustment for a) described above
1200             popupX -= container.getPaddingLeft();
1201         } else {
1202             popupX += miniKeyWidth;  // adjustment for b) described above
1203             popupX -= container.getMeasuredWidth();
1204             popupX += container.getPaddingRight();
1205         }
1206         int popupY = popupKey.y + mWindowOffset[1];
1207         popupY += getPaddingTop();
1208         popupY -= container.getMeasuredHeight();
1209         popupY += container.getPaddingBottom();
1210         final int x = popupX;
1211         final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY;
1212 
1213         int adjustedX = x;
1214         if (x < 0) {
1215             adjustedX = 0;
1216         } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) {
1217             adjustedX = getMeasuredWidth() - container.getMeasuredWidth();
1218         }
1219         mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0];
1220         mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1];
1221         mMiniKeyboard.setPopupOffset(adjustedX, y);
1222         mMiniKeyboard.setShifted(isShifted());
1223         // Mini keyboard needs no pop-up key preview displayed.
1224         mMiniKeyboard.setPreviewEnabled(false);
1225         mMiniKeyboardPopup.setContentView(container);
1226         mMiniKeyboardPopup.setWidth(container.getMeasuredWidth());
1227         mMiniKeyboardPopup.setHeight(container.getMeasuredHeight());
1228         mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1229 
1230         // Inject down event on the key to mini keyboard.
1231         long eventTime = SystemClock.uptimeMillis();
1232         mMiniKeyboardPopupTime = eventTime;
1233         MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.x
1234                 + popupKey.width / 2, popupKey.y + popupKey.height / 2, eventTime);
1235         mMiniKeyboard.onTouchEvent(downEvent);
1236         downEvent.recycle();
1237 
1238         invalidateAllKeys();
1239         return true;
1240     }
1241 
hasMultiplePopupChars(Key key)1242     private static boolean hasMultiplePopupChars(Key key) {
1243         if (key.popupCharacters != null && key.popupCharacters.length() > 1) {
1244             return true;
1245         }
1246         return false;
1247     }
1248 
shouldDrawIconFully(Key key)1249     private boolean shouldDrawIconFully(Key key) {
1250         return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key)
1251                 || LatinKeyboard.hasPuncOrSmileysPopup(key);
1252     }
1253 
shouldDrawLabelAndIcon(Key key)1254     private boolean shouldDrawLabelAndIcon(Key key) {
1255         return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key)
1256                 || LatinKeyboard.hasPuncOrSmileysPopup(key);
1257     }
1258 
isLatinF1Key(Key key)1259     private boolean isLatinF1Key(Key key) {
1260         return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key);
1261     }
1262 
isNonMicLatinF1Key(Key key)1263     private boolean isNonMicLatinF1Key(Key key) {
1264         return isLatinF1Key(key) && key.label != null;
1265     }
1266 
isNumberAtEdgeOfPopupChars(Key key)1267     private static boolean isNumberAtEdgeOfPopupChars(Key key) {
1268         return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key);
1269     }
1270 
isNumberAtLeftmostPopupChar(Key key)1271     /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) {
1272         if (key.popupCharacters != null && key.popupCharacters.length() > 0
1273                 && isAsciiDigit(key.popupCharacters.charAt(0))) {
1274             return true;
1275         }
1276         return false;
1277     }
1278 
isNumberAtRightmostPopupChar(Key key)1279     /* package */ static boolean isNumberAtRightmostPopupChar(Key key) {
1280         if (key.popupCharacters != null && key.popupCharacters.length() > 0
1281                 && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) {
1282             return true;
1283         }
1284         return false;
1285     }
1286 
isAsciiDigit(char c)1287     private static boolean isAsciiDigit(char c) {
1288         return (c < 0x80) && Character.isDigit(c);
1289     }
1290 
generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime)1291     private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) {
1292         return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action,
1293                     x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0);
1294     }
1295 
getPointerTracker(final int id)1296     private PointerTracker getPointerTracker(final int id) {
1297         final ArrayList<PointerTracker> pointers = mPointerTrackers;
1298         final Key[] keys = mKeys;
1299         final OnKeyboardActionListener listener = mKeyboardActionListener;
1300 
1301         // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
1302         for (int i = pointers.size(); i <= id; i++) {
1303             final PointerTracker tracker =
1304                 new PointerTracker(i, mHandler, mKeyDetector, this, getResources());
1305             if (keys != null)
1306                 tracker.setKeyboard(keys, mKeyHysteresisDistance);
1307             if (listener != null)
1308                 tracker.setOnKeyboardActionListener(listener);
1309             pointers.add(tracker);
1310         }
1311 
1312         return pointers.get(id);
1313     }
1314 
isInSlidingKeyInput()1315     public boolean isInSlidingKeyInput() {
1316         if (mMiniKeyboard != null) {
1317             return mMiniKeyboard.isInSlidingKeyInput();
1318         } else {
1319             return mPointerQueue.isInSlidingKeyInput();
1320         }
1321     }
1322 
getPointerCount()1323     public int getPointerCount() {
1324         return mOldPointerCount;
1325     }
1326 
1327     @Override
onTouchEvent(MotionEvent me)1328     public boolean onTouchEvent(MotionEvent me) {
1329         final int action = me.getActionMasked();
1330         final int pointerCount = me.getPointerCount();
1331         final int oldPointerCount = mOldPointerCount;
1332         mOldPointerCount = pointerCount;
1333 
1334         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1335         // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1336         // events except a transition from/to single-touch.
1337         if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
1338             return true;
1339         }
1340 
1341         // Track the last few movements to look for spurious swipes.
1342         mSwipeTracker.addMovement(me);
1343 
1344         // Gesture detector must be enabled only when mini-keyboard is not on the screen.
1345         if (mMiniKeyboard == null
1346                 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
1347             dismissKeyPreview();
1348             mHandler.cancelKeyTimers();
1349             return true;
1350         }
1351 
1352         final long eventTime = me.getEventTime();
1353         final int index = me.getActionIndex();
1354         final int id = me.getPointerId(index);
1355         final int x = (int)me.getX(index);
1356         final int y = (int)me.getY(index);
1357 
1358         // Needs to be called after the gesture detector gets a turn, as it may have
1359         // displayed the mini keyboard
1360         if (mMiniKeyboard != null) {
1361             final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId);
1362             if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) {
1363                 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex);
1364                 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex);
1365                 MotionEvent translated = generateMiniKeyboardMotionEvent(action,
1366                         miniKeyboardX, miniKeyboardY, eventTime);
1367                 mMiniKeyboard.onTouchEvent(translated);
1368                 translated.recycle();
1369             }
1370             return true;
1371         }
1372 
1373         if (mHandler.isInKeyRepeat()) {
1374             // It will keep being in the key repeating mode while the key is being pressed.
1375             if (action == MotionEvent.ACTION_MOVE) {
1376                 return true;
1377             }
1378             final PointerTracker tracker = getPointerTracker(id);
1379             // Key repeating timer will be canceled if 2 or more keys are in action, and current
1380             // event (UP or DOWN) is non-modifier key.
1381             if (pointerCount > 1 && !tracker.isModifier()) {
1382                 mHandler.cancelKeyRepeatTimer();
1383             }
1384             // Up event will pass through.
1385         }
1386 
1387         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1388         // Translate mutli-touch event to single-touch events on the device that has no distinct
1389         // multi-touch panel.
1390         if (!mHasDistinctMultitouch) {
1391             // Use only main (id=0) pointer tracker.
1392             PointerTracker tracker = getPointerTracker(0);
1393             if (pointerCount == 1 && oldPointerCount == 2) {
1394                 // Multi-touch to single touch transition.
1395                 // Send a down event for the latest pointer.
1396                 tracker.onDownEvent(x, y, eventTime);
1397             } else if (pointerCount == 2 && oldPointerCount == 1) {
1398                 // Single-touch to multi-touch transition.
1399                 // Send an up event for the last pointer.
1400                 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime);
1401             } else if (pointerCount == 1 && oldPointerCount == 1) {
1402                 tracker.onTouchEvent(action, x, y, eventTime);
1403             } else {
1404                 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1405                         + " (old " + oldPointerCount + ")");
1406             }
1407             return true;
1408         }
1409 
1410         if (action == MotionEvent.ACTION_MOVE) {
1411             for (int i = 0; i < pointerCount; i++) {
1412                 PointerTracker tracker = getPointerTracker(me.getPointerId(i));
1413                 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime);
1414             }
1415         } else {
1416             PointerTracker tracker = getPointerTracker(id);
1417             switch (action) {
1418             case MotionEvent.ACTION_DOWN:
1419             case MotionEvent.ACTION_POINTER_DOWN:
1420                 onDownEvent(tracker, x, y, eventTime);
1421                 break;
1422             case MotionEvent.ACTION_UP:
1423             case MotionEvent.ACTION_POINTER_UP:
1424                 onUpEvent(tracker, x, y, eventTime);
1425                 break;
1426             case MotionEvent.ACTION_CANCEL:
1427                 onCancelEvent(tracker, x, y, eventTime);
1428                 break;
1429             }
1430         }
1431 
1432         return true;
1433     }
1434 
onDownEvent(PointerTracker tracker, int x, int y, long eventTime)1435     private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) {
1436         if (tracker.isOnModifierKey(x, y)) {
1437             // Before processing a down event of modifier key, all pointers already being tracked
1438             // should be released.
1439             mPointerQueue.releaseAllPointersExcept(null, eventTime);
1440         }
1441         tracker.onDownEvent(x, y, eventTime);
1442         mPointerQueue.add(tracker);
1443     }
1444 
onUpEvent(PointerTracker tracker, int x, int y, long eventTime)1445     private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) {
1446         if (tracker.isModifier()) {
1447             // Before processing an up event of modifier key, all pointers already being tracked
1448             // should be released.
1449             mPointerQueue.releaseAllPointersExcept(tracker, eventTime);
1450         } else {
1451             int index = mPointerQueue.lastIndexOf(tracker);
1452             if (index >= 0) {
1453                 mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime);
1454             } else {
1455                 Log.w(TAG, "onUpEvent: corresponding down event not found for pointer "
1456                         + tracker.mPointerId);
1457             }
1458         }
1459         tracker.onUpEvent(x, y, eventTime);
1460         mPointerQueue.remove(tracker);
1461     }
1462 
onCancelEvent(PointerTracker tracker, int x, int y, long eventTime)1463     private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) {
1464         tracker.onCancelEvent(x, y, eventTime);
1465         mPointerQueue.remove(tracker);
1466     }
1467 
swipeRight()1468     protected void swipeRight() {
1469         mKeyboardActionListener.swipeRight();
1470     }
1471 
swipeLeft()1472     protected void swipeLeft() {
1473         mKeyboardActionListener.swipeLeft();
1474     }
1475 
swipeUp()1476     protected void swipeUp() {
1477         mKeyboardActionListener.swipeUp();
1478     }
1479 
swipeDown()1480     protected void swipeDown() {
1481         mKeyboardActionListener.swipeDown();
1482     }
1483 
closing()1484     public void closing() {
1485         mPreviewPopup.dismiss();
1486         mHandler.cancelAllMessages();
1487 
1488         dismissPopupKeyboard();
1489         mBuffer = null;
1490         mCanvas = null;
1491         mMiniKeyboardCache.clear();
1492     }
1493 
1494     @Override
onDetachedFromWindow()1495     public void onDetachedFromWindow() {
1496         super.onDetachedFromWindow();
1497         closing();
1498     }
1499 
dismissPopupKeyboard()1500     private void dismissPopupKeyboard() {
1501         if (mMiniKeyboardPopup.isShowing()) {
1502             mMiniKeyboardPopup.dismiss();
1503             mMiniKeyboard = null;
1504             mMiniKeyboardOriginX = 0;
1505             mMiniKeyboardOriginY = 0;
1506             invalidateAllKeys();
1507         }
1508     }
1509 
handleBack()1510     public boolean handleBack() {
1511         if (mMiniKeyboardPopup.isShowing()) {
1512             dismissPopupKeyboard();
1513             return true;
1514         }
1515         return false;
1516     }
1517 }
1518