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