Home | History | Annotate | Download | only in keyboard
      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 
     17 package com.android.inputmethod.keyboard;
     18 
     19 import android.animation.AnimatorInflater;
     20 import android.animation.ObjectAnimator;
     21 import android.content.Context;
     22 import android.content.pm.PackageManager;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.Paint.Align;
     28 import android.graphics.Typeface;
     29 import android.graphics.drawable.Drawable;
     30 import android.os.Message;
     31 import android.text.TextUtils;
     32 import android.util.AttributeSet;
     33 import android.util.Log;
     34 import android.view.LayoutInflater;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 import android.view.ViewConfiguration;
     38 import android.view.ViewGroup;
     39 import android.view.inputmethod.InputMethodSubtype;
     40 import android.widget.PopupWindow;
     41 
     42 import com.android.inputmethod.accessibility.AccessibilityUtils;
     43 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
     44 import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
     45 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
     46 import com.android.inputmethod.latin.LatinIME;
     47 import com.android.inputmethod.latin.LatinImeLogger;
     48 import com.android.inputmethod.latin.R;
     49 import com.android.inputmethod.latin.ResearchLogger;
     50 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
     51 import com.android.inputmethod.latin.StringUtils;
     52 import com.android.inputmethod.latin.SubtypeLocale;
     53 import com.android.inputmethod.latin.Utils;
     54 import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
     55 import com.android.inputmethod.latin.define.ProductionFlag;
     56 
     57 import java.util.Locale;
     58 import java.util.WeakHashMap;
     59 
     60 /**
     61  * A view that is responsible for detecting key presses and touch movements.
     62  *
     63  * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
     64  * @attr ref R.styleable#KeyboardView_verticalCorrection
     65  * @attr ref R.styleable#KeyboardView_popupLayout
     66  */
     67 public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
     68         SuddenJumpingTouchEventHandler.ProcessMotionEvent {
     69     private static final String TAG = LatinKeyboardView.class.getSimpleName();
     70 
     71     // TODO: Kill process when the usability study mode was changed.
     72     private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
     73 
     74     /** Listener for {@link KeyboardActionListener}. */
     75     private KeyboardActionListener mKeyboardActionListener;
     76 
     77     /* Space key and its icons */
     78     private Key mSpaceKey;
     79     private Drawable mSpaceIcon;
     80     // Stuff to draw language name on spacebar.
     81     private final int mLanguageOnSpacebarFinalAlpha;
     82     private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
     83     private static final int ALPHA_OPAQUE = 255;
     84     private boolean mNeedsToDisplayLanguage;
     85     private boolean mHasMultipleEnabledIMEsOrSubtypes;
     86     private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE;
     87     private final float mSpacebarTextRatio;
     88     private float mSpacebarTextSize;
     89     private final int mSpacebarTextColor;
     90     private final int mSpacebarTextShadowColor;
     91     // The minimum x-scale to fit the language name on spacebar.
     92     private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
     93     // Stuff to draw auto correction LED on spacebar.
     94     private boolean mAutoCorrectionSpacebarLedOn;
     95     private final boolean mAutoCorrectionSpacebarLedEnabled;
     96     private final Drawable mAutoCorrectionSpacebarLedIcon;
     97     private static final int SPACE_LED_LENGTH_PERCENT = 80;
     98 
     99     // Stuff to draw altCodeWhileTyping keys.
    100     private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
    101     private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
    102     private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE;
    103 
    104     // More keys keyboard
    105     private PopupWindow mMoreKeysWindow;
    106     private MoreKeysPanel mMoreKeysPanel;
    107     private int mMoreKeysPanelPointerTrackerId;
    108     private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
    109             new WeakHashMap<Key, MoreKeysPanel>();
    110     private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
    111 
    112     private final PointerTrackerParams mPointerTrackerParams;
    113     private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
    114 
    115     protected KeyDetector mKeyDetector;
    116     private boolean mHasDistinctMultitouch;
    117     private int mOldPointerCount = 1;
    118     private Key mOldKey;
    119 
    120     private final KeyTimerHandler mKeyTimerHandler;
    121 
    122     private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
    123             implements TimerProxy {
    124         private static final int MSG_REPEAT_KEY = 1;
    125         private static final int MSG_LONGPRESS_KEY = 2;
    126         private static final int MSG_DOUBLE_TAP = 3;
    127         private static final int MSG_TYPING_STATE_EXPIRED = 4;
    128 
    129         private final KeyTimerParams mParams;
    130         private boolean mInKeyRepeat;
    131 
    132         public KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params) {
    133             super(outerInstance);
    134             mParams = params;
    135         }
    136 
    137         @Override
    138         public void handleMessage(Message msg) {
    139             final LatinKeyboardView keyboardView = getOuterInstance();
    140             final PointerTracker tracker = (PointerTracker) msg.obj;
    141             switch (msg.what) {
    142             case MSG_REPEAT_KEY:
    143                 tracker.onRegisterKey(tracker.getKey());
    144                 startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
    145                 break;
    146             case MSG_LONGPRESS_KEY:
    147                 if (tracker != null) {
    148                     keyboardView.openMoreKeysKeyboardIfRequired(tracker.getKey(), tracker);
    149                 } else {
    150                     KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
    151                 }
    152                 break;
    153             case MSG_TYPING_STATE_EXPIRED:
    154                 cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
    155                         keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
    156                 break;
    157             }
    158         }
    159 
    160         private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
    161             sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
    162         }
    163 
    164         @Override
    165         public void startKeyRepeatTimer(PointerTracker tracker) {
    166             mInKeyRepeat = true;
    167             startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
    168         }
    169 
    170         public void cancelKeyRepeatTimer() {
    171             mInKeyRepeat = false;
    172             removeMessages(MSG_REPEAT_KEY);
    173         }
    174 
    175         public boolean isInKeyRepeat() {
    176             return mInKeyRepeat;
    177         }
    178 
    179         @Override
    180         public void startLongPressTimer(int code) {
    181             cancelLongPressTimer();
    182             final int delay;
    183             switch (code) {
    184             case Keyboard.CODE_SHIFT:
    185                 delay = mParams.mLongPressShiftKeyTimeout;
    186                 break;
    187             default:
    188                 delay = 0;
    189                 break;
    190             }
    191             if (delay > 0) {
    192                 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay);
    193             }
    194         }
    195 
    196         @Override
    197         public void startLongPressTimer(PointerTracker tracker) {
    198             cancelLongPressTimer();
    199             if (tracker == null) {
    200                 return;
    201             }
    202             final Key key = tracker.getKey();
    203             final int delay;
    204             switch (key.mCode) {
    205             case Keyboard.CODE_SHIFT:
    206                 delay = mParams.mLongPressShiftKeyTimeout;
    207                 break;
    208             default:
    209                 if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
    210                     // We use longer timeout for sliding finger input started from the symbols
    211                     // mode key.
    212                     delay = mParams.mLongPressKeyTimeout * 3;
    213                 } else {
    214                     delay = mParams.mLongPressKeyTimeout;
    215                 }
    216                 break;
    217             }
    218             if (delay > 0) {
    219                 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
    220             }
    221         }
    222 
    223         @Override
    224         public void cancelLongPressTimer() {
    225             removeMessages(MSG_LONGPRESS_KEY);
    226         }
    227 
    228         public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
    229                 final ObjectAnimator animatorToStart) {
    230             float startFraction = 0.0f;
    231             if (animatorToCancel.isStarted()) {
    232                 animatorToCancel.cancel();
    233                 startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
    234             }
    235             final long startTime = (long)(animatorToStart.getDuration() * startFraction);
    236             animatorToStart.start();
    237             animatorToStart.setCurrentPlayTime(startTime);
    238         }
    239 
    240         @Override
    241         public void startTypingStateTimer() {
    242             final boolean isTyping = isTypingState();
    243             removeMessages(MSG_TYPING_STATE_EXPIRED);
    244             sendMessageDelayed(
    245                     obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
    246             if (isTyping) {
    247                 return;
    248             }
    249             final LatinKeyboardView keyboardView = getOuterInstance();
    250             cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
    251                     keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
    252         }
    253 
    254         @Override
    255         public boolean isTypingState() {
    256             return hasMessages(MSG_TYPING_STATE_EXPIRED);
    257         }
    258 
    259         @Override
    260         public void startDoubleTapTimer() {
    261             sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP),
    262                     ViewConfiguration.getDoubleTapTimeout());
    263         }
    264 
    265         @Override
    266         public void cancelDoubleTapTimer() {
    267             removeMessages(MSG_DOUBLE_TAP);
    268         }
    269 
    270         @Override
    271         public boolean isInDoubleTapTimeout() {
    272             return hasMessages(MSG_DOUBLE_TAP);
    273         }
    274 
    275         @Override
    276         public void cancelKeyTimers() {
    277             cancelKeyRepeatTimer();
    278             cancelLongPressTimer();
    279         }
    280 
    281         public void cancelAllMessages() {
    282             cancelKeyTimers();
    283         }
    284     }
    285 
    286     public static class PointerTrackerParams {
    287         public final boolean mSlidingKeyInputEnabled;
    288         public final int mTouchNoiseThresholdTime;
    289         public final float mTouchNoiseThresholdDistance;
    290 
    291         public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
    292 
    293         private PointerTrackerParams() {
    294             mSlidingKeyInputEnabled = false;
    295             mTouchNoiseThresholdTime =0;
    296             mTouchNoiseThresholdDistance = 0;
    297         }
    298 
    299         public PointerTrackerParams(TypedArray latinKeyboardViewAttr) {
    300             mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
    301                     R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
    302             mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
    303                     R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
    304             mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
    305                     R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
    306         }
    307     }
    308 
    309     static class KeyTimerParams {
    310         public final int mKeyRepeatStartTimeout;
    311         public final int mKeyRepeatInterval;
    312         public final int mLongPressKeyTimeout;
    313         public final int mLongPressShiftKeyTimeout;
    314         public final int mIgnoreAltCodeKeyTimeout;
    315 
    316         public KeyTimerParams(TypedArray latinKeyboardViewAttr) {
    317             mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
    318                     R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
    319             mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
    320                     R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
    321             mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
    322                     R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
    323             mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
    324                     R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
    325             mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
    326                     R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
    327         }
    328     }
    329 
    330     public LatinKeyboardView(Context context, AttributeSet attrs) {
    331         this(context, attrs, R.attr.latinKeyboardViewStyle);
    332     }
    333 
    334     public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
    335         super(context, attrs, defStyle);
    336 
    337         mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
    338 
    339         mHasDistinctMultitouch = context.getPackageManager()
    340                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
    341         final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
    342                 Utils.getDeviceOverrideValue(context.getResources(),
    343                         R.array.phantom_sudden_move_event_device_list, "false"));
    344         PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack);
    345 
    346         final TypedArray a = context.obtainStyledAttributes(
    347                 attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
    348         mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
    349                 R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
    350         mAutoCorrectionSpacebarLedIcon = a.getDrawable(
    351                 R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
    352         mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
    353                 1000, 1000, 1) / 1000.0f;
    354         mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
    355         mSpacebarTextShadowColor = a.getColor(
    356                 R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
    357         mLanguageOnSpacebarFinalAlpha = a.getInt(
    358                 R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE);
    359         final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
    360                 R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
    361         final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
    362                 R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
    363         final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
    364                 R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
    365 
    366         final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
    367         mPointerTrackerParams = new PointerTrackerParams(a);
    368 
    369         final float keyHysteresisDistance = a.getDimension(
    370                 R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
    371         mKeyDetector = new KeyDetector(keyHysteresisDistance);
    372         mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
    373         mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
    374                 R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
    375         a.recycle();
    376 
    377         PointerTracker.setParameters(mPointerTrackerParams);
    378 
    379         mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
    380                 languageOnSpacebarFadeoutAnimatorResId, this);
    381         mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
    382                 altCodeKeyWhileTypingFadeoutAnimatorResId, this);
    383         mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
    384                 altCodeKeyWhileTypingFadeinAnimatorResId, this);
    385     }
    386 
    387     private ObjectAnimator loadObjectAnimator(int resId, Object target) {
    388         if (resId == 0) return null;
    389         final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
    390                 getContext(), resId);
    391         if (animator != null) {
    392             animator.setTarget(target);
    393         }
    394         return animator;
    395     }
    396 
    397     // Getter/setter methods for {@link ObjectAnimator}.
    398     public int getLanguageOnSpacebarAnimAlpha() {
    399         return mLanguageOnSpacebarAnimAlpha;
    400     }
    401 
    402     public void setLanguageOnSpacebarAnimAlpha(int alpha) {
    403         mLanguageOnSpacebarAnimAlpha = alpha;
    404         invalidateKey(mSpaceKey);
    405     }
    406 
    407     public int getAltCodeKeyWhileTypingAnimAlpha() {
    408         return mAltCodeKeyWhileTypingAnimAlpha;
    409     }
    410 
    411     public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
    412         mAltCodeKeyWhileTypingAnimAlpha = alpha;
    413         updateAltCodeKeyWhileTyping();
    414     }
    415 
    416     public void setKeyboardActionListener(KeyboardActionListener listener) {
    417         mKeyboardActionListener = listener;
    418         PointerTracker.setKeyboardActionListener(listener);
    419     }
    420 
    421     /**
    422      * Returns the {@link KeyboardActionListener} object.
    423      * @return the listener attached to this keyboard
    424      */
    425     @Override
    426     public KeyboardActionListener getKeyboardActionListener() {
    427         return mKeyboardActionListener;
    428     }
    429 
    430     @Override
    431     public KeyDetector getKeyDetector() {
    432         return mKeyDetector;
    433     }
    434 
    435     @Override
    436     public DrawingProxy getDrawingProxy() {
    437         return this;
    438     }
    439 
    440     @Override
    441     public TimerProxy getTimerProxy() {
    442         return mKeyTimerHandler;
    443     }
    444 
    445     /**
    446      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
    447      * view will re-layout itself to accommodate the keyboard.
    448      * @see Keyboard
    449      * @see #getKeyboard()
    450      * @param keyboard the keyboard to display in this view
    451      */
    452     @Override
    453     public void setKeyboard(Keyboard keyboard) {
    454         // Remove any pending messages, except dismissing preview
    455         mKeyTimerHandler.cancelKeyTimers();
    456         super.setKeyboard(keyboard);
    457         mKeyDetector.setKeyboard(
    458                 keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
    459         PointerTracker.setKeyDetector(mKeyDetector);
    460         mTouchScreenRegulator.setKeyboard(keyboard);
    461         mMoreKeysPanelCache.clear();
    462 
    463         mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
    464         mSpaceIcon = (mSpaceKey != null)
    465                 ? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null;
    466         final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
    467         mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
    468         if (ProductionFlag.IS_EXPERIMENTAL) {
    469             ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
    470         }
    471 
    472         // This always needs to be set since the accessibility state can
    473         // potentially change without the keyboard being set again.
    474         AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
    475     }
    476 
    477     /**
    478      * Returns whether the device has distinct multi-touch panel.
    479      * @return true if the device has distinct multi-touch panel.
    480      */
    481     public boolean hasDistinctMultitouch() {
    482         return mHasDistinctMultitouch;
    483     }
    484 
    485     public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
    486         mHasDistinctMultitouch = hasDistinctMultitouch;
    487     }
    488 
    489     /**
    490      * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
    491      * codes for adjacent keys.  When disabled, only the primary key code will be
    492      * reported.
    493      * @param enabled whether or not the proximity correction is enabled
    494      */
    495     public void setProximityCorrectionEnabled(boolean enabled) {
    496         mKeyDetector.setProximityCorrectionEnabled(enabled);
    497     }
    498 
    499     /**
    500      * Returns true if proximity correction is enabled.
    501      */
    502     public boolean isProximityCorrectionEnabled() {
    503         return mKeyDetector.isProximityCorrectionEnabled();
    504     }
    505 
    506     @Override
    507     public void cancelAllMessages() {
    508         mKeyTimerHandler.cancelAllMessages();
    509         super.cancelAllMessages();
    510     }
    511 
    512     private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
    513         // Check if we have a popup layout specified first.
    514         if (mMoreKeysLayout == 0) {
    515             return false;
    516         }
    517 
    518         // Check if we are already displaying popup panel.
    519         if (mMoreKeysPanel != null)
    520             return false;
    521         if (parentKey == null)
    522             return false;
    523         return onLongPress(parentKey, tracker);
    524     }
    525 
    526     // This default implementation returns a more keys panel.
    527     protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
    528         if (parentKey.mMoreKeys == null)
    529             return null;
    530 
    531         final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
    532         if (container == null)
    533             throw new NullPointerException();
    534 
    535         final MoreKeysKeyboardView moreKeysKeyboardView =
    536                 (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
    537         final Keyboard moreKeysKeyboard = new MoreKeysKeyboard.Builder(container, parentKey, this)
    538                 .build();
    539         moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
    540         container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    541 
    542         return moreKeysKeyboardView;
    543     }
    544 
    545     /**
    546      * Called when a key is long pressed. By default this will open more keys keyboard associated
    547      * with this key.
    548      * @param parentKey the key that was long pressed
    549      * @param tracker the pointer tracker which pressed the parent key
    550      * @return true if the long press is handled, false otherwise. Subclasses should call the
    551      * method on the base class if the subclass doesn't wish to handle the call.
    552      */
    553     protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
    554         if (ProductionFlag.IS_EXPERIMENTAL) {
    555             ResearchLogger.latinKeyboardView_onLongPress();
    556         }
    557         final int primaryCode = parentKey.mCode;
    558         if (parentKey.hasEmbeddedMoreKey()) {
    559             final int embeddedCode = parentKey.mMoreKeys[0].mCode;
    560             tracker.onLongPressed();
    561             invokeCodeInput(embeddedCode);
    562             invokeReleaseKey(primaryCode);
    563             KeyboardSwitcher.getInstance().hapticAndAudioFeedback(primaryCode);
    564             return true;
    565         }
    566         if (primaryCode == Keyboard.CODE_SPACE || primaryCode == Keyboard.CODE_LANGUAGE_SWITCH) {
    567             // Long pressing the space key invokes IME switcher dialog.
    568             if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
    569                 tracker.onLongPressed();
    570                 invokeReleaseKey(primaryCode);
    571                 return true;
    572             }
    573         }
    574         return openMoreKeysPanel(parentKey, tracker);
    575     }
    576 
    577     private boolean invokeCustomRequest(int code) {
    578         return mKeyboardActionListener.onCustomRequest(code);
    579     }
    580 
    581     private void invokeCodeInput(int primaryCode) {
    582         mKeyboardActionListener.onCodeInput(primaryCode,
    583                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
    584                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
    585     }
    586 
    587     private void invokeReleaseKey(int primaryCode) {
    588         mKeyboardActionListener.onReleaseKey(primaryCode, false);
    589     }
    590 
    591     private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
    592         MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
    593         if (moreKeysPanel == null) {
    594             moreKeysPanel = onCreateMoreKeysPanel(parentKey);
    595             if (moreKeysPanel == null)
    596                 return false;
    597             mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
    598         }
    599         if (mMoreKeysWindow == null) {
    600             mMoreKeysWindow = new PopupWindow(getContext());
    601             mMoreKeysWindow.setBackgroundDrawable(null);
    602             mMoreKeysWindow.setAnimationStyle(R.style.MoreKeysKeyboardAnimation);
    603         }
    604         mMoreKeysPanel = moreKeysPanel;
    605         mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
    606 
    607         final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview();
    608         // The more keys keyboard is usually horizontally aligned with the center of the parent key.
    609         // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
    610         // keys keyboard is placed at the touch point of the parent key.
    611         final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
    612                 ? tracker.getLastX()
    613                 : parentKey.mX + parentKey.mWidth / 2;
    614         // The more keys keyboard is usually vertically aligned with the top edge of the parent key
    615         // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
    616         // aligned with the bottom edge of the visible part of the key preview.
    617         final int pointY = parentKey.mY + (keyPreviewEnabled
    618                 ? mKeyPreviewDrawParams.mPreviewVisibleOffset
    619                 : -parentKey.mVerticalGap);
    620         moreKeysPanel.showMoreKeysPanel(
    621                 this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
    622         final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
    623         final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
    624         tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
    625         dimEntireKeyboard(true);
    626         return true;
    627     }
    628 
    629     public boolean isInSlidingKeyInput() {
    630         if (mMoreKeysPanel != null) {
    631             return true;
    632         } else {
    633             return PointerTracker.isAnyInSlidingKeyInput();
    634         }
    635     }
    636 
    637     public int getPointerCount() {
    638         return mOldPointerCount;
    639     }
    640 
    641     @Override
    642     public boolean onTouchEvent(MotionEvent me) {
    643         if (getKeyboard() == null) {
    644             return false;
    645         }
    646         return mTouchScreenRegulator.onTouchEvent(me);
    647     }
    648 
    649     @Override
    650     public boolean processMotionEvent(MotionEvent me) {
    651         final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
    652         final int action = me.getActionMasked();
    653         final int pointerCount = me.getPointerCount();
    654         final int oldPointerCount = mOldPointerCount;
    655         mOldPointerCount = pointerCount;
    656 
    657         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
    658         // If the device does not have distinct multi-touch support panel, ignore all multi-touch
    659         // events except a transition from/to single-touch.
    660         if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
    661             return true;
    662         }
    663 
    664         final long eventTime = me.getEventTime();
    665         final int index = me.getActionIndex();
    666         final int id = me.getPointerId(index);
    667         final int x, y;
    668         if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
    669             x = mMoreKeysPanel.translateX((int)me.getX(index));
    670             y = mMoreKeysPanel.translateY((int)me.getY(index));
    671         } else {
    672             x = (int)me.getX(index);
    673             y = (int)me.getY(index);
    674         }
    675         if (ENABLE_USABILITY_STUDY_LOG) {
    676             final String eventTag;
    677             switch (action) {
    678                 case MotionEvent.ACTION_UP:
    679                     eventTag = "[Up]";
    680                     break;
    681                 case MotionEvent.ACTION_DOWN:
    682                     eventTag = "[Down]";
    683                     break;
    684                 case MotionEvent.ACTION_POINTER_UP:
    685                     eventTag = "[PointerUp]";
    686                     break;
    687                 case MotionEvent.ACTION_POINTER_DOWN:
    688                     eventTag = "[PointerDown]";
    689                     break;
    690                 case MotionEvent.ACTION_MOVE: // Skip this as being logged below
    691                     eventTag = "";
    692                     break;
    693                 default:
    694                     eventTag = "[Action" + action + "]";
    695                     break;
    696             }
    697             if (!TextUtils.isEmpty(eventTag)) {
    698                 final float size = me.getSize(index);
    699                 final float pressure = me.getPressure(index);
    700                 UsabilityStudyLogUtils.getInstance().write(
    701                         eventTag + eventTime + "," + id + "," + x + "," + y + ","
    702                         + size + "," + pressure);
    703             }
    704         }
    705         if (ProductionFlag.IS_EXPERIMENTAL) {
    706             ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
    707                     x, y);
    708         }
    709 
    710         if (mKeyTimerHandler.isInKeyRepeat()) {
    711             final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
    712             // Key repeating timer will be canceled if 2 or more keys are in action, and current
    713             // event (UP or DOWN) is non-modifier key.
    714             if (pointerCount > 1 && !tracker.isModifier()) {
    715                 mKeyTimerHandler.cancelKeyRepeatTimer();
    716             }
    717             // Up event will pass through.
    718         }
    719 
    720         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
    721         // Translate mutli-touch event to single-touch events on the device that has no distinct
    722         // multi-touch panel.
    723         if (nonDistinctMultitouch) {
    724             // Use only main (id=0) pointer tracker.
    725             final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
    726             if (pointerCount == 1 && oldPointerCount == 2) {
    727                 // Multi-touch to single touch transition.
    728                 // Send a down event for the latest pointer if the key is different from the
    729                 // previous key.
    730                 final Key newKey = tracker.getKeyOn(x, y);
    731                 if (mOldKey != newKey) {
    732                     tracker.onDownEvent(x, y, eventTime, this);
    733                     if (action == MotionEvent.ACTION_UP)
    734                         tracker.onUpEvent(x, y, eventTime);
    735                 }
    736             } else if (pointerCount == 2 && oldPointerCount == 1) {
    737                 // Single-touch to multi-touch transition.
    738                 // Send an up event for the last pointer.
    739                 final int lastX = tracker.getLastX();
    740                 final int lastY = tracker.getLastY();
    741                 mOldKey = tracker.getKeyOn(lastX, lastY);
    742                 tracker.onUpEvent(lastX, lastY, eventTime);
    743             } else if (pointerCount == 1 && oldPointerCount == 1) {
    744                 tracker.processMotionEvent(action, x, y, eventTime, this);
    745             } else {
    746                 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
    747                         + " (old " + oldPointerCount + ")");
    748             }
    749             return true;
    750         }
    751 
    752         if (action == MotionEvent.ACTION_MOVE) {
    753             for (int i = 0; i < pointerCount; i++) {
    754                 final int pointerId = me.getPointerId(i);
    755                 final PointerTracker tracker = PointerTracker.getPointerTracker(
    756                         pointerId, this);
    757                 final int px, py;
    758                 if (mMoreKeysPanel != null
    759                         && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
    760                     px = mMoreKeysPanel.translateX((int)me.getX(i));
    761                     py = mMoreKeysPanel.translateY((int)me.getY(i));
    762                 } else {
    763                     px = (int)me.getX(i);
    764                     py = (int)me.getY(i);
    765                 }
    766                 tracker.onMoveEvent(px, py, eventTime);
    767                 if (ENABLE_USABILITY_STUDY_LOG) {
    768                     final float pointerSize = me.getSize(i);
    769                     final float pointerPressure = me.getPressure(i);
    770                     UsabilityStudyLogUtils.getInstance().write("[Move]"  + eventTime + ","
    771                             + pointerId + "," + px + "," + py + ","
    772                             + pointerSize + "," + pointerPressure);
    773                 }
    774                 if (ProductionFlag.IS_EXPERIMENTAL) {
    775                     ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
    776                             i, pointerId, px, py);
    777                 }
    778             }
    779         } else {
    780             final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
    781             tracker.processMotionEvent(action, x, y, eventTime, this);
    782         }
    783 
    784         return true;
    785     }
    786 
    787     @Override
    788     public void closing() {
    789         super.closing();
    790         dismissMoreKeysPanel();
    791         mMoreKeysPanelCache.clear();
    792     }
    793 
    794     @Override
    795     public boolean dismissMoreKeysPanel() {
    796         if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
    797             mMoreKeysWindow.dismiss();
    798             mMoreKeysPanel = null;
    799             mMoreKeysPanelPointerTrackerId = -1;
    800             dimEntireKeyboard(false);
    801             return true;
    802         }
    803         return false;
    804     }
    805 
    806     @Override
    807     public void draw(Canvas c) {
    808         Utils.GCUtils.getInstance().reset();
    809         boolean tryGC = true;
    810         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
    811             try {
    812                 super.draw(c);
    813                 tryGC = false;
    814             } catch (OutOfMemoryError e) {
    815                 tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
    816             }
    817         }
    818     }
    819 
    820     /**
    821      * Receives hover events from the input framework.
    822      *
    823      * @param event The motion event to be dispatched.
    824      * @return {@code true} if the event was handled by the view, {@code false}
    825      *         otherwise
    826      */
    827     @Override
    828     public boolean dispatchHoverEvent(MotionEvent event) {
    829         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
    830             final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
    831             return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
    832         }
    833 
    834         // Reflection doesn't support calling superclass methods.
    835         return false;
    836     }
    837 
    838     public void updateShortcutKey(boolean available) {
    839         final Keyboard keyboard = getKeyboard();
    840         if (keyboard == null) return;
    841         final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
    842         if (shortcutKey == null) return;
    843         shortcutKey.setEnabled(available);
    844         invalidateKey(shortcutKey);
    845     }
    846 
    847     private void updateAltCodeKeyWhileTyping() {
    848         final Keyboard keyboard = getKeyboard();
    849         if (keyboard == null) return;
    850         for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
    851             invalidateKey(key);
    852         }
    853     }
    854 
    855     public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
    856             boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
    857         mNeedsToDisplayLanguage = needsToDisplayLanguage;
    858         mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
    859         final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
    860         if (animator == null) {
    861             mNeedsToDisplayLanguage = false;
    862         } else {
    863             if (subtypeChanged && needsToDisplayLanguage) {
    864                 setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE);
    865                 if (animator.isStarted()) {
    866                     animator.cancel();
    867                 }
    868                 animator.start();
    869             } else {
    870                 if (!animator.isStarted()) {
    871                     mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
    872                 }
    873             }
    874         }
    875         invalidateKey(mSpaceKey);
    876     }
    877 
    878     public void updateAutoCorrectionState(boolean isAutoCorrection) {
    879         if (!mAutoCorrectionSpacebarLedEnabled) return;
    880         mAutoCorrectionSpacebarLedOn = isAutoCorrection;
    881         invalidateKey(mSpaceKey);
    882     }
    883 
    884     @Override
    885     protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
    886         if (key.altCodeWhileTyping() && key.isEnabled()) {
    887             params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
    888         }
    889         if (key.mCode == Keyboard.CODE_SPACE) {
    890             drawSpacebar(key, canvas, paint);
    891             // Whether space key needs to show the "..." popup hint for special purposes
    892             if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
    893                 drawKeyPopupHint(key, canvas, paint, params);
    894             }
    895         } else if (key.mCode == Keyboard.CODE_LANGUAGE_SWITCH) {
    896             super.onDrawKeyTopVisuals(key, canvas, paint, params);
    897             drawKeyPopupHint(key, canvas, paint, params);
    898         } else {
    899             super.onDrawKeyTopVisuals(key, canvas, paint, params);
    900         }
    901     }
    902 
    903     private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
    904         paint.setTextScaleX(1.0f);
    905         final float textWidth = getLabelWidth(text, paint);
    906         if (textWidth < width) return true;
    907 
    908         final float scaleX = width / textWidth;
    909         if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) return false;
    910 
    911         paint.setTextScaleX(scaleX);
    912         return getLabelWidth(text, paint) < width;
    913     }
    914 
    915     // Layout language name on spacebar.
    916     private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
    917             final int width) {
    918         // Choose appropriate language name to fit into the width.
    919         String text = getFullDisplayName(subtype, getResources());
    920         if (fitsTextIntoWidth(width, text, paint)) {
    921             return text;
    922         }
    923 
    924         text = getMiddleDisplayName(subtype);
    925         if (fitsTextIntoWidth(width, text, paint)) {
    926             return text;
    927         }
    928 
    929         text = getShortDisplayName(subtype);
    930         if (fitsTextIntoWidth(width, text, paint)) {
    931             return text;
    932         }
    933 
    934         return "";
    935     }
    936 
    937     private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
    938         final int width = key.mWidth;
    939         final int height = key.mHeight;
    940 
    941         // If input language are explicitly selected.
    942         if (mNeedsToDisplayLanguage) {
    943             paint.setTextAlign(Align.CENTER);
    944             paint.setTypeface(Typeface.DEFAULT);
    945             paint.setTextSize(mSpacebarTextSize);
    946             final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
    947             final String language = layoutLanguageOnSpacebar(paint, subtype, width);
    948             // Draw language text with shadow
    949             final float descent = paint.descent();
    950             final float textHeight = -paint.ascent() + descent;
    951             final float baseline = height / 2 + textHeight / 2;
    952             paint.setColor(mSpacebarTextShadowColor);
    953             paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
    954             canvas.drawText(language, width / 2, baseline - descent - 1, paint);
    955             paint.setColor(mSpacebarTextColor);
    956             paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
    957             canvas.drawText(language, width / 2, baseline - descent, paint);
    958         }
    959 
    960         // Draw the spacebar icon at the bottom
    961         if (mAutoCorrectionSpacebarLedOn) {
    962             final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
    963             final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
    964             int x = (width - iconWidth) / 2;
    965             int y = height - iconHeight;
    966             drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
    967         } else if (mSpaceIcon != null) {
    968             final int iconWidth = mSpaceIcon.getIntrinsicWidth();
    969             final int iconHeight = mSpaceIcon.getIntrinsicHeight();
    970             int x = (width - iconWidth) / 2;
    971             int y = height - iconHeight;
    972             drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
    973         }
    974     }
    975 
    976     // InputMethodSubtype's display name for spacebar text in its locale.
    977     //        isAdditionalSubtype (T=true, F=false)
    978     // locale layout | Short  Middle      Full
    979     // ------ ------ - ---- --------- ----------------------
    980     //  en_US qwerty F  En  English   English (US)           exception
    981     //  en_GB qwerty F  En  English   English (UK)           exception
    982     //  fr    azerty F  Fr  Franais  Franais
    983     //  fr_CA qwerty F  Fr  Franais  Franais (Canada)
    984     //  de    qwertz F  De  Deutsch   Deutsch
    985     //  zz    qwerty F      QWERTY    QWERTY
    986     //  fr    qwertz T  Fr  Franais  Franais (QWERTZ)
    987     //  de    qwerty T  De  Deutsch   Deutsch (QWERTY)
    988     //  en_US azerty T  En  English   English (US) (AZERTY)
    989     //  zz    azerty T      AZERTY    AZERTY
    990 
    991     // Get InputMethodSubtype's full display name in its locale.
    992     static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
    993         if (SubtypeLocale.isNoLanguage(subtype)) {
    994             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
    995         }
    996 
    997         return SubtypeLocale.getSubtypeDisplayName(subtype, res);
    998     }
    999 
   1000     // Get InputMethodSubtype's short display name in its locale.
   1001     static String getShortDisplayName(InputMethodSubtype subtype) {
   1002         if (SubtypeLocale.isNoLanguage(subtype)) {
   1003             return "";
   1004         }
   1005         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
   1006         return StringUtils.toTitleCase(locale.getLanguage(), locale);
   1007     }
   1008 
   1009     // Get InputMethodSubtype's middle display name in its locale.
   1010     static String getMiddleDisplayName(InputMethodSubtype subtype) {
   1011         if (SubtypeLocale.isNoLanguage(subtype)) {
   1012             return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
   1013         }
   1014         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
   1015         return StringUtils.toTitleCase(locale.getDisplayLanguage(locale), locale);
   1016     }
   1017 }
   1018