KeyboardView.java revision 9d9522abdcee70408c9e99ac20c8e1c224eef19d
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.Paint;
27import android.graphics.Paint.Align;
28import android.graphics.PorterDuff;
29import android.graphics.Rect;
30import android.graphics.Region.Op;
31import android.graphics.Typeface;
32import android.graphics.drawable.Drawable;
33import android.os.Handler;
34import android.os.Message;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.TypedValue;
38import android.view.GestureDetector;
39import android.view.LayoutInflater;
40import android.view.MotionEvent;
41import android.view.View;
42import android.view.ViewConfiguration;
43import android.view.ViewGroup;
44import android.view.ViewGroup.MarginLayoutParams;
45import android.widget.FrameLayout;
46import android.widget.LinearLayout;
47import android.widget.PopupWindow;
48import android.widget.TextView;
49
50import com.android.inputmethod.latin.LatinImeLogger;
51import com.android.inputmethod.latin.R;
52
53import java.util.ArrayList;
54import java.util.HashMap;
55import java.util.WeakHashMap;
56
57/**
58 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key
59 * presses and touch movements.
60 *
61 * @attr ref R.styleable#KeyboardView_backgroundDimAmount
62 * @attr ref R.styleable#KeyboardView_colorScheme
63 * @attr ref R.styleable#KeyboardView_keyBackground
64 * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
65 * @attr ref R.styleable#KeyboardView_keyLetterRatio
66 * @attr ref R.styleable#KeyboardView_keyLabelRatio
67 * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
68 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio
69 * @attr ref R.styleable#KeyboardView_keyTextStyle
70 * @attr ref R.styleable#KeyboardView_keyPreviewLayout
71 * @attr ref R.styleable#KeyboardView_keyPreviewOffset
72 * @attr ref R.styleable#KeyboardView_keyPreviewHeight
73 * @attr ref R.styleable#KeyboardView_keyTextColor
74 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
75 * @attr ref R.styleable#KeyboardView_keyHintLetterColor
76 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor
77 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor
78 * @attr ref R.styleable#KeyboardView_verticalCorrection
79 * @attr ref R.styleable#KeyboardView_popupLayout
80 * @attr ref R.styleable#KeyboardView_shadowColor
81 * @attr ref R.styleable#KeyboardView_shadowRadius
82 */
83public class KeyboardView extends View implements PointerTracker.UIProxy {
84    private static final String TAG = KeyboardView.class.getSimpleName();
85    private static final boolean DEBUG_SHOW_ALIGN = false;
86    private static final boolean DEBUG_KEYBOARD_GRID = false;
87
88    private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true;
89    private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
90
91    public static final int COLOR_SCHEME_WHITE = 0;
92    public static final int COLOR_SCHEME_BLACK = 1;
93
94    // Timing constants
95    private final int mKeyRepeatInterval;
96
97    // Miscellaneous constants
98    private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
99    private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1;
100
101    // XML attribute
102    private final float mKeyLetterRatio;
103    private final int mKeyTextColor;
104    private final int mKeyTextInactivatedColor;
105    private final Typeface mKeyTextStyle;
106    private final float mKeyLabelRatio;
107    private final float mKeyHintLetterRatio;
108    private final float mKeyUppercaseLetterRatio;
109    private final int mColorScheme;
110    private final int mShadowColor;
111    private final float mShadowRadius;
112    private final Drawable mKeyBackground;
113    private final float mBackgroundDimAmount;
114    private final float mKeyHysteresisDistance;
115    private final float mVerticalCorrection;
116    private final int mPreviewOffset;
117    private final int mPreviewHeight;
118    private final int mPopupLayout;
119    private final Drawable mKeyPopupHintIcon;
120    private final int mKeyHintLetterColor;
121    private final int mKeyUppercaseLetterInactivatedColor;
122    private final int mKeyUppercaseLetterActivatedColor;
123
124    // Main keyboard
125    private Keyboard mKeyboard;
126    private int mKeyLetterSize;
127    private int mKeyLabelSize;
128    private int mKeyHintLetterSize;
129    private int mKeyUppercaseLetterSize;
130
131    // Key preview
132    private boolean mInForeground;
133    private TextView mPreviewText;
134    private float mPreviewTextRatio;
135    private int mPreviewTextSize;
136    private boolean mShowKeyPreviewPopup = true;
137    private int mKeyPreviewPopupDisplayedY = -1;
138    private final int mDelayBeforePreview;
139    private int mDelayAfterPreview;
140    private ViewGroup mPreviewPlacer;
141    private final int[] mCoordinates = new int[2];
142
143    // Mini keyboard
144    private PopupWindow mPopupWindow;
145    private PopupPanel mPopupMiniKeyboardPanel;
146    private final WeakHashMap<Key, PopupPanel> mPopupPanelCache =
147            new WeakHashMap<Key, PopupPanel>();
148
149    /** Listener for {@link KeyboardActionListener}. */
150    private KeyboardActionListener mKeyboardActionListener;
151
152    private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
153
154    // TODO: Let the PointerTracker class manage this pointer queue
155    private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
156
157    private final boolean mHasDistinctMultitouch;
158    private int mOldPointerCount = 1;
159    private int mOldKeyIndex;
160
161    protected KeyDetector mKeyDetector = new KeyDetector();
162
163    // Swipe gesture detector
164    protected GestureDetector mGestureDetector;
165    private final SwipeTracker mSwipeTracker = new SwipeTracker();
166    private final int mSwipeThreshold;
167    private final boolean mDisambiguateSwipe;
168
169    // Drawing
170    /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
171    private boolean mDrawPending;
172    /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
173    private boolean mKeyboardChanged;
174    /** The dirty region in the keyboard bitmap */
175    private final Rect mDirtyRect = new Rect();
176    /** The key to invalidate. */
177    private Key mInvalidatedKey;
178    /** The dirty region for single key drawing */
179    private final Rect mInvalidatedKeyRect = new Rect();
180    /** The keyboard bitmap for faster updates */
181    private Bitmap mBuffer;
182    /** The canvas for the above mutable keyboard bitmap */
183    private Canvas mCanvas;
184    private final Paint mPaint = new Paint();
185    private final Rect mPadding = new Rect();
186    private final Rect mTextBounds = new Rect();
187    // This map caches key label text height in pixel as value and key label text size as map key.
188    private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
189    // This map caches key label text width in pixel as value and key label text size as map key.
190    private final HashMap<Integer, Integer> mTextWidthCache = new HashMap<Integer, Integer>();
191    // Distance from horizontal center of the key, proportional to key label text height and width.
192    private static final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
193    private static final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
194    private static final String KEY_LABEL_REFERENCE_CHAR = "M";
195    private final int mKeyLabelHorizontalPadding;
196
197    private final UIHandler mHandler = new UIHandler();
198
199    class UIHandler extends Handler {
200        private static final int MSG_SHOW_KEY_PREVIEW = 1;
201        private static final int MSG_DISMISS_KEY_PREVIEW = 2;
202        private static final int MSG_REPEAT_KEY = 3;
203        private static final int MSG_LONGPRESS_KEY = 4;
204        private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
205        private static final int MSG_IGNORE_DOUBLE_TAP = 6;
206
207        private boolean mInKeyRepeat;
208
209        @Override
210        public void handleMessage(Message msg) {
211            final PointerTracker tracker = (PointerTracker) msg.obj;
212            switch (msg.what) {
213            case MSG_SHOW_KEY_PREVIEW:
214                showKey(msg.arg1, tracker);
215                break;
216            case MSG_DISMISS_KEY_PREVIEW:
217                mPreviewText.setVisibility(View.INVISIBLE);
218                break;
219            case MSG_REPEAT_KEY:
220                tracker.onRepeatKey(msg.arg1);
221                startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
222                break;
223            case MSG_LONGPRESS_KEY:
224                openMiniKeyboardIfRequired(msg.arg1, tracker);
225                break;
226            case MSG_LONGPRESS_SHIFT_KEY:
227                onLongPressShiftKey(tracker);
228                break;
229            }
230        }
231
232        public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
233            removeMessages(MSG_SHOW_KEY_PREVIEW);
234            if (mPreviewText.getVisibility() == VISIBLE || delay == 0) {
235                // Show right away, if it's already visible and finger is moving around
236                showKey(keyIndex, tracker);
237            } else {
238                sendMessageDelayed(
239                        obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
240            }
241        }
242
243        public void cancelShowKeyPreview(PointerTracker tracker) {
244            removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
245        }
246
247        public void cancelAllShowKeyPreviews() {
248            removeMessages(MSG_SHOW_KEY_PREVIEW);
249        }
250
251        public void dismissKeyPreview(long delay, PointerTracker tracker) {
252            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
253        }
254
255        public void cancelDismissKeyPreview(PointerTracker tracker) {
256            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
257        }
258
259        public void cancelAllDismissKeyPreviews() {
260            removeMessages(MSG_DISMISS_KEY_PREVIEW);
261        }
262
263        public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
264            mInKeyRepeat = true;
265            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
266        }
267
268        public void cancelKeyRepeatTimer() {
269            mInKeyRepeat = false;
270            removeMessages(MSG_REPEAT_KEY);
271        }
272
273        public boolean isInKeyRepeat() {
274            return mInKeyRepeat;
275        }
276
277        public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
278            cancelLongPressTimers();
279            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
280        }
281
282        public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
283            cancelLongPressTimers();
284            if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
285                sendMessageDelayed(
286                        obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
287            }
288        }
289
290        public void cancelLongPressTimers() {
291            removeMessages(MSG_LONGPRESS_KEY);
292            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
293        }
294
295        public void cancelKeyTimers() {
296            cancelKeyRepeatTimer();
297            cancelLongPressTimers();
298            removeMessages(MSG_IGNORE_DOUBLE_TAP);
299        }
300
301        public void startIgnoringDoubleTap() {
302            sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP),
303                    ViewConfiguration.getDoubleTapTimeout());
304        }
305
306        public boolean isIgnoringDoubleTap() {
307            return hasMessages(MSG_IGNORE_DOUBLE_TAP);
308        }
309
310        public void cancelAllMessages() {
311            cancelKeyTimers();
312            cancelAllShowKeyPreviews();
313            cancelAllDismissKeyPreviews();
314        }
315    }
316
317    public KeyboardView(Context context, AttributeSet attrs) {
318        this(context, attrs, R.attr.keyboardViewStyle);
319    }
320
321    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
322        super(context, attrs, defStyle);
323
324        final TypedArray a = context.obtainStyledAttributes(
325                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
326
327        mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
328        mKeyHysteresisDistance = a.getDimensionPixelOffset(
329                R.styleable.KeyboardView_keyHysteresisDistance, 0);
330        mVerticalCorrection = a.getDimensionPixelOffset(
331                R.styleable.KeyboardView_verticalCorrection, 0);
332        final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
333        mPreviewOffset = a.getDimensionPixelOffset(R.styleable.KeyboardView_keyPreviewOffset, 0);
334        mPreviewHeight = a.getDimensionPixelSize(R.styleable.KeyboardView_keyPreviewHeight, 80);
335        mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
336        mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
337        mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
338        mKeyUppercaseLetterRatio = getRatio(a,
339                R.styleable.KeyboardView_keyUppercaseLetterRatio);
340        mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
341        mKeyTextInactivatedColor = a.getColor(
342                R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
343        mKeyPopupHintIcon = a.getDrawable(R.styleable.KeyboardView_keyPopupHintIcon);
344        mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
345        mKeyUppercaseLetterInactivatedColor = a.getColor(
346                R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0);
347        mKeyUppercaseLetterActivatedColor = a.getColor(
348                R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0);
349        mKeyTextStyle = Typeface.defaultFromStyle(
350                a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
351        mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0);
352        mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
353        mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
354        // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
355        mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f);
356        mColorScheme = a.getInt(R.styleable.KeyboardView_colorScheme, COLOR_SCHEME_WHITE);
357        a.recycle();
358
359        final Resources res = getResources();
360
361        if (previewLayout != 0) {
362            mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null);
363            mPreviewTextRatio = getRatio(res, R.fraction.key_preview_text_ratio);
364        } else {
365            mShowKeyPreviewPopup = false;
366        }
367        mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
368        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
369        mKeyLabelHorizontalPadding = (int)res.getDimension(
370                R.dimen.key_label_horizontal_alignment_padding);
371
372        mPaint.setAntiAlias(true);
373        mPaint.setTextAlign(Align.CENTER);
374        mPaint.setAlpha(255);
375
376        mKeyBackground.getPadding(mPadding);
377
378        mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
379        // TODO: Refer to frameworks/base/core/res/res/values/config.xml
380        mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
381
382        GestureDetector.SimpleOnGestureListener listener =
383                new GestureDetector.SimpleOnGestureListener() {
384            private boolean mProcessingShiftDoubleTapEvent = false;
385
386            @Override
387            public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
388                    float velocityY) {
389                final float absX = Math.abs(velocityX);
390                final float absY = Math.abs(velocityY);
391                float deltaY = me2.getY() - me1.getY();
392                int travelY = getHeight() / 2; // Half the keyboard height
393                mSwipeTracker.computeCurrentVelocity(1000);
394                final float endingVelocityY = mSwipeTracker.getYVelocity();
395                if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
396                    if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
397                        onSwipeDown();
398                        return true;
399                    }
400                }
401                return false;
402            }
403
404            @Override
405            public boolean onDoubleTap(MotionEvent firstDown) {
406                if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
407                        && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
408                    final int pointerIndex = firstDown.getActionIndex();
409                    final int id = firstDown.getPointerId(pointerIndex);
410                    final PointerTracker tracker = getPointerTracker(id);
411                    // If the first down event is on shift key.
412                    if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
413                        mProcessingShiftDoubleTapEvent = true;
414                        return true;
415                    }
416                }
417                mProcessingShiftDoubleTapEvent = false;
418                return false;
419            }
420
421            @Override
422            public boolean onDoubleTapEvent(MotionEvent secondTap) {
423                if (mProcessingShiftDoubleTapEvent
424                        && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
425                    final MotionEvent secondDown = secondTap;
426                    final int pointerIndex = secondDown.getActionIndex();
427                    final int id = secondDown.getPointerId(pointerIndex);
428                    final PointerTracker tracker = getPointerTracker(id);
429                    // If the second down event is also on shift key.
430                    if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
431                        // Detected a double tap on shift key. If we are in the ignoring double tap
432                        // mode, it means we have already turned off caps lock in
433                        // {@link KeyboardSwitcher#onReleaseShift} .
434                        final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap();
435                        if (!ignoringDoubleTap)
436                            onDoubleTapShiftKey(tracker);
437                        return true;
438                    }
439                    // Otherwise these events should not be handled as double tap.
440                    mProcessingShiftDoubleTapEvent = false;
441                }
442                return mProcessingShiftDoubleTapEvent;
443            }
444        };
445
446        final boolean ignoreMultitouch = true;
447        mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
448        mGestureDetector.setIsLongpressEnabled(false);
449
450        mHasDistinctMultitouch = context.getPackageManager()
451                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
452        mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
453    }
454
455    // Read fraction value in TypedArray as float.
456    private static float getRatio(TypedArray a, int index) {
457        return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
458    }
459
460    // Read fraction value in resource as float.
461    private static float getRatio(Resources res, int id) {
462        return res.getFraction(id, 1000, 1000) / 1000.0f;
463    }
464
465    public void startIgnoringDoubleTap() {
466        if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
467            mHandler.startIgnoringDoubleTap();
468    }
469
470    public void setOnKeyboardActionListener(KeyboardActionListener listener) {
471        mKeyboardActionListener = listener;
472        for (PointerTracker tracker : mPointerTrackers) {
473            tracker.setOnKeyboardActionListener(listener);
474        }
475    }
476
477    /**
478     * Returns the {@link KeyboardActionListener} object.
479     * @return the listener attached to this keyboard
480     */
481    protected KeyboardActionListener getOnKeyboardActionListener() {
482        return mKeyboardActionListener;
483    }
484
485    @Override
486    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
487        // TODO: Should notify InputMethodService instead?
488        KeyboardSwitcher.getInstance().onSizeChanged();
489    }
490
491    /**
492     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
493     * view will re-layout itself to accommodate the keyboard.
494     * @see Keyboard
495     * @see #getKeyboard()
496     * @param keyboard the keyboard to display in this view
497     */
498    public void setKeyboard(Keyboard keyboard) {
499        if (mKeyboard != null) {
500            dismissAllKeyPreviews();
501        }
502        // Remove any pending messages, except dismissing preview
503        mHandler.cancelKeyTimers();
504        mHandler.cancelAllShowKeyPreviews();
505        mKeyboard = keyboard;
506        LatinImeLogger.onSetKeyboard(keyboard);
507        mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
508                -getPaddingTop() + mVerticalCorrection);
509        for (PointerTracker tracker : mPointerTrackers) {
510            tracker.setKeyboard(keyboard, mKeyHysteresisDistance);
511        }
512        requestLayout();
513        mKeyboardChanged = true;
514        invalidateAllKeys();
515        mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth());
516        mPopupPanelCache.clear();
517        final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap();
518        mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
519        mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
520        mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
521        mKeyUppercaseLetterSize = (int)(
522                keyHeight * mKeyUppercaseLetterRatio);
523        mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
524    }
525
526    /**
527     * Returns the current keyboard being displayed by this view.
528     * @return the currently attached keyboard
529     * @see #setKeyboard(Keyboard)
530     */
531    public Keyboard getKeyboard() {
532        return mKeyboard;
533    }
534
535    /**
536     * Returns whether the device has distinct multi-touch panel.
537     * @return true if the device has distinct multi-touch panel.
538     */
539    @Override
540    public boolean hasDistinctMultitouch() {
541        return mHasDistinctMultitouch;
542    }
543
544    /**
545     * Enables or disables the key feedback popup. This is a popup that shows a magnified
546     * version of the depressed key. By default the preview is enabled.
547     * @param previewEnabled whether or not to enable the key feedback preview
548     * @param delay the delay after which the preview is dismissed
549     * @see #isKeyPreviewPopupEnabled()
550     */
551    public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
552        mShowKeyPreviewPopup = previewEnabled;
553        mDelayAfterPreview = delay;
554    }
555
556    /**
557     * Returns the enabled state of the key feedback preview
558     * @return whether or not the key feedback preview is enabled
559     * @see #setKeyPreviewPopupEnabled(boolean, int)
560     */
561    public boolean isKeyPreviewPopupEnabled() {
562        return mShowKeyPreviewPopup;
563    }
564
565    public int getColorScheme() {
566        return mColorScheme;
567    }
568
569    /**
570     * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
571     * codes for adjacent keys.  When disabled, only the primary key code will be
572     * reported.
573     * @param enabled whether or not the proximity correction is enabled
574     */
575    public void setProximityCorrectionEnabled(boolean enabled) {
576        mKeyDetector.setProximityCorrectionEnabled(enabled);
577    }
578
579    /**
580     * Returns true if proximity correction is enabled.
581     */
582    public boolean isProximityCorrectionEnabled() {
583        return mKeyDetector.isProximityCorrectionEnabled();
584    }
585
586    protected CharSequence adjustCase(CharSequence label) {
587        if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
588                && Character.isLowerCase(label.charAt(0))) {
589            return label.toString().toUpperCase();
590        }
591        return label;
592    }
593
594    @Override
595    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
596        // Round up a little
597        if (mKeyboard == null) {
598            setMeasuredDimension(
599                    getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
600        } else {
601            int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
602            if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
603                width = MeasureSpec.getSize(widthMeasureSpec);
604            }
605            setMeasuredDimension(
606                    width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
607        }
608    }
609
610    @Override
611    public void onDraw(Canvas canvas) {
612        super.onDraw(canvas);
613        if (mDrawPending || mBuffer == null || mKeyboardChanged) {
614            onBufferDraw();
615        }
616        canvas.drawBitmap(mBuffer, 0, 0, null);
617    }
618
619    private void onBufferDraw() {
620        final int width = getWidth();
621        final int height = getHeight();
622        if (width == 0 || height == 0)
623            return;
624        if (mBuffer == null || mKeyboardChanged) {
625            mKeyboardChanged = false;
626            mDirtyRect.union(0, 0, width, height);
627        }
628        if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
629            mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
630            mCanvas = new Canvas(mBuffer);
631        }
632        final Canvas canvas = mCanvas;
633        canvas.clipRect(mDirtyRect, Op.REPLACE);
634        canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
635
636        if (mKeyboard == null) return;
637
638        if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
639            // Draw a single key.
640            onBufferDrawKey(canvas, mInvalidatedKey);
641        } else {
642            // Draw all keys.
643            for (final Key key : mKeyboard.getKeys()) {
644                onBufferDrawKey(canvas, key);
645            }
646        }
647
648        // TODO: Move this function to ProximityInfo for getting rid of
649        // public declarations for
650        // GRID_WIDTH and GRID_HEIGHT
651        if (DEBUG_KEYBOARD_GRID) {
652            Paint p = new Paint();
653            p.setStyle(Paint.Style.STROKE);
654            p.setStrokeWidth(1.0f);
655            p.setColor(0x800000c0);
656            int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1)
657                    / mKeyboard.GRID_WIDTH;
658            int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1)
659                    / mKeyboard.GRID_HEIGHT;
660            for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++)
661                canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p);
662            for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++)
663                canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p);
664        }
665
666        // Overlay a dark rectangle to dim the keyboard
667        if (mPopupMiniKeyboardPanel != null) {
668            mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
669            canvas.drawRect(0, 0, width, height, mPaint);
670        }
671
672        mInvalidatedKey = null;
673        mDrawPending = false;
674        mDirtyRect.setEmpty();
675    }
676
677    private void onBufferDrawKey(final Canvas canvas, final Key key) {
678        final Paint paint = mPaint;
679        final Drawable keyBackground = mKeyBackground;
680        final Rect padding = mPadding;
681        final int kbdPaddingLeft = getPaddingLeft();
682        final int kbdPaddingTop = getPaddingTop();
683        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
684        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
685        final int rowHeight = padding.top + key.mHeight;
686        final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
687
688        canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop);
689
690        // Draw key background.
691        final int[] drawableState = key.getCurrentDrawableState();
692        keyBackground.setState(drawableState);
693        final Rect bounds = keyBackground.getBounds();
694        if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) {
695            keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight);
696        }
697        keyBackground.draw(canvas);
698
699        // Draw key label.
700        if (key.mLabel != null) {
701            // Switch the character to uppercase if shift is pressed
702            final String label = key.mLabel == null ? null : adjustCase(key.mLabel).toString();
703            // For characters, use large font. For labels like "Done", use small font.
704            final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint);
705            final int labelCharHeight = getLabelCharHeight(labelSize, paint);
706
707            // Vertical label text alignment.
708            final float baseline;
709            if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_BOTTOM) != 0) {
710                baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR;
711                if (DEBUG_SHOW_ALIGN)
712                    drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
713                            new Paint());
714            } else { // Align center
715                final float centerY = (key.mHeight + padding.top - padding.bottom) / 2;
716                baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER;
717                if (DEBUG_SHOW_ALIGN)
718                    drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
719                            new Paint());
720            }
721            // Horizontal label text alignment
722            final int positionX;
723            if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
724                positionX = mKeyLabelHorizontalPadding + padding.left;
725                paint.setTextAlign(Align.LEFT);
726                if (DEBUG_SHOW_ALIGN)
727                    drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint());
728            } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
729                positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right;
730                paint.setTextAlign(Align.RIGHT);
731                if (DEBUG_SHOW_ALIGN)
732                    drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint());
733            } else {
734                positionX = (keyDrawWidth + padding.left - padding.right) / 2;
735                paint.setTextAlign(Align.CENTER);
736                if (DEBUG_SHOW_ALIGN) {
737                    if (label.length() > 1)
738                        drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint());
739                }
740            }
741            if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) {
742                paint.setColor(mKeyTextInactivatedColor);
743            } else {
744                paint.setColor(mKeyTextColor);
745            }
746            if (key.mEnabled) {
747                // Set a drop shadow for the text
748                paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
749            } else {
750                // Make label invisible
751                paint.setColor(Color.TRANSPARENT);
752            }
753            canvas.drawText(label, positionX, baseline, paint);
754            // Turn off drop shadow
755            paint.setShadowLayer(0, 0, 0, 0);
756        }
757
758        // Draw hint letter.
759        if (key.mHintLetter != null) {
760            final String label = key.mHintLetter.toString();
761            final int textColor;
762            final int textSize;
763            if (key.hasUppercaseLetter()) {
764                textColor = isManualTemporaryUpperCase ? mKeyUppercaseLetterActivatedColor
765                        : mKeyUppercaseLetterInactivatedColor;
766                textSize = mKeyUppercaseLetterSize;
767            } else {
768                textColor = mKeyHintLetterColor;
769                textSize = mKeyHintLetterSize;
770            }
771            paint.setColor(textColor);
772            paint.setTextSize(textSize);
773            // Note: padding.right for drawX?
774            final float drawX = keyDrawWidth - getLabelCharWidth(textSize, paint);
775            final float drawY = -paint.ascent() + padding.top;
776            canvas.drawText(label, drawX, drawY, paint);
777        }
778
779        // Draw key icon.
780        final Drawable icon = key.getIcon();
781        if (key.mLabel == null && icon != null) {
782            final int drawableWidth = icon.getIntrinsicWidth();
783            final int drawableHeight = icon.getIntrinsicHeight();
784            final int drawableX;
785            final int drawableY = (key.mHeight + padding.top - padding.bottom - drawableHeight) / 2;
786            if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
787                drawableX = padding.left + mKeyLabelHorizontalPadding;
788                if (DEBUG_SHOW_ALIGN)
789                    drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint());
790            } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
791                drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding
792                        - drawableWidth;
793                if (DEBUG_SHOW_ALIGN)
794                    drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight,
795                            0xc0808000, new Paint());
796            } else { // Align center
797                drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2;
798                if (DEBUG_SHOW_ALIGN)
799                    drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight,
800                            0xc0008080, new Paint());
801            }
802            drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight);
803            if (DEBUG_SHOW_ALIGN)
804                drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
805                        0x80c00000, new Paint());
806        }
807
808        // Draw popup hint icon "...".
809        // TODO: Draw "..." by text.
810        if (key.hasPopupHint()) {
811            final int drawableWidth = keyDrawWidth;
812            final int drawableHeight = key.mHeight;
813            final int drawableX = 0;
814            final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
815            final Drawable hintIcon = mKeyPopupHintIcon;
816            drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight);
817            if (DEBUG_SHOW_ALIGN)
818                drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
819                        0x80c0c000, new Paint());
820        }
821
822        canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop);
823    }
824
825    public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) {
826        // For characters, use large font. For labels like "Done", use small font.
827        final int labelSize;
828        final Typeface labelStyle;
829        if (label.length() > 1) {
830            labelSize = mKeyLabelSize;
831            if ((keyLabelOption & Key.LABEL_OPTION_FONT_NORMAL) != 0) {
832                labelStyle = Typeface.DEFAULT;
833            } else {
834                labelStyle = Typeface.DEFAULT_BOLD;
835            }
836        } else {
837            labelSize = mKeyLetterSize;
838            labelStyle = mKeyTextStyle;
839        }
840        paint.setTextSize(labelSize);
841        paint.setTypeface(labelStyle);
842        return labelSize;
843    }
844
845    private int getLabelCharHeight(int labelSize, Paint paint) {
846        Integer labelHeightValue = mTextHeightCache.get(labelSize);
847        final int labelCharHeight;
848        if (labelHeightValue != null) {
849            labelCharHeight = labelHeightValue;
850        } else {
851            paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
852            labelCharHeight = mTextBounds.height();
853            mTextHeightCache.put(labelSize, labelCharHeight);
854        }
855        return labelCharHeight;
856    }
857
858    private int getLabelCharWidth(int labelSize, Paint paint) {
859        Integer labelWidthValue = mTextWidthCache.get(labelSize);
860        final int labelCharWidth;
861        if (labelWidthValue != null) {
862            labelCharWidth = labelWidthValue;
863        } else {
864            paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
865            labelCharWidth = mTextBounds.width();
866            mTextWidthCache.put(labelSize, labelCharWidth);
867        }
868        return labelCharWidth;
869    }
870
871    private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
872            int height) {
873        canvas.translate(x, y);
874        icon.setBounds(0, 0, width, height);
875        icon.draw(canvas);
876        canvas.translate(-x, -y);
877    }
878
879    private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) {
880        paint.setStyle(Paint.Style.STROKE);
881        paint.setStrokeWidth(1.0f);
882        paint.setColor(color);
883        canvas.drawLine(0, y, w, y, paint);
884    }
885
886    private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) {
887        paint.setStyle(Paint.Style.STROKE);
888        paint.setStrokeWidth(1.0f);
889        paint.setColor(color);
890        canvas.drawLine(x, 0, x, h, paint);
891    }
892
893    private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color,
894            Paint paint) {
895        paint.setStyle(Paint.Style.STROKE);
896        paint.setStrokeWidth(1.0f);
897        paint.setColor(color);
898        canvas.translate(x, y);
899        canvas.drawRect(0, 0, w, h, paint);
900        canvas.translate(-x, -y);
901    }
902
903    public void setForeground(boolean foreground) {
904        mInForeground = foreground;
905    }
906
907    // TODO: clean up this method.
908    private void dismissAllKeyPreviews() {
909        for (PointerTracker tracker : mPointerTrackers) {
910            tracker.setReleasedKeyGraphics();
911            dismissKeyPreview(tracker);
912        }
913    }
914
915    @Override
916    public void showKeyPreview(int keyIndex, PointerTracker tracker) {
917        if (mShowKeyPreviewPopup) {
918            mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
919        } else if (mKeyboard.needSpacebarPreview(keyIndex)) {
920            // Show key preview (in this case, slide language switcher) without any delay.
921            showKey(keyIndex, tracker);
922        }
923    }
924
925    @Override
926    public void dismissKeyPreview(PointerTracker tracker) {
927        if (mShowKeyPreviewPopup) {
928            mHandler.cancelShowKeyPreview(tracker);
929            mHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
930        } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) {
931            // Dismiss key preview (in this case, slide language switcher) without any delay.
932            mPreviewText.setVisibility(View.INVISIBLE);
933        }
934        // Clear key preview display position.
935        mKeyPreviewPopupDisplayedY = -1;
936    }
937
938    private void addKeyPreview(TextView keyPreview) {
939        ViewGroup placer = mPreviewPlacer;
940        if (placer == null) {
941            final FrameLayout screenContent = (FrameLayout) getRootView().findViewById(
942                    android.R.id.content);
943            if (android.os.Build.VERSION.SDK_INT >= /* HONEYCOMB */11) {
944                placer = screenContent;
945            } else {
946                // Insert LinearLayout to be able to setMargin because pre-Honeycomb FrameLayout
947                // could not handle setMargin properly.
948                placer = new LinearLayout(getContext());
949                screenContent.addView(placer);
950            }
951            mPreviewPlacer = placer;
952        }
953        if (placer instanceof FrameLayout) {
954            // Honeycomb or later.
955            placer.addView(keyPreview, new FrameLayout.LayoutParams(0, 0));
956        } else {
957            // Gingerbread or ealier.
958            placer.addView(keyPreview, new LinearLayout.LayoutParams(0, 0));
959        }
960    }
961
962    // TODO: Introduce minimum duration for displaying key previews
963    // TODO: Display up to two key previews when the user presses two keys at the same time
964    private void showKey(final int keyIndex, PointerTracker tracker) {
965        final TextView previewText = mPreviewText;
966        // If the key preview has no parent view yet, add it to the ViewGroup which can place
967        // key preview absolutely in SoftInputWindow.
968        if (previewText.getParent() == null) {
969            addKeyPreview(previewText);
970        }
971
972        final Key key = tracker.getKey(keyIndex);
973        // If keyIndex is invalid or IME is already closed, we must not show key preview.
974        // Trying to show key preview while root window is closed causes
975        // WindowManager.BadTokenException.
976        if (key == null || !mInForeground)
977            return;
978
979        mHandler.cancelAllDismissKeyPreviews();
980
981        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
982        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
983        // What we show as preview should match what we show on key top in onBufferDraw().
984        if (key.mLabel != null) {
985            // TODO Should take care of temporaryShiftLabel here.
986            previewText.setCompoundDrawables(null, null, null, null);
987            previewText.setText(adjustCase(tracker.getPreviewText(key)));
988            if (key.mLabel.length() > 1) {
989                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
990                previewText.setTypeface(Typeface.DEFAULT_BOLD);
991            } else {
992                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSize);
993                previewText.setTypeface(mKeyTextStyle);
994            }
995        } else {
996            final Drawable previewIcon = key.getPreviewIcon();
997            previewText.setCompoundDrawables(null, null, null,
998                   previewIcon != null ? previewIcon : key.getIcon());
999            previewText.setText(null);
1000        }
1001        // Set the preview background state
1002        previewText.getBackground().setState(
1003                key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
1004
1005        previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1006                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
1007        final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
1008                + previewText.getPaddingLeft() + previewText.getPaddingRight());
1009        final int previewHeight = mPreviewHeight;
1010        getLocationInWindow(mCoordinates);
1011        final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
1012        final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset;
1013        // Record key preview position to display mini-keyboard later at the same position
1014        mKeyPreviewPopupDisplayedY = previewY;
1015
1016        // Place the key preview.
1017        // TODO: Adjust position of key previews which touch screen edges
1018        final MarginLayoutParams lp = (MarginLayoutParams)previewText.getLayoutParams();
1019        lp.width = previewWidth;
1020        lp.height = previewHeight;
1021        lp.setMargins(previewX, previewY, 0, 0);
1022        previewText.setVisibility(VISIBLE);
1023    }
1024
1025    /**
1026     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1027     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1028     * draws the cached buffer.
1029     * @see #invalidateKey(Key)
1030     */
1031    public void invalidateAllKeys() {
1032        mDirtyRect.union(0, 0, getWidth(), getHeight());
1033        mDrawPending = true;
1034        invalidate();
1035    }
1036
1037    /**
1038     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1039     * one key is changing it's content. Any changes that affect the position or size of the key
1040     * may not be honored.
1041     * @param key key in the attached {@link Keyboard}.
1042     * @see #invalidateAllKeys
1043     */
1044    @Override
1045    public void invalidateKey(Key key) {
1046        if (key == null)
1047            return;
1048        mInvalidatedKey = key;
1049        final int x = key.mX + getPaddingLeft();
1050        final int y = key.mY + getPaddingTop();
1051        mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
1052        mDirtyRect.union(mInvalidatedKeyRect);
1053        onBufferDraw();
1054        invalidate(mInvalidatedKeyRect);
1055    }
1056
1057    private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
1058        // Check if we have a popup layout specified first.
1059        if (mPopupLayout == 0) {
1060            return false;
1061        }
1062
1063        final Key parentKey = tracker.getKey(keyIndex);
1064        if (parentKey == null)
1065            return false;
1066        boolean result = onLongPress(parentKey, tracker);
1067        if (result) {
1068            dismissAllKeyPreviews();
1069            tracker.onLongPressed(mPointerQueue);
1070        }
1071        return result;
1072    }
1073
1074    private void onLongPressShiftKey(PointerTracker tracker) {
1075        tracker.onLongPressed(mPointerQueue);
1076        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1077    }
1078
1079    private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
1080        // When shift key is double tapped, the first tap is correctly processed as usual tap. And
1081        // the second tap is treated as this double tap event, so that we need not mark tracker
1082        // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
1083        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1084    }
1085
1086    // This default implementation returns a popup mini keyboard panel.
1087    // A derived class may return a language switcher popup panel, for instance.
1088    protected PopupPanel onCreatePopupPanel(Key parentKey) {
1089        if (parentKey.mPopupCharacters == null)
1090            return null;
1091
1092        final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
1093        if (container == null)
1094            throw new NullPointerException();
1095
1096        final PopupMiniKeyboardView miniKeyboardView =
1097                (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
1098        miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
1099            @Override
1100            public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1101                mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
1102                dismissMiniKeyboard();
1103            }
1104
1105            @Override
1106            public void onTextInput(CharSequence text) {
1107                mKeyboardActionListener.onTextInput(text);
1108                dismissMiniKeyboard();
1109            }
1110
1111            @Override
1112            public void onCancelInput() {
1113                mKeyboardActionListener.onCancelInput();
1114                dismissMiniKeyboard();
1115            }
1116
1117            @Override
1118            public void onSwipeDown() {
1119                // Nothing to do.
1120            }
1121            @Override
1122            public void onPress(int primaryCode, boolean withSliding) {
1123                mKeyboardActionListener.onPress(primaryCode, withSliding);
1124            }
1125            @Override
1126            public void onRelease(int primaryCode, boolean withSliding) {
1127                mKeyboardActionListener.onRelease(primaryCode, withSliding);
1128            }
1129        });
1130
1131        final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
1132                parentKey, mKeyboard).build();
1133        miniKeyboardView.setKeyboard(keyboard);
1134
1135        container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1136                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1137
1138        return miniKeyboardView;
1139    }
1140
1141    /**
1142     * Called when a key is long pressed. By default this will open mini keyboard associated
1143     * with this key.
1144     * @param parentKey the key that was long pressed
1145     * @param tracker the pointer tracker which pressed the parent key
1146     * @return true if the long press is handled, false otherwise. Subclasses should call the
1147     * method on the base class if the subclass doesn't wish to handle the call.
1148     */
1149    protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
1150        PopupPanel popupPanel = mPopupPanelCache.get(parentKey);
1151        if (popupPanel == null) {
1152            popupPanel = onCreatePopupPanel(parentKey);
1153            if (popupPanel == null)
1154                return false;
1155            mPopupPanelCache.put(parentKey, popupPanel);
1156        }
1157        if (mPopupWindow == null) {
1158            mPopupWindow = new PopupWindow(getContext());
1159            mPopupWindow.setBackgroundDrawable(null);
1160            mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation);
1161            // Allow popup window to be drawn off the screen.
1162            mPopupWindow.setClippingEnabled(false);
1163        }
1164        mPopupMiniKeyboardPanel = popupPanel;
1165        popupPanel.showPanel(this, parentKey, tracker, mKeyPreviewPopupDisplayedY, mPopupWindow);
1166
1167        invalidateAllKeys();
1168        return true;
1169    }
1170
1171    private PointerTracker getPointerTracker(final int id) {
1172        final ArrayList<PointerTracker> pointers = mPointerTrackers;
1173        final KeyboardActionListener listener = mKeyboardActionListener;
1174
1175        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
1176        for (int i = pointers.size(); i <= id; i++) {
1177            final PointerTracker tracker =
1178                new PointerTracker(i, this, mHandler, mKeyDetector, this);
1179            if (mKeyboard != null)
1180                tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance);
1181            if (listener != null)
1182                tracker.setOnKeyboardActionListener(listener);
1183            pointers.add(tracker);
1184        }
1185
1186        return pointers.get(id);
1187    }
1188
1189    public boolean isInSlidingKeyInput() {
1190        if (mPopupMiniKeyboardPanel != null) {
1191            return mPopupMiniKeyboardPanel.isInSlidingKeyInput();
1192        } else {
1193            return mPointerQueue.isInSlidingKeyInput();
1194        }
1195    }
1196
1197    public int getPointerCount() {
1198        return mOldPointerCount;
1199    }
1200
1201    @Override
1202    public boolean onTouchEvent(MotionEvent me) {
1203        final int action = me.getActionMasked();
1204        final int pointerCount = me.getPointerCount();
1205        final int oldPointerCount = mOldPointerCount;
1206        mOldPointerCount = pointerCount;
1207
1208        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1209        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1210        // events except a transition from/to single-touch.
1211        if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
1212            return true;
1213        }
1214
1215        // Track the last few movements to look for spurious swipes.
1216        mSwipeTracker.addMovement(me);
1217
1218        // Gesture detector must be enabled only when mini-keyboard is not on the screen.
1219        if (mPopupMiniKeyboardPanel == null && mGestureDetector != null
1220                && mGestureDetector.onTouchEvent(me)) {
1221            dismissAllKeyPreviews();
1222            mHandler.cancelKeyTimers();
1223            return true;
1224        }
1225
1226        final long eventTime = me.getEventTime();
1227        final int index = me.getActionIndex();
1228        final int id = me.getPointerId(index);
1229        final int x = (int)me.getX(index);
1230        final int y = (int)me.getY(index);
1231
1232        // Needs to be called after the gesture detector gets a turn, as it may have displayed the
1233        // mini keyboard
1234        if (mPopupMiniKeyboardPanel != null) {
1235            return mPopupMiniKeyboardPanel.onTouchEvent(me);
1236        }
1237
1238        if (mHandler.isInKeyRepeat()) {
1239            final PointerTracker tracker = getPointerTracker(id);
1240            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1241            // event (UP or DOWN) is non-modifier key.
1242            if (pointerCount > 1 && !tracker.isModifier()) {
1243                mHandler.cancelKeyRepeatTimer();
1244            }
1245            // Up event will pass through.
1246        }
1247
1248        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1249        // Translate mutli-touch event to single-touch events on the device that has no distinct
1250        // multi-touch panel.
1251        if (!mHasDistinctMultitouch) {
1252            // Use only main (id=0) pointer tracker.
1253            PointerTracker tracker = getPointerTracker(0);
1254            if (pointerCount == 1 && oldPointerCount == 2) {
1255                // Multi-touch to single touch transition.
1256                // Send a down event for the latest pointer if the key is different from the
1257                // previous key.
1258                final int newKeyIndex = tracker.getKeyIndexOn(x, y);
1259                if (mOldKeyIndex != newKeyIndex) {
1260                    tracker.onDownEvent(x, y, eventTime, null);
1261                    if (action == MotionEvent.ACTION_UP)
1262                        tracker.onUpEvent(x, y, eventTime, null);
1263                }
1264            } else if (pointerCount == 2 && oldPointerCount == 1) {
1265                // Single-touch to multi-touch transition.
1266                // Send an up event for the last pointer.
1267                final int lastX = tracker.getLastX();
1268                final int lastY = tracker.getLastY();
1269                mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
1270                tracker.onUpEvent(lastX, lastY, eventTime, null);
1271            } else if (pointerCount == 1 && oldPointerCount == 1) {
1272                tracker.onTouchEvent(action, x, y, eventTime, null);
1273            } else {
1274                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1275                        + " (old " + oldPointerCount + ")");
1276            }
1277            return true;
1278        }
1279
1280        final PointerTrackerQueue queue = mPointerQueue;
1281        if (action == MotionEvent.ACTION_MOVE) {
1282            for (int i = 0; i < pointerCount; i++) {
1283                final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
1284                tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
1285            }
1286        } else {
1287            final PointerTracker tracker = getPointerTracker(id);
1288            switch (action) {
1289            case MotionEvent.ACTION_DOWN:
1290            case MotionEvent.ACTION_POINTER_DOWN:
1291                tracker.onDownEvent(x, y, eventTime, queue);
1292                break;
1293            case MotionEvent.ACTION_UP:
1294            case MotionEvent.ACTION_POINTER_UP:
1295                tracker.onUpEvent(x, y, eventTime, queue);
1296                break;
1297            case MotionEvent.ACTION_CANCEL:
1298                tracker.onCancelEvent(x, y, eventTime, queue);
1299                break;
1300            }
1301        }
1302
1303        return true;
1304    }
1305
1306    protected void onSwipeDown() {
1307        mKeyboardActionListener.onSwipeDown();
1308    }
1309
1310    public void closing() {
1311        mPreviewText.setVisibility(View.GONE);
1312        mHandler.cancelAllMessages();
1313
1314        dismissMiniKeyboard();
1315        mDirtyRect.union(0, 0, getWidth(), getHeight());
1316        mPopupPanelCache.clear();
1317        requestLayout();
1318    }
1319
1320    public void purgeKeyboardAndClosing() {
1321        mKeyboard = null;
1322        closing();
1323    }
1324
1325    @Override
1326    public void onDetachedFromWindow() {
1327        super.onDetachedFromWindow();
1328        closing();
1329    }
1330
1331    private boolean dismissMiniKeyboard() {
1332        if (mPopupWindow != null && mPopupWindow.isShowing()) {
1333            mPopupWindow.dismiss();
1334            mPopupMiniKeyboardPanel = null;
1335            invalidateAllKeys();
1336            return true;
1337        }
1338        return false;
1339    }
1340
1341    public boolean handleBack() {
1342        return dismissMiniKeyboard();
1343    }
1344}
1345