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