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