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