MainKeyboardView.java revision 212165b0b8308802a461a6a526d367ba67b5567a
1/*
2 * Copyright (C) 2011 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.animation.AnimatorInflater;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.content.pm.PackageManager;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
28import android.graphics.Paint.Align;
29import android.graphics.Typeface;
30import android.graphics.drawable.Drawable;
31import android.os.Message;
32import android.os.SystemClock;
33import android.preference.PreferenceManager;
34import android.util.AttributeSet;
35import android.util.DisplayMetrics;
36import android.util.Log;
37import android.util.SparseArray;
38import android.util.TypedValue;
39import android.view.LayoutInflater;
40import android.view.MotionEvent;
41import android.view.View;
42import android.view.ViewConfiguration;
43import android.view.ViewGroup;
44import android.view.inputmethod.InputMethodSubtype;
45import android.widget.TextView;
46
47import com.android.inputmethod.accessibility.AccessibilityUtils;
48import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
49import com.android.inputmethod.annotations.ExternallyReferenced;
50import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
51import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
52import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText;
53import com.android.inputmethod.keyboard.internal.GestureTrailsPreview;
54import com.android.inputmethod.keyboard.internal.KeyDrawParams;
55import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
56import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
57import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview;
58import com.android.inputmethod.keyboard.internal.TouchScreenRegulator;
59import com.android.inputmethod.latin.Constants;
60import com.android.inputmethod.latin.LatinImeLogger;
61import com.android.inputmethod.latin.R;
62import com.android.inputmethod.latin.SuggestedWords;
63import com.android.inputmethod.latin.define.ProductionFlag;
64import com.android.inputmethod.latin.settings.DebugSettings;
65import com.android.inputmethod.latin.utils.CollectionUtils;
66import com.android.inputmethod.latin.utils.CoordinateUtils;
67import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
68import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
69import com.android.inputmethod.latin.utils.TypefaceUtils;
70import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils;
71import com.android.inputmethod.latin.utils.ViewLayoutUtils;
72import com.android.inputmethod.research.ResearchLogger;
73
74import java.util.WeakHashMap;
75
76/**
77 * A view that is responsible for detecting key presses and touch movements.
78 *
79 * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled
80 * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon
81 * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio
82 * @attr ref R.styleable#MainKeyboardView_spacebarTextColor
83 * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor
84 * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha
85 * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator
86 * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator
87 * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator
88 * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance
89 * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime
90 * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance
91 * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable
92 * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout
93 * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval
94 * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout
95 * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout
96 * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout
97 * @attr ref R.styleable#MainKeyboardView_keyPreviewLayout
98 * @attr ref R.styleable#MainKeyboardView_keyPreviewOffset
99 * @attr ref R.styleable#MainKeyboardView_keyPreviewHeight
100 * @attr ref R.styleable#MainKeyboardView_keyPreviewLingerTimeout
101 * @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardLayout
102 * @attr ref R.styleable#MainKeyboardView_backgroundDimAlpha
103 * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint
104 * @attr ref R.styleable#MainKeyboardView_gestureFloatingPreviewTextLingerTimeout
105 * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping
106 * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold
107 * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration
108 * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom
109 * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo
110 * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom
111 * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo
112 * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance
113 * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime
114 * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold
115 * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
116 */
117public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
118        PointerTracker.DrawingProxy, MoreKeysPanel.Controller,
119        TouchScreenRegulator.ProcessMotionEvent {
120    private static final String TAG = MainKeyboardView.class.getSimpleName();
121
122    // TODO: Kill process when the usability study mode was changed.
123    private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
124
125    /** Listener for {@link KeyboardActionListener}. */
126    private KeyboardActionListener mKeyboardActionListener;
127
128    /* Space key and its icons */
129    private Key mSpaceKey;
130    private Drawable mSpaceIcon;
131    // Stuff to draw language name on spacebar.
132    private final int mLanguageOnSpacebarFinalAlpha;
133    private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
134    private boolean mNeedsToDisplayLanguage;
135    private boolean mHasMultipleEnabledIMEsOrSubtypes;
136    private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
137    private final float mSpacebarTextRatio;
138    private float mSpacebarTextSize;
139    private final int mSpacebarTextColor;
140    private final int mSpacebarTextShadowColor;
141    // The minimum x-scale to fit the language name on spacebar.
142    private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
143    // Stuff to draw auto correction LED on spacebar.
144    private boolean mAutoCorrectionSpacebarLedOn;
145    private final boolean mAutoCorrectionSpacebarLedEnabled;
146    private final Drawable mAutoCorrectionSpacebarLedIcon;
147    private static final int SPACE_LED_LENGTH_PERCENT = 80;
148
149    // Stuff to draw altCodeWhileTyping keys.
150    private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
151    private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
152    private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
153
154    // Preview placer view
155    private final PreviewPlacerView mPreviewPlacerView;
156    private final int[] mOriginCoords = CoordinateUtils.newInstance();
157    private final GestureFloatingPreviewText mGestureFloatingPreviewText;
158    private final GestureTrailsPreview mGestureTrailsPreview;
159    private final SlidingKeyInputPreview mSlidingKeyInputPreview;
160
161    // Key preview
162    private static final int PREVIEW_ALPHA = 240;
163    private final int mKeyPreviewLayoutId;
164    private final int mKeyPreviewOffset;
165    private final int mKeyPreviewHeight;
166    private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
167    private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
168    private boolean mShowKeyPreviewPopup = true;
169    private int mKeyPreviewLingerTimeout;
170
171    // More keys keyboard
172    private final Paint mBackgroundDimAlphaPaint = new Paint();
173    private boolean mNeedsToDimEntireKeyboard;
174    private final View mMoreKeysKeyboardContainer;
175    private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache =
176            CollectionUtils.newWeakHashMap();
177    private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
178    // More keys panel (used by both more keys keyboard and more suggestions view)
179    // TODO: Consider extending to support multiple more keys panels
180    private MoreKeysPanel mMoreKeysPanel;
181
182    // Gesture floating preview text
183    // TODO: Make this parameter customizable by user via settings.
184    private int mGestureFloatingPreviewTextLingerTimeout;
185
186    private final TouchScreenRegulator mTouchScreenRegulator;
187
188    private KeyDetector mKeyDetector;
189    private final boolean mHasDistinctMultitouch;
190    private int mOldPointerCount = 1;
191    private Key mOldKey;
192
193    private final KeyTimerHandler mKeyTimerHandler;
194
195    private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
196            implements TimerProxy {
197        private static final int MSG_TYPING_STATE_EXPIRED = 0;
198        private static final int MSG_REPEAT_KEY = 1;
199        private static final int MSG_LONGPRESS_KEY = 2;
200        private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
201        private static final int MSG_UPDATE_BATCH_INPUT = 4;
202
203        private final int mKeyRepeatStartTimeout;
204        private final int mKeyRepeatInterval;
205        private final int mIgnoreAltCodeKeyTimeout;
206        private final int mGestureRecognitionUpdateTime;
207
208        public KeyTimerHandler(final MainKeyboardView outerInstance,
209                final TypedArray mainKeyboardViewAttr) {
210            super(outerInstance);
211
212            mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
213                    R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
214            mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
215                    R.styleable.MainKeyboardView_keyRepeatInterval, 0);
216            mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
217                    R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
218            mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
219                    R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0);
220        }
221
222        @Override
223        public void handleMessage(final Message msg) {
224            final MainKeyboardView keyboardView = getOuterInstance();
225            if (keyboardView == null) {
226                return;
227            }
228            final PointerTracker tracker = (PointerTracker) msg.obj;
229            switch (msg.what) {
230            case MSG_TYPING_STATE_EXPIRED:
231                startWhileTypingFadeinAnimation(keyboardView);
232                break;
233            case MSG_REPEAT_KEY:
234                final Key currentKey = tracker.getKey();
235                final int code = msg.arg1;
236                if (currentKey != null && currentKey.mCode == code) {
237                    startKeyRepeatTimer(tracker, mKeyRepeatInterval);
238                    startTypingStateTimer(currentKey);
239                    final KeyboardActionListener listener =
240                            keyboardView.getKeyboardActionListener();
241                    listener.onPressKey(code, true /* isRepeatKey */, true /* isSinglePointer */);
242                    listener.onCodeInput(code,
243                            Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
244                }
245                break;
246            case MSG_LONGPRESS_KEY:
247                keyboardView.onLongPress(tracker);
248                break;
249            case MSG_UPDATE_BATCH_INPUT:
250                tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
251                startUpdateBatchInputTimer(tracker);
252                break;
253            }
254        }
255
256        private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) {
257            final Key key = tracker.getKey();
258            if (key == null) {
259                return;
260            }
261            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
262        }
263
264        @Override
265        public void startKeyRepeatTimer(final PointerTracker tracker) {
266            startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
267        }
268
269        public void cancelKeyRepeatTimer() {
270            removeMessages(MSG_REPEAT_KEY);
271        }
272
273        // TODO: Suppress layout changes in key repeat mode
274        public boolean isInKeyRepeat() {
275            return hasMessages(MSG_REPEAT_KEY);
276        }
277
278        @Override
279        public void startLongPressTimer(final PointerTracker tracker, final int delay) {
280            cancelLongPressTimer();
281            if (delay <= 0) return;
282            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
283        }
284
285        @Override
286        public void cancelLongPressTimer() {
287            removeMessages(MSG_LONGPRESS_KEY);
288        }
289
290        private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
291                final ObjectAnimator animatorToStart) {
292            if (animatorToCancel == null || animatorToStart == null) {
293                // TODO: Stop using null as a no-operation animator.
294                return;
295            }
296            float startFraction = 0.0f;
297            if (animatorToCancel.isStarted()) {
298                animatorToCancel.cancel();
299                startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
300            }
301            final long startTime = (long)(animatorToStart.getDuration() * startFraction);
302            animatorToStart.start();
303            animatorToStart.setCurrentPlayTime(startTime);
304        }
305
306        private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) {
307            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
308                    keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
309        }
310
311        private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) {
312            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
313                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
314        }
315
316        @Override
317        public void startTypingStateTimer(final Key typedKey) {
318            if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
319                return;
320            }
321
322            final boolean isTyping = isTypingState();
323            removeMessages(MSG_TYPING_STATE_EXPIRED);
324            final MainKeyboardView keyboardView = getOuterInstance();
325
326            // When user hits the space or the enter key, just cancel the while-typing timer.
327            final int typedCode = typedKey.mCode;
328            if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
329                if (isTyping) {
330                    startWhileTypingFadeinAnimation(keyboardView);
331                }
332                return;
333            }
334
335            sendMessageDelayed(
336                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
337            if (isTyping) {
338                return;
339            }
340            startWhileTypingFadeoutAnimation(keyboardView);
341        }
342
343        @Override
344        public boolean isTypingState() {
345            return hasMessages(MSG_TYPING_STATE_EXPIRED);
346        }
347
348        @Override
349        public void startDoubleTapShiftKeyTimer() {
350            sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
351                    ViewConfiguration.getDoubleTapTimeout());
352        }
353
354        @Override
355        public void cancelDoubleTapShiftKeyTimer() {
356            removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
357        }
358
359        @Override
360        public boolean isInDoubleTapShiftKeyTimeout() {
361            return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
362        }
363
364        @Override
365        public void cancelKeyTimers() {
366            cancelKeyRepeatTimer();
367            cancelLongPressTimer();
368        }
369
370        @Override
371        public void startUpdateBatchInputTimer(final PointerTracker tracker) {
372            if (mGestureRecognitionUpdateTime <= 0) {
373                return;
374            }
375            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
376            sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker),
377                    mGestureRecognitionUpdateTime);
378        }
379
380        @Override
381        public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
382            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
383        }
384
385        @Override
386        public void cancelAllUpdateBatchInputTimers() {
387            removeMessages(MSG_UPDATE_BATCH_INPUT);
388        }
389
390        public void cancelAllMessages() {
391            cancelKeyTimers();
392            cancelAllUpdateBatchInputTimers();
393        }
394    }
395
396    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
397
398    public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> {
399        private static final int MSG_DISMISS_KEY_PREVIEW = 0;
400        private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
401
402        public DrawingHandler(final MainKeyboardView outerInstance) {
403            super(outerInstance);
404        }
405
406        @Override
407        public void handleMessage(final Message msg) {
408            final MainKeyboardView mainKeyboardView = getOuterInstance();
409            if (mainKeyboardView == null) return;
410            final PointerTracker tracker = (PointerTracker) msg.obj;
411            switch (msg.what) {
412            case MSG_DISMISS_KEY_PREVIEW:
413                final TextView previewText = mainKeyboardView.mKeyPreviewTexts.get(
414                        tracker.mPointerId);
415                if (previewText != null) {
416                    previewText.setVisibility(INVISIBLE);
417                }
418                break;
419            case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
420                mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY);
421                break;
422            }
423        }
424
425        public void dismissKeyPreview(final long delay, final PointerTracker tracker) {
426            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
427        }
428
429        public void cancelDismissKeyPreview(final PointerTracker tracker) {
430            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
431        }
432
433        private void cancelAllDismissKeyPreviews() {
434            removeMessages(MSG_DISMISS_KEY_PREVIEW);
435        }
436
437        public void dismissGestureFloatingPreviewText(final long delay) {
438            sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay);
439        }
440
441        public void cancelAllMessages() {
442            cancelAllDismissKeyPreviews();
443        }
444    }
445
446    public MainKeyboardView(final Context context, final AttributeSet attrs) {
447        this(context, attrs, R.attr.mainKeyboardViewStyle);
448    }
449
450    public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
451        super(context, attrs, defStyle);
452
453        mTouchScreenRegulator = new TouchScreenRegulator(context, this);
454
455        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
456        final boolean forceNonDistinctMultitouch = prefs.getBoolean(
457                DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false);
458        final boolean hasDistinctMultitouch = context.getPackageManager()
459                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
460        mHasDistinctMultitouch = hasDistinctMultitouch && !forceNonDistinctMultitouch;
461        PointerTracker.init(getResources());
462        mPreviewPlacerView = new PreviewPlacerView(context, attrs);
463
464        final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes(
465                attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
466        final int backgroundDimAlpha = mainKeyboardViewAttr.getInt(
467                R.styleable.MainKeyboardView_backgroundDimAlpha, 0);
468        mBackgroundDimAlphaPaint.setColor(Color.BLACK);
469        mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha);
470        mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean(
471                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false);
472        mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable(
473                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon);
474        mSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
475                R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f);
476        mSpacebarTextColor = mainKeyboardViewAttr.getColor(
477                R.styleable.MainKeyboardView_spacebarTextColor, 0);
478        mSpacebarTextShadowColor = mainKeyboardViewAttr.getColor(
479                R.styleable.MainKeyboardView_spacebarTextShadowColor, 0);
480        mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt(
481                R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
482                Constants.Color.ALPHA_OPAQUE);
483        final int languageOnSpacebarFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
484                R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
485        final int altCodeKeyWhileTypingFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
486                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
487        final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId(
488                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
489
490        final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension(
491                R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f);
492        final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension(
493                R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
494        mKeyDetector = new KeyDetector(
495                keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
496        mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr);
497        mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
498                R.styleable.MainKeyboardView_keyPreviewOffset, 0);
499        mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
500                R.styleable.MainKeyboardView_keyPreviewHeight, 0);
501        mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt(
502                R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0);
503        mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId(
504                R.styleable.MainKeyboardView_keyPreviewLayout, 0);
505        if (mKeyPreviewLayoutId == 0) {
506            mShowKeyPreviewPopup = false;
507        }
508        final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId(
509                R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0);
510        mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean(
511                R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
512
513        mGestureFloatingPreviewTextLingerTimeout = mainKeyboardViewAttr.getInt(
514                R.styleable.MainKeyboardView_gestureFloatingPreviewTextLingerTimeout, 0);
515        PointerTracker.setParameters(mainKeyboardViewAttr);
516
517        mGestureFloatingPreviewText = new GestureFloatingPreviewText(
518                mPreviewPlacerView, mainKeyboardViewAttr);
519        mPreviewPlacerView.addPreview(mGestureFloatingPreviewText);
520
521        mGestureTrailsPreview = new GestureTrailsPreview(
522                mPreviewPlacerView, mainKeyboardViewAttr);
523        mPreviewPlacerView.addPreview(mGestureTrailsPreview);
524
525        mSlidingKeyInputPreview = new SlidingKeyInputPreview(
526                mPreviewPlacerView, mainKeyboardViewAttr);
527        mPreviewPlacerView.addPreview(mSlidingKeyInputPreview);
528        mainKeyboardViewAttr.recycle();
529
530        mMoreKeysKeyboardContainer = LayoutInflater.from(getContext())
531                .inflate(moreKeysKeyboardLayoutId, null);
532        mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
533                languageOnSpacebarFadeoutAnimatorResId, this);
534        mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
535                altCodeKeyWhileTypingFadeoutAnimatorResId, this);
536        mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
537                altCodeKeyWhileTypingFadeinAnimatorResId, this);
538
539        mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
540    }
541
542    private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
543        if (resId == 0) {
544            // TODO: Stop returning null.
545            return null;
546        }
547        final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
548                getContext(), resId);
549        if (animator != null) {
550            animator.setTarget(target);
551        }
552        return animator;
553    }
554
555    @ExternallyReferenced
556    public int getLanguageOnSpacebarAnimAlpha() {
557        return mLanguageOnSpacebarAnimAlpha;
558    }
559
560    @ExternallyReferenced
561    public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
562        mLanguageOnSpacebarAnimAlpha = alpha;
563        invalidateKey(mSpaceKey);
564    }
565
566    @ExternallyReferenced
567    public int getAltCodeKeyWhileTypingAnimAlpha() {
568        return mAltCodeKeyWhileTypingAnimAlpha;
569    }
570
571    @ExternallyReferenced
572    public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) {
573        if (mAltCodeKeyWhileTypingAnimAlpha == alpha) {
574            return;
575        }
576        // Update the visual of alt-code-key-while-typing.
577        mAltCodeKeyWhileTypingAnimAlpha = alpha;
578        final Keyboard keyboard = getKeyboard();
579        if (keyboard == null) {
580            return;
581        }
582        for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
583            invalidateKey(key);
584        }
585    }
586
587    public void setKeyboardActionListener(final KeyboardActionListener listener) {
588        mKeyboardActionListener = listener;
589        PointerTracker.setKeyboardActionListener(listener);
590    }
591
592    /**
593     * Returns the {@link KeyboardActionListener} object.
594     * @return the listener attached to this keyboard
595     */
596    @Override
597    public KeyboardActionListener getKeyboardActionListener() {
598        return mKeyboardActionListener;
599    }
600
601    @Override
602    public KeyDetector getKeyDetector() {
603        return mKeyDetector;
604    }
605
606    @Override
607    public DrawingProxy getDrawingProxy() {
608        return this;
609    }
610
611    @Override
612    public TimerProxy getTimerProxy() {
613        return mKeyTimerHandler;
614    }
615
616    /**
617     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
618     * view will re-layout itself to accommodate the keyboard.
619     * @see Keyboard
620     * @see #getKeyboard()
621     * @param keyboard the keyboard to display in this view
622     */
623    @Override
624    public void setKeyboard(final Keyboard keyboard) {
625        // Remove any pending messages, except dismissing preview and key repeat.
626        mKeyTimerHandler.cancelLongPressTimer();
627        super.setKeyboard(keyboard);
628        mKeyDetector.setKeyboard(
629                keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
630        PointerTracker.setKeyDetector(mKeyDetector);
631        mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth);
632        mMoreKeysKeyboardCache.clear();
633
634        mSpaceKey = keyboard.getKey(Constants.CODE_SPACE);
635        mSpaceIcon = (mSpaceKey != null)
636                ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
637        final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
638        mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
639        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
640            ResearchLogger.mainKeyboardView_setKeyboard(keyboard);
641        }
642
643        // This always needs to be set since the accessibility state can
644        // potentially change without the keyboard being set again.
645        AccessibleKeyboardViewProxy.getInstance().setKeyboard();
646    }
647
648    /**
649     * Enables or disables the key feedback popup. This is a popup that shows a magnified
650     * version of the depressed key. By default the preview is enabled.
651     * @param previewEnabled whether or not to enable the key feedback preview
652     * @param delay the delay after which the preview is dismissed
653     * @see #isKeyPreviewPopupEnabled()
654     */
655    public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
656        mShowKeyPreviewPopup = previewEnabled;
657        mKeyPreviewLingerTimeout = delay;
658    }
659
660
661    private void locatePreviewPlacerView() {
662        if (mPreviewPlacerView.getParent() != null) {
663            return;
664        }
665        final int width = getWidth();
666        final int height = getHeight();
667        if (width == 0 || height == 0) {
668            // In transient state.
669            return;
670        }
671        getLocationInWindow(mOriginCoords);
672        final DisplayMetrics dm = getResources().getDisplayMetrics();
673        if (CoordinateUtils.y(mOriginCoords) < dm.heightPixels / 4) {
674            // In transient state.
675            return;
676        }
677        final View rootView = getRootView();
678        if (rootView == null) {
679            Log.w(TAG, "Cannot find root view");
680            return;
681        }
682        final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
683        // Note: It'd be very weird if we get null by android.R.id.content.
684        if (windowContentView == null) {
685            Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView");
686        } else {
687            windowContentView.addView(mPreviewPlacerView);
688            mPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, width, height);
689        }
690    }
691
692    /**
693     * Returns the enabled state of the key feedback preview
694     * @return whether or not the key feedback preview is enabled
695     * @see #setKeyPreviewPopupEnabled(boolean, int)
696     */
697    public boolean isKeyPreviewPopupEnabled() {
698        return mShowKeyPreviewPopup;
699    }
700
701    private void addKeyPreview(final TextView keyPreview) {
702        locatePreviewPlacerView();
703        mPreviewPlacerView.addView(
704                keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
705    }
706
707    private TextView getKeyPreviewText(final int pointerId) {
708        TextView previewText = mKeyPreviewTexts.get(pointerId);
709        if (previewText != null) {
710            return previewText;
711        }
712        final Context context = getContext();
713        if (mKeyPreviewLayoutId != 0) {
714            previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
715        } else {
716            previewText = new TextView(context);
717        }
718        mKeyPreviewTexts.put(pointerId, previewText);
719        return previewText;
720    }
721
722    private void dismissAllKeyPreviews() {
723        final int pointerCount = mKeyPreviewTexts.size();
724        for (int id = 0; id < pointerCount; id++) {
725            final TextView previewText = mKeyPreviewTexts.get(id);
726            if (previewText != null) {
727                previewText.setVisibility(INVISIBLE);
728            }
729        }
730        PointerTracker.setReleasedKeyGraphicsToAllKeys();
731    }
732
733    // Background state set
734    private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
735        { // STATE_MIDDLE
736            EMPTY_STATE_SET,
737            { R.attr.state_has_morekeys }
738        },
739        { // STATE_LEFT
740            { R.attr.state_left_edge },
741            { R.attr.state_left_edge, R.attr.state_has_morekeys }
742        },
743        { // STATE_RIGHT
744            { R.attr.state_right_edge },
745            { R.attr.state_right_edge, R.attr.state_has_morekeys }
746        }
747    };
748    private static final int STATE_MIDDLE = 0;
749    private static final int STATE_LEFT = 1;
750    private static final int STATE_RIGHT = 2;
751    private static final int STATE_NORMAL = 0;
752    private static final int STATE_HAS_MOREKEYS = 1;
753
754    @Override
755    public void showKeyPreview(final PointerTracker tracker) {
756        final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
757        final Keyboard keyboard = getKeyboard();
758        if (!mShowKeyPreviewPopup) {
759            previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap;
760            return;
761        }
762
763        final TextView previewText = getKeyPreviewText(tracker.mPointerId);
764        // If the key preview has no parent view yet, add it to the ViewGroup which can place
765        // key preview absolutely in SoftInputWindow.
766        if (previewText.getParent() == null) {
767            addKeyPreview(previewText);
768        }
769
770        mDrawingHandler.cancelDismissKeyPreview(tracker);
771        final Key key = tracker.getKey();
772        // If key is invalid or IME is already closed, we must not show key preview.
773        // Trying to show key preview while root window is closed causes
774        // WindowManager.BadTokenException.
775        if (key == null) {
776            return;
777        }
778
779        final KeyDrawParams drawParams = mKeyDrawParams;
780        previewText.setTextColor(drawParams.mPreviewTextColor);
781        final Drawable background = previewText.getBackground();
782        final String label = key.getPreviewLabel();
783        // What we show as preview should match what we show on a key top in onDraw().
784        if (label != null) {
785            // TODO Should take care of temporaryShiftLabel here.
786            previewText.setCompoundDrawables(null, null, null, null);
787            previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
788                    key.selectPreviewTextSize(drawParams));
789            previewText.setTypeface(key.selectPreviewTypeface(drawParams));
790            previewText.setText(label);
791        } else {
792            previewText.setCompoundDrawables(null, null, null,
793                    key.getPreviewIcon(keyboard.mIconsSet));
794            previewText.setText(null);
795        }
796
797        previewText.measure(
798                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
799        final int keyDrawWidth = key.getDrawWidth();
800        final int previewWidth = previewText.getMeasuredWidth();
801        final int previewHeight = mKeyPreviewHeight;
802        // The width and height of visible part of the key preview background. The content marker
803        // of the background 9-patch have to cover the visible part of the background.
804        previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
805                - previewText.getPaddingRight();
806        previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
807                - previewText.getPaddingBottom();
808        // The distance between the top edge of the parent key and the bottom of the visible part
809        // of the key preview background.
810        previewParams.mPreviewVisibleOffset = mKeyPreviewOffset - previewText.getPaddingBottom();
811        getLocationInWindow(mOriginCoords);
812        // The key preview is horizontally aligned with the center of the visible part of the
813        // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
814        // the left/right background is used if such background is specified.
815        final int statePosition;
816        int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2
817                + CoordinateUtils.x(mOriginCoords);
818        if (previewX < 0) {
819            previewX = 0;
820            statePosition = STATE_LEFT;
821        } else if (previewX > getWidth() - previewWidth) {
822            previewX = getWidth() - previewWidth;
823            statePosition = STATE_RIGHT;
824        } else {
825            statePosition = STATE_MIDDLE;
826        }
827        // The key preview is placed vertically above the top edge of the parent key with an
828        // arbitrary offset.
829        final int previewY = key.mY - previewHeight + mKeyPreviewOffset
830                + CoordinateUtils.y(mOriginCoords);
831
832        if (background != null) {
833            final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
834            background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
835            background.setAlpha(PREVIEW_ALPHA);
836        }
837        ViewLayoutUtils.placeViewAt(
838                previewText, previewX, previewY, previewWidth, previewHeight);
839        previewText.setVisibility(VISIBLE);
840    }
841
842    @Override
843    public void dismissKeyPreview(final PointerTracker tracker) {
844        mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, tracker);
845    }
846
847    public void setSlidingKeyInputPreviewEnabled(final boolean enabled) {
848        mSlidingKeyInputPreview.setPreviewEnabled(enabled);
849    }
850
851    @Override
852    public void showSlidingKeyInputPreview(final PointerTracker tracker) {
853        locatePreviewPlacerView();
854        mSlidingKeyInputPreview.setPreviewPosition(tracker);
855    }
856
857    @Override
858    public void dismissSlidingKeyInputPreview() {
859        mSlidingKeyInputPreview.dismissSlidingKeyInputPreview();
860    }
861
862    public void setGesturePreviewMode(final boolean drawsGestureTrail,
863            final boolean drawsGestureFloatingPreviewText) {
864        mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
865        mGestureTrailsPreview.setPreviewEnabled(drawsGestureTrail);
866    }
867
868    public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
869        locatePreviewPlacerView();
870        mGestureFloatingPreviewText.setSuggetedWords(suggestedWords);
871    }
872
873    public void dismissGestureFloatingPreviewText() {
874        locatePreviewPlacerView();
875        mDrawingHandler.dismissGestureFloatingPreviewText(mGestureFloatingPreviewTextLingerTimeout);
876    }
877
878    @Override
879    public void showGestureTrail(final PointerTracker tracker,
880            final boolean showsFloatingPreviewText) {
881        locatePreviewPlacerView();
882        if (showsFloatingPreviewText) {
883            mGestureFloatingPreviewText.setPreviewPosition(tracker);
884        }
885        mGestureTrailsPreview.setPreviewPosition(tracker);
886    }
887
888    // Note that this method is called from a non-UI thread.
889    public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
890        PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
891    }
892
893    public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
894        PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
895    }
896
897    @Override
898    protected void onAttachedToWindow() {
899        super.onAttachedToWindow();
900        // Notify the ResearchLogger (development only diagnostics) that the keyboard view has
901        // been attached.  This is needed to properly show the splash screen, which requires that
902        // the window token of the KeyboardView be non-null.
903        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
904            ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this);
905        }
906    }
907
908    @Override
909    protected void onDetachedFromWindow() {
910        super.onDetachedFromWindow();
911        mPreviewPlacerView.removeAllViews();
912        // Notify the ResearchLogger (development only diagnostics) that the keyboard view has
913        // been detached.  This is needed to invalidate the reference of {@link MainKeyboardView}
914        // to null.
915        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
916            ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow();
917        }
918    }
919
920    private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) {
921        if (key.mMoreKeys == null) {
922            return null;
923        }
924        Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
925        if (moreKeysKeyboard == null) {
926            moreKeysKeyboard = new MoreKeysKeyboard.Builder(
927                    context, key, this, mKeyPreviewDrawParams).build();
928            mMoreKeysKeyboardCache.put(key, moreKeysKeyboard);
929        }
930
931        final View container = mMoreKeysKeyboardContainer;
932        final MoreKeysKeyboardView moreKeysKeyboardView =
933                (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
934        moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
935        container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
936        return moreKeysKeyboardView;
937    }
938
939    /**
940     * Called when a key is long pressed.
941     * @param tracker the pointer tracker which pressed the parent key
942     */
943    private void onLongPress(final PointerTracker tracker) {
944        if (isShowingMoreKeysPanel()) {
945            return;
946        }
947        final Key key = tracker.getKey();
948        if (key == null) {
949            return;
950        }
951        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
952            ResearchLogger.mainKeyboardView_onLongPress();
953        }
954        final KeyboardActionListener listener = mKeyboardActionListener;
955        if (key.hasNoPanelAutoMoreKey()) {
956            final int moreKeyCode = key.mMoreKeys[0].mCode;
957            tracker.onLongPressed();
958            listener.onPressKey(moreKeyCode, false /* isRepeatKey */, true /* isSinglePointer */);
959            listener.onCodeInput(moreKeyCode,
960                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
961            listener.onReleaseKey(moreKeyCode, false /* withSliding */);
962            return;
963        }
964        final int code = key.mCode;
965        if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
966            // Long pressing the space key invokes IME switcher dialog.
967            if (listener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
968                tracker.onLongPressed();
969                listener.onReleaseKey(code, false /* withSliding */);
970                return;
971            }
972        }
973        openMoreKeysPanel(key, tracker);
974    }
975
976    private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
977        final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
978        if (moreKeysPanel == null) {
979            return;
980        }
981
982        final int[] lastCoords = CoordinateUtils.newInstance();
983        tracker.getLastCoordinates(lastCoords);
984        final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !key.noKeyPreview();
985        // The more keys keyboard is usually horizontally aligned with the center of the parent key.
986        // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
987        // keys keyboard is placed at the touch point of the parent key.
988        final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
989                ? CoordinateUtils.x(lastCoords)
990                : key.mX + key.mWidth / 2;
991        // The more keys keyboard is usually vertically aligned with the top edge of the parent key
992        // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
993        // aligned with the bottom edge of the visible part of the key preview.
994        // {@code mPreviewVisibleOffset} has been set appropriately in
995        // {@link KeyboardView#showKeyPreview(PointerTracker)}.
996        final int pointY = key.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
997        moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
998        final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords));
999        final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords));
1000        tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
1001    }
1002
1003    public boolean isInSlidingKeyInput() {
1004        if (isShowingMoreKeysPanel()) {
1005            return true;
1006        }
1007        return PointerTracker.isAnyInSlidingKeyInput();
1008    }
1009
1010    @Override
1011    public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
1012        locatePreviewPlacerView();
1013        // TODO: Remove this check
1014        if (panel.isShowingInParent()) {
1015            panel.dismissMoreKeysPanel();
1016        }
1017        mPreviewPlacerView.addView(panel.getContainerView());
1018        mMoreKeysPanel = panel;
1019        dimEntireKeyboard(true /* dimmed */);
1020    }
1021
1022    public boolean isShowingMoreKeysPanel() {
1023        return mMoreKeysPanel != null && mMoreKeysPanel.isShowingInParent();
1024    }
1025
1026    @Override
1027    public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
1028        PointerTracker.dismissAllMoreKeysPanels();
1029    }
1030
1031    @Override
1032    public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
1033        dimEntireKeyboard(false /* dimmed */);
1034        if (isShowingMoreKeysPanel()) {
1035            mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView());
1036            mMoreKeysPanel = null;
1037        }
1038    }
1039
1040    @Override
1041    public boolean dispatchTouchEvent(MotionEvent event) {
1042        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
1043            return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event);
1044        }
1045        return super.dispatchTouchEvent(event);
1046    }
1047
1048    @Override
1049    public boolean onTouchEvent(final MotionEvent me) {
1050        if (getKeyboard() == null) {
1051            return false;
1052        }
1053        return mTouchScreenRegulator.onTouchEvent(me);
1054    }
1055
1056    @Override
1057    public boolean processMotionEvent(final MotionEvent me) {
1058        final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
1059        final int action = me.getActionMasked();
1060        final int pointerCount = me.getPointerCount();
1061        final int oldPointerCount = mOldPointerCount;
1062        mOldPointerCount = pointerCount;
1063
1064        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1065        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1066        // events except a transition from/to single-touch.
1067        if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
1068            return true;
1069        }
1070
1071        final long eventTime = me.getEventTime();
1072        final int index = me.getActionIndex();
1073        final int id = me.getPointerId(index);
1074        final int x = (int)me.getX(index);
1075        final int y = (int)me.getY(index);
1076
1077        // TODO: This might be moved to the tracker.processMotionEvent() call below.
1078        if (ENABLE_USABILITY_STUDY_LOG && action != MotionEvent.ACTION_MOVE) {
1079            writeUsabilityStudyLog(me, action, eventTime, index, id, x, y);
1080        }
1081        // TODO: This should be moved to the tracker.processMotionEvent() call below.
1082        // Currently the same "move" event is being logged twice.
1083        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1084            ResearchLogger.mainKeyboardView_processMotionEvent(
1085                    me, action, eventTime, index, id, x, y);
1086        }
1087
1088        if (mKeyTimerHandler.isInKeyRepeat()) {
1089            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
1090            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1091            // event (UP or DOWN) is non-modifier key.
1092            if (pointerCount > 1 && !tracker.isModifier()) {
1093                mKeyTimerHandler.cancelKeyRepeatTimer();
1094            }
1095            // Up event will pass through.
1096        }
1097
1098        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1099        // Translate mutli-touch event to single-touch events on the device that has no distinct
1100        // multi-touch panel.
1101        if (nonDistinctMultitouch) {
1102            // Use only main (id=0) pointer tracker.
1103            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
1104            if (pointerCount == 1 && oldPointerCount == 2) {
1105                // Multi-touch to single touch transition.
1106                // Send a down event for the latest pointer if the key is different from the
1107                // previous key.
1108                final Key newKey = tracker.getKeyOn(x, y);
1109                if (mOldKey != newKey) {
1110                    tracker.onDownEvent(x, y, eventTime, this);
1111                    if (action == MotionEvent.ACTION_UP) {
1112                        tracker.onUpEvent(x, y, eventTime);
1113                    }
1114                }
1115            } else if (pointerCount == 2 && oldPointerCount == 1) {
1116                // Single-touch to multi-touch transition.
1117                // Send an up event for the last pointer.
1118                final int[] lastCoords = CoordinateUtils.newInstance();
1119                mOldKey = tracker.getKeyOn(
1120                        CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords));
1121                tracker.onUpEvent(
1122                        CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords), eventTime);
1123            } else if (pointerCount == 1 && oldPointerCount == 1) {
1124                tracker.processMotionEvent(action, x, y, eventTime, this);
1125            } else {
1126                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1127                        + " (old " + oldPointerCount + ")");
1128            }
1129            return true;
1130        }
1131
1132        if (action == MotionEvent.ACTION_MOVE) {
1133            for (int i = 0; i < pointerCount; i++) {
1134                final int pointerId = me.getPointerId(i);
1135                final PointerTracker tracker = PointerTracker.getPointerTracker(
1136                        pointerId, this);
1137                final int px = (int)me.getX(i);
1138                final int py = (int)me.getY(i);
1139                tracker.onMoveEvent(px, py, eventTime, me);
1140                if (ENABLE_USABILITY_STUDY_LOG) {
1141                    writeUsabilityStudyLog(me, action, eventTime, i, pointerId, px, py);
1142                }
1143                // TODO: This seems to be no longer necessary, and confusing because it leads to
1144                // duplicate MotionEvents being recorded.
1145                // if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1146                //     ResearchLogger.mainKeyboardView_processMotionEvent(
1147                //             me, action, eventTime, i, pointerId, px, py);
1148                // }
1149            }
1150        } else {
1151            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
1152            tracker.processMotionEvent(action, x, y, eventTime, this);
1153        }
1154
1155        return true;
1156    }
1157
1158    private static void writeUsabilityStudyLog(final MotionEvent me, final int action,
1159            final long eventTime, final int index, final int id, final int x, final int y) {
1160        final String eventTag;
1161        switch (action) {
1162        case MotionEvent.ACTION_UP:
1163            eventTag = "[Up]";
1164            break;
1165        case MotionEvent.ACTION_DOWN:
1166            eventTag = "[Down]";
1167            break;
1168        case MotionEvent.ACTION_POINTER_UP:
1169            eventTag = "[PointerUp]";
1170            break;
1171        case MotionEvent.ACTION_POINTER_DOWN:
1172            eventTag = "[PointerDown]";
1173            break;
1174        case MotionEvent.ACTION_MOVE:
1175            eventTag = "[Move]";
1176            break;
1177        default:
1178            eventTag = "[Action" + action + "]";
1179            break;
1180        }
1181        final float size = me.getSize(index);
1182        final float pressure = me.getPressure(index);
1183        UsabilityStudyLogUtils.getInstance().write(
1184                eventTag + eventTime + "," + id + "," + x + "," + y + "," + size + "," + pressure);
1185    }
1186
1187    public void cancelAllOngoingEvents() {
1188        mKeyTimerHandler.cancelAllMessages();
1189        mDrawingHandler.cancelAllMessages();
1190        dismissAllKeyPreviews();
1191        dismissGestureFloatingPreviewText();
1192        dismissSlidingKeyInputPreview();
1193        PointerTracker.dismissAllMoreKeysPanels();
1194        PointerTracker.cancelAllPointerTrackers();
1195    }
1196
1197    public void closing() {
1198        cancelAllOngoingEvents();
1199        mMoreKeysKeyboardCache.clear();
1200    }
1201
1202    /**
1203     * Receives hover events from the input framework.
1204     *
1205     * @param event The motion event to be dispatched.
1206     * @return {@code true} if the event was handled by the view, {@code false}
1207     *         otherwise
1208     */
1209    @Override
1210    public boolean dispatchHoverEvent(final MotionEvent event) {
1211        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
1212            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
1213            return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
1214        }
1215
1216        // Reflection doesn't support calling superclass methods.
1217        return false;
1218    }
1219
1220    public void updateShortcutKey(final boolean available) {
1221        final Keyboard keyboard = getKeyboard();
1222        if (keyboard == null) {
1223            return;
1224        }
1225        final Key shortcutKey = keyboard.getKey(Constants.CODE_SHORTCUT);
1226        if (shortcutKey == null) {
1227            return;
1228        }
1229        shortcutKey.setEnabled(available);
1230        invalidateKey(shortcutKey);
1231    }
1232
1233    public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
1234            final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) {
1235        mNeedsToDisplayLanguage = needsToDisplayLanguage;
1236        mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
1237        final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
1238        if (animator == null) {
1239            mNeedsToDisplayLanguage = false;
1240        } else {
1241            if (subtypeChanged && needsToDisplayLanguage) {
1242                setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
1243                if (animator.isStarted()) {
1244                    animator.cancel();
1245                }
1246                animator.start();
1247            } else {
1248                if (!animator.isStarted()) {
1249                    mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
1250                }
1251            }
1252        }
1253        invalidateKey(mSpaceKey);
1254    }
1255
1256    public void updateAutoCorrectionState(final boolean isAutoCorrection) {
1257        if (!mAutoCorrectionSpacebarLedEnabled) {
1258            return;
1259        }
1260        mAutoCorrectionSpacebarLedOn = isAutoCorrection;
1261        invalidateKey(mSpaceKey);
1262    }
1263
1264    private void dimEntireKeyboard(final boolean dimmed) {
1265        final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
1266        mNeedsToDimEntireKeyboard = dimmed;
1267        if (needsRedrawing) {
1268            invalidateAllKeys();
1269        }
1270    }
1271
1272    @Override
1273    protected void onDraw(final Canvas canvas) {
1274        super.onDraw(canvas);
1275
1276        // Overlay a dark rectangle to dim.
1277        if (mNeedsToDimEntireKeyboard) {
1278            canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint);
1279        }
1280    }
1281
1282    @Override
1283    protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
1284            final KeyDrawParams params) {
1285        if (key.altCodeWhileTyping() && key.isEnabled()) {
1286            params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
1287        }
1288        if (key.mCode == Constants.CODE_SPACE) {
1289            drawSpacebar(key, canvas, paint);
1290            // Whether space key needs to show the "..." popup hint for special purposes
1291            if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
1292                drawKeyPopupHint(key, canvas, paint, params);
1293            }
1294        } else if (key.mCode == Constants.CODE_LANGUAGE_SWITCH) {
1295            super.onDrawKeyTopVisuals(key, canvas, paint, params);
1296            drawKeyPopupHint(key, canvas, paint, params);
1297        } else {
1298            super.onDrawKeyTopVisuals(key, canvas, paint, params);
1299        }
1300    }
1301
1302    private static boolean fitsTextIntoWidth(final int width, final String text,
1303            final Paint paint) {
1304        paint.setTextScaleX(1.0f);
1305        final float textWidth = TypefaceUtils.getLabelWidth(text, paint);
1306        if (textWidth < width) {
1307            return true;
1308        }
1309
1310        final float scaleX = width / textWidth;
1311        if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) {
1312            return false;
1313        }
1314
1315        paint.setTextScaleX(scaleX);
1316        return TypefaceUtils.getLabelWidth(text, paint) < width;
1317    }
1318
1319    // Layout language name on spacebar.
1320    private static String layoutLanguageOnSpacebar(final Paint paint,
1321            final InputMethodSubtype subtype, final int width) {
1322        // Choose appropriate language name to fit into the width.
1323        final String fullText = SubtypeLocaleUtils.getFullDisplayName(subtype);
1324        if (fitsTextIntoWidth(width, fullText, paint)) {
1325            return fullText;
1326        }
1327
1328        final String middleText = SubtypeLocaleUtils.getMiddleDisplayName(subtype);
1329        if (fitsTextIntoWidth(width, middleText, paint)) {
1330            return middleText;
1331        }
1332
1333        final String shortText = SubtypeLocaleUtils.getShortDisplayName(subtype);
1334        if (fitsTextIntoWidth(width, shortText, paint)) {
1335            return shortText;
1336        }
1337
1338        return "";
1339    }
1340
1341    private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
1342        final int width = key.mWidth;
1343        final int height = key.mHeight;
1344
1345        // If input language are explicitly selected.
1346        if (mNeedsToDisplayLanguage) {
1347            paint.setTextAlign(Align.CENTER);
1348            paint.setTypeface(Typeface.DEFAULT);
1349            paint.setTextSize(mSpacebarTextSize);
1350            final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
1351            final String language = layoutLanguageOnSpacebar(paint, subtype, width);
1352            // Draw language text with shadow
1353            final float descent = paint.descent();
1354            final float textHeight = -paint.ascent() + descent;
1355            final float baseline = height / 2 + textHeight / 2;
1356            paint.setColor(mSpacebarTextShadowColor);
1357            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
1358            canvas.drawText(language, width / 2, baseline - descent - 1, paint);
1359            paint.setColor(mSpacebarTextColor);
1360            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
1361            canvas.drawText(language, width / 2, baseline - descent, paint);
1362        }
1363
1364        // Draw the spacebar icon at the bottom
1365        if (mAutoCorrectionSpacebarLedOn) {
1366            final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
1367            final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
1368            int x = (width - iconWidth) / 2;
1369            int y = height - iconHeight;
1370            drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
1371        } else if (mSpaceIcon != null) {
1372            final int iconWidth = mSpaceIcon.getIntrinsicWidth();
1373            final int iconHeight = mSpaceIcon.getIntrinsicHeight();
1374            int x = (width - iconWidth) / 2;
1375            int y = height - iconHeight;
1376            drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
1377        }
1378    }
1379}
1380