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