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