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