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