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