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