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