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