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