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