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