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