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