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