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