KeyboardView.java revision 520a297ad1d148a57bcf6559a9802d5d49182d70
1/* 2 * Copyright (C) 2010 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.content.Context; 20import android.content.pm.PackageManager; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.graphics.Bitmap; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.Paint; 27import android.graphics.Paint.Align; 28import android.graphics.PorterDuff; 29import android.graphics.Rect; 30import android.graphics.Region.Op; 31import android.graphics.Typeface; 32import android.graphics.drawable.Drawable; 33import android.os.Message; 34import android.util.AttributeSet; 35import android.util.Log; 36import android.util.TypedValue; 37import android.view.GestureDetector; 38import android.view.LayoutInflater; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.ViewConfiguration; 42import android.view.ViewGroup; 43import android.view.accessibility.AccessibilityEvent; 44import android.widget.PopupWindow; 45import android.widget.TextView; 46 47import com.android.inputmethod.accessibility.AccessibilityUtils; 48import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 49import com.android.inputmethod.compat.FrameLayoutCompatUtils; 50import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder; 51import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; 52import com.android.inputmethod.keyboard.internal.SwipeTracker; 53import com.android.inputmethod.latin.LatinImeLogger; 54import com.android.inputmethod.latin.R; 55import com.android.inputmethod.latin.StaticInnerHandlerWrapper; 56 57import java.util.ArrayList; 58import java.util.HashMap; 59import java.util.WeakHashMap; 60 61/** 62 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key 63 * presses and touch movements. 64 * 65 * @attr ref R.styleable#KeyboardView_backgroundDimAmount 66 * @attr ref R.styleable#KeyboardView_keyBackground 67 * @attr ref R.styleable#KeyboardView_keyHysteresisDistance 68 * @attr ref R.styleable#KeyboardView_keyLetterRatio 69 * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio 70 * @attr ref R.styleable#KeyboardView_keyLabelRatio 71 * @attr ref R.styleable#KeyboardView_keyHintLetterRatio 72 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio 73 * @attr ref R.styleable#KeyboardView_keyHintLabelRatio 74 * @attr ref R.styleable#KeyboardView_keyTextStyle 75 * @attr ref R.styleable#KeyboardView_keyPreviewLayout 76 * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio 77 * @attr ref R.styleable#KeyboardView_keyPreviewOffset 78 * @attr ref R.styleable#KeyboardView_keyPreviewHeight 79 * @attr ref R.styleable#KeyboardView_keyTextColor 80 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled 81 * @attr ref R.styleable#KeyboardView_keyHintLetterColor 82 * @attr ref R.styleable#KeyboardView_keyHintLabelColor 83 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor 84 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor 85 * @attr ref R.styleable#KeyboardView_verticalCorrection 86 * @attr ref R.styleable#KeyboardView_popupLayout 87 * @attr ref R.styleable#KeyboardView_shadowColor 88 * @attr ref R.styleable#KeyboardView_shadowRadius 89 */ 90public class KeyboardView extends View implements PointerTracker.UIProxy { 91 private static final String TAG = KeyboardView.class.getSimpleName(); 92 private static final boolean DEBUG_SHOW_ALIGN = false; 93 private static final boolean DEBUG_KEYBOARD_GRID = false; 94 95 private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true; 96 private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true; 97 98 // Timing constants 99 private final int mKeyRepeatInterval; 100 101 // Miscellaneous constants 102 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 103 private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1; 104 105 // XML attribute 106 private final int mKeyTextColor; 107 private final int mKeyTextInactivatedColor; 108 private final Typeface mKeyTextStyle; 109 private final float mKeyLetterRatio; 110 private final float mKeyLargeLetterRatio; 111 private final float mKeyLabelRatio; 112 private final float mKeyHintLetterRatio; 113 private final float mKeyUppercaseLetterRatio; 114 private final float mKeyHintLabelRatio; 115 private final int mShadowColor; 116 private final float mShadowRadius; 117 private final Drawable mKeyBackground; 118 private final float mBackgroundDimAmount; 119 private final float mKeyHysteresisDistance; 120 private final float mVerticalCorrection; 121 private final int mPreviewOffset; 122 private final int mPreviewHeight; 123 private final int mPopupLayout; 124 private final Drawable mKeyPopupHintIcon; 125 private final int mKeyHintLetterColor; 126 private final int mKeyHintLabelColor; 127 private final int mKeyUppercaseLetterInactivatedColor; 128 private final int mKeyUppercaseLetterActivatedColor; 129 130 // Main keyboard 131 private Keyboard mKeyboard; 132 private int mKeyLetterSize; 133 private int mKeyLargeLetterSize; 134 private int mKeyLabelSize; 135 private int mKeyHintLetterSize; 136 private int mKeyUppercaseLetterSize; 137 private int mKeyHintLabelSize; 138 139 // Key preview 140 private boolean mInForeground; 141 private TextView mPreviewText; 142 private Drawable mPreviewBackground; 143 private float mPreviewTextRatio; 144 private int mPreviewTextSize; 145 private boolean mShowKeyPreviewPopup = true; 146 private final int mDelayBeforePreview; 147 private int mDelayAfterPreview; 148 private ViewGroup mPreviewPlacer; 149 private final int[] mCoordinates = new int[2]; 150 151 // Mini keyboard 152 private PopupWindow mPopupWindow; 153 private PopupPanel mPopupMiniKeyboardPanel; 154 private final WeakHashMap<Key, PopupPanel> mPopupPanelCache = 155 new WeakHashMap<Key, PopupPanel>(); 156 157 /** Listener for {@link KeyboardActionListener}. */ 158 private KeyboardActionListener mKeyboardActionListener; 159 160 private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); 161 162 // TODO: Let the PointerTracker class manage this pointer queue 163 private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue(); 164 165 private final boolean mHasDistinctMultitouch; 166 private int mOldPointerCount = 1; 167 private int mOldKeyIndex; 168 169 protected KeyDetector mKeyDetector = new KeyDetector(); 170 171 // Swipe gesture detector 172 protected GestureDetector mGestureDetector; 173 private final SwipeTracker mSwipeTracker = new SwipeTracker(); 174 private final int mSwipeThreshold; 175 private final boolean mDisambiguateSwipe; 176 177 // Drawing 178 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 179 private boolean mDrawPending; 180 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ 181 private boolean mKeyboardChanged; 182 /** The dirty region in the keyboard bitmap */ 183 private final Rect mDirtyRect = new Rect(); 184 /** The key to invalidate. */ 185 private Key mInvalidatedKey; 186 /** The dirty region for single key drawing */ 187 private final Rect mInvalidatedKeyRect = new Rect(); 188 /** The keyboard bitmap for faster updates */ 189 private Bitmap mBuffer; 190 /** The canvas for the above mutable keyboard bitmap */ 191 private Canvas mCanvas; 192 private final Paint mPaint = new Paint(); 193 private final Rect mPadding = new Rect(); 194 // This map caches key label text height in pixel as value and key label text size as map key. 195 private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>(); 196 // This map caches key label text width in pixel as value and key label text size as map key. 197 private final HashMap<Integer, Integer> mTextWidthCache = new HashMap<Integer, Integer>(); 198 // Distance from horizontal center of the key, proportional to key label text height and width. 199 private static final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f; 200 private static final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f; 201 private static final String KEY_LABEL_REFERENCE_CHAR = "M"; 202 private final int mKeyLabelHorizontalPadding; 203 204 private final UIHandler mHandler = new UIHandler(this); 205 206 public static class UIHandler extends StaticInnerHandlerWrapper<KeyboardView> { 207 private static final int MSG_SHOW_KEY_PREVIEW = 1; 208 private static final int MSG_DISMISS_KEY_PREVIEW = 2; 209 private static final int MSG_REPEAT_KEY = 3; 210 private static final int MSG_LONGPRESS_KEY = 4; 211 private static final int MSG_LONGPRESS_SHIFT_KEY = 5; 212 private static final int MSG_IGNORE_DOUBLE_TAP = 6; 213 214 private boolean mInKeyRepeat; 215 216 public UIHandler(KeyboardView outerInstance) { 217 super(outerInstance); 218 } 219 220 @Override 221 public void handleMessage(Message msg) { 222 final KeyboardView keyboardView = getOuterInstance(); 223 final PointerTracker tracker = (PointerTracker) msg.obj; 224 switch (msg.what) { 225 case MSG_SHOW_KEY_PREVIEW: 226 keyboardView.showKey(msg.arg1, tracker); 227 break; 228 case MSG_DISMISS_KEY_PREVIEW: 229 keyboardView.mPreviewText.setVisibility(View.INVISIBLE); 230 break; 231 case MSG_REPEAT_KEY: 232 tracker.onRepeatKey(msg.arg1); 233 startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, msg.arg1, tracker); 234 break; 235 case MSG_LONGPRESS_KEY: 236 keyboardView.openMiniKeyboardIfRequired(msg.arg1, tracker); 237 break; 238 case MSG_LONGPRESS_SHIFT_KEY: 239 keyboardView.onLongPressShiftKey(tracker); 240 break; 241 } 242 } 243 244 public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) { 245 final KeyboardView keyboardView = getOuterInstance(); 246 removeMessages(MSG_SHOW_KEY_PREVIEW); 247 if (keyboardView.mPreviewText.getVisibility() == VISIBLE || delay == 0) { 248 // Show right away, if it's already visible and finger is moving around 249 keyboardView.showKey(keyIndex, tracker); 250 } else { 251 sendMessageDelayed( 252 obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay); 253 } 254 } 255 256 public void cancelShowKeyPreview(PointerTracker tracker) { 257 removeMessages(MSG_SHOW_KEY_PREVIEW, tracker); 258 } 259 260 public void cancelAllShowKeyPreviews() { 261 removeMessages(MSG_SHOW_KEY_PREVIEW); 262 } 263 264 public void dismissKeyPreview(long delay, PointerTracker tracker) { 265 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 266 } 267 268 public void cancelDismissKeyPreview(PointerTracker tracker) { 269 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 270 } 271 272 public void cancelAllDismissKeyPreviews() { 273 removeMessages(MSG_DISMISS_KEY_PREVIEW); 274 } 275 276 public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { 277 mInKeyRepeat = true; 278 sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); 279 } 280 281 public void cancelKeyRepeatTimer() { 282 mInKeyRepeat = false; 283 removeMessages(MSG_REPEAT_KEY); 284 } 285 286 public boolean isInKeyRepeat() { 287 return mInKeyRepeat; 288 } 289 290 public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { 291 cancelLongPressTimers(); 292 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); 293 } 294 295 public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) { 296 cancelLongPressTimers(); 297 if (ENABLE_CAPSLOCK_BY_LONGPRESS) { 298 sendMessageDelayed( 299 obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay); 300 } 301 } 302 303 public void cancelLongPressTimers() { 304 removeMessages(MSG_LONGPRESS_KEY); 305 removeMessages(MSG_LONGPRESS_SHIFT_KEY); 306 } 307 308 public void cancelKeyTimers() { 309 cancelKeyRepeatTimer(); 310 cancelLongPressTimers(); 311 removeMessages(MSG_IGNORE_DOUBLE_TAP); 312 } 313 314 public void startIgnoringDoubleTap() { 315 sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP), 316 ViewConfiguration.getDoubleTapTimeout()); 317 } 318 319 public boolean isIgnoringDoubleTap() { 320 return hasMessages(MSG_IGNORE_DOUBLE_TAP); 321 } 322 323 public void cancelAllMessages() { 324 cancelKeyTimers(); 325 cancelAllShowKeyPreviews(); 326 cancelAllDismissKeyPreviews(); 327 } 328 } 329 330 public KeyboardView(Context context, AttributeSet attrs) { 331 this(context, attrs, R.attr.keyboardViewStyle); 332 } 333 334 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 335 super(context, attrs, defStyle); 336 337 final TypedArray a = context.obtainStyledAttributes( 338 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 339 340 mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); 341 mKeyHysteresisDistance = a.getDimensionPixelOffset( 342 R.styleable.KeyboardView_keyHysteresisDistance, 0); 343 mVerticalCorrection = a.getDimensionPixelOffset( 344 R.styleable.KeyboardView_verticalCorrection, 0); 345 final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); 346 mPreviewOffset = a.getDimensionPixelOffset(R.styleable.KeyboardView_keyPreviewOffset, 0); 347 mPreviewHeight = a.getDimensionPixelSize(R.styleable.KeyboardView_keyPreviewHeight, 80); 348 mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio); 349 mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio); 350 mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio); 351 mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio); 352 mKeyUppercaseLetterRatio = getRatio(a, 353 R.styleable.KeyboardView_keyUppercaseLetterRatio); 354 mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio); 355 mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio); 356 mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000); 357 mKeyTextInactivatedColor = a.getColor( 358 R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000); 359 mKeyPopupHintIcon = a.getDrawable(R.styleable.KeyboardView_keyPopupHintIcon); 360 mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0); 361 mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0); 362 mKeyUppercaseLetterInactivatedColor = a.getColor( 363 R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0); 364 mKeyUppercaseLetterActivatedColor = a.getColor( 365 R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0); 366 mKeyTextStyle = Typeface.defaultFromStyle( 367 a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL)); 368 mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0); 369 mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0); 370 mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f); 371 // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) 372 mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f); 373 a.recycle(); 374 375 final Resources res = getResources(); 376 377 if (previewLayout != 0) { 378 mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null); 379 mPreviewBackground = mPreviewText.getBackground(); 380 } else { 381 mShowKeyPreviewPopup = false; 382 } 383 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 384 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 385 mKeyLabelHorizontalPadding = (int)res.getDimension( 386 R.dimen.key_label_horizontal_alignment_padding); 387 388 mPaint.setAntiAlias(true); 389 mPaint.setTextAlign(Align.CENTER); 390 mPaint.setAlpha(255); 391 392 mKeyBackground.getPadding(mPadding); 393 394 mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); 395 // TODO: Refer to frameworks/base/core/res/res/values/config.xml 396 mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); 397 398 GestureDetector.SimpleOnGestureListener listener = 399 new GestureDetector.SimpleOnGestureListener() { 400 private boolean mProcessingShiftDoubleTapEvent = false; 401 402 @Override 403 public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, 404 float velocityY) { 405 final float absX = Math.abs(velocityX); 406 final float absY = Math.abs(velocityY); 407 float deltaY = me2.getY() - me1.getY(); 408 int travelY = getHeight() / 2; // Half the keyboard height 409 mSwipeTracker.computeCurrentVelocity(1000); 410 final float endingVelocityY = mSwipeTracker.getYVelocity(); 411 if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 412 if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { 413 onSwipeDown(); 414 return true; 415 } 416 } 417 return false; 418 } 419 420 @Override 421 public boolean onDoubleTap(MotionEvent firstDown) { 422 if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard 423 && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) { 424 final int pointerIndex = firstDown.getActionIndex(); 425 final int id = firstDown.getPointerId(pointerIndex); 426 final PointerTracker tracker = getPointerTracker(id); 427 // If the first down event is on shift key. 428 if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) { 429 mProcessingShiftDoubleTapEvent = true; 430 return true; 431 } 432 } 433 mProcessingShiftDoubleTapEvent = false; 434 return false; 435 } 436 437 @Override 438 public boolean onDoubleTapEvent(MotionEvent secondTap) { 439 if (mProcessingShiftDoubleTapEvent 440 && secondTap.getAction() == MotionEvent.ACTION_DOWN) { 441 final MotionEvent secondDown = secondTap; 442 final int pointerIndex = secondDown.getActionIndex(); 443 final int id = secondDown.getPointerId(pointerIndex); 444 final PointerTracker tracker = getPointerTracker(id); 445 // If the second down event is also on shift key. 446 if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) { 447 // Detected a double tap on shift key. If we are in the ignoring double tap 448 // mode, it means we have already turned off caps lock in 449 // {@link KeyboardSwitcher#onReleaseShift} . 450 final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap(); 451 if (!ignoringDoubleTap) 452 onDoubleTapShiftKey(tracker); 453 return true; 454 } 455 // Otherwise these events should not be handled as double tap. 456 mProcessingShiftDoubleTapEvent = false; 457 } 458 return mProcessingShiftDoubleTapEvent; 459 } 460 }; 461 462 final boolean ignoreMultitouch = true; 463 mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); 464 mGestureDetector.setIsLongpressEnabled(false); 465 466 mHasDistinctMultitouch = context.getPackageManager() 467 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 468 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 469 } 470 471 // Read fraction value in TypedArray as float. 472 private static float getRatio(TypedArray a, int index) { 473 return a.getFraction(index, 1000, 1000, 1) / 1000.0f; 474 } 475 476 public void startIgnoringDoubleTap() { 477 if (ENABLE_CAPSLOCK_BY_DOUBLETAP) 478 mHandler.startIgnoringDoubleTap(); 479 } 480 481 public void setOnKeyboardActionListener(KeyboardActionListener listener) { 482 mKeyboardActionListener = listener; 483 for (PointerTracker tracker : mPointerTrackers) { 484 tracker.setOnKeyboardActionListener(listener); 485 } 486 } 487 488 /** 489 * Returns the {@link KeyboardActionListener} object. 490 * @return the listener attached to this keyboard 491 */ 492 protected KeyboardActionListener getOnKeyboardActionListener() { 493 return mKeyboardActionListener; 494 } 495 496 @Override 497 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 498 // TODO: Should notify InputMethodService instead? 499 KeyboardSwitcher.getInstance().onSizeChanged(); 500 } 501 502 /** 503 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 504 * view will re-layout itself to accommodate the keyboard. 505 * @see Keyboard 506 * @see #getKeyboard() 507 * @param keyboard the keyboard to display in this view 508 */ 509 public void setKeyboard(Keyboard keyboard) { 510 if (mKeyboard != null) { 511 dismissAllKeyPreviews(); 512 } 513 // Remove any pending messages, except dismissing preview 514 mHandler.cancelKeyTimers(); 515 mHandler.cancelAllShowKeyPreviews(); 516 mKeyboard = keyboard; 517 LatinImeLogger.onSetKeyboard(keyboard); 518 mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), 519 -getPaddingTop() + mVerticalCorrection); 520 for (PointerTracker tracker : mPointerTrackers) { 521 tracker.setKeyboard(keyboard, mKeyHysteresisDistance); 522 } 523 requestLayout(); 524 mKeyboardChanged = true; 525 invalidateAllKeys(); 526 mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth()); 527 mPopupPanelCache.clear(); 528 final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap(); 529 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 530 mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio); 531 mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio); 532 mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio); 533 mKeyUppercaseLetterSize = (int)( 534 keyHeight * mKeyUppercaseLetterRatio); 535 mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio); 536 mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); 537 } 538 539 /** 540 * Returns the current keyboard being displayed by this view. 541 * @return the currently attached keyboard 542 * @see #setKeyboard(Keyboard) 543 */ 544 public Keyboard getKeyboard() { 545 return mKeyboard; 546 } 547 548 /** 549 * Returns whether the device has distinct multi-touch panel. 550 * @return true if the device has distinct multi-touch panel. 551 */ 552 @Override 553 public boolean hasDistinctMultitouch() { 554 return mHasDistinctMultitouch; 555 } 556 557 /** 558 * Enables or disables the key feedback popup. This is a popup that shows a magnified 559 * version of the depressed key. By default the preview is enabled. 560 * @param previewEnabled whether or not to enable the key feedback preview 561 * @param delay the delay after which the preview is dismissed 562 * @see #isKeyPreviewPopupEnabled() 563 */ 564 public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { 565 mShowKeyPreviewPopup = previewEnabled; 566 mDelayAfterPreview = delay; 567 } 568 569 /** 570 * Returns the enabled state of the key feedback preview 571 * @return whether or not the key feedback preview is enabled 572 * @see #setKeyPreviewPopupEnabled(boolean, int) 573 */ 574 public boolean isKeyPreviewPopupEnabled() { 575 return mShowKeyPreviewPopup; 576 } 577 578 /** 579 * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key 580 * codes for adjacent keys. When disabled, only the primary key code will be 581 * reported. 582 * @param enabled whether or not the proximity correction is enabled 583 */ 584 public void setProximityCorrectionEnabled(boolean enabled) { 585 mKeyDetector.setProximityCorrectionEnabled(enabled); 586 } 587 588 /** 589 * Returns true if proximity correction is enabled. 590 */ 591 public boolean isProximityCorrectionEnabled() { 592 return mKeyDetector.isProximityCorrectionEnabled(); 593 } 594 595 protected CharSequence adjustCase(CharSequence label) { 596 if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3 597 && Character.isLowerCase(label.charAt(0))) { 598 return label.toString().toUpperCase(mKeyboard.mId.mLocale); 599 } 600 return label; 601 } 602 603 @Override 604 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 605 // Round up a little 606 if (mKeyboard == null) { 607 setMeasuredDimension( 608 getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); 609 } else { 610 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 611 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 612 width = MeasureSpec.getSize(widthMeasureSpec); 613 } 614 setMeasuredDimension( 615 width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 616 } 617 } 618 619 @Override 620 public void onDraw(Canvas canvas) { 621 super.onDraw(canvas); 622 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 623 onBufferDraw(); 624 } 625 canvas.drawBitmap(mBuffer, 0, 0, null); 626 } 627 628 private void onBufferDraw() { 629 final int width = getWidth(); 630 final int height = getHeight(); 631 if (width == 0 || height == 0) 632 return; 633 if (mBuffer == null || mKeyboardChanged) { 634 mKeyboardChanged = false; 635 mDirtyRect.union(0, 0, width, height); 636 } 637 if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { 638 if (mBuffer != null) 639 mBuffer.recycle(); 640 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 641 if (mCanvas != null) { 642 mCanvas.setBitmap(mBuffer); 643 } else { 644 mCanvas = new Canvas(mBuffer); 645 } 646 } 647 final Canvas canvas = mCanvas; 648 canvas.clipRect(mDirtyRect, Op.REPLACE); 649 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 650 651 if (mKeyboard == null) return; 652 653 if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) { 654 // Draw a single key. 655 onBufferDrawKey(canvas, mInvalidatedKey); 656 } else { 657 // Draw all keys. 658 for (final Key key : mKeyboard.getKeys()) { 659 onBufferDrawKey(canvas, key); 660 } 661 } 662 663 // TODO: Move this function to ProximityInfo for getting rid of 664 // public declarations for 665 // GRID_WIDTH and GRID_HEIGHT 666 if (DEBUG_KEYBOARD_GRID) { 667 Paint p = new Paint(); 668 p.setStyle(Paint.Style.STROKE); 669 p.setStrokeWidth(1.0f); 670 p.setColor(0x800000c0); 671 int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) 672 / mKeyboard.GRID_WIDTH; 673 int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) 674 / mKeyboard.GRID_HEIGHT; 675 for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) 676 canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); 677 for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) 678 canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); 679 } 680 681 // Overlay a dark rectangle to dim the keyboard 682 if (mPopupMiniKeyboardPanel != null) { 683 mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 684 canvas.drawRect(0, 0, width, height, mPaint); 685 } 686 687 mInvalidatedKey = null; 688 mDrawPending = false; 689 mDirtyRect.setEmpty(); 690 } 691 692 private void onBufferDrawKey(final Canvas canvas, final Key key) { 693 final Paint paint = mPaint; 694 final Drawable keyBackground = mKeyBackground; 695 final Rect padding = mPadding; 696 final int kbdPaddingLeft = getPaddingLeft(); 697 final int kbdPaddingTop = getPaddingTop(); 698 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 699 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 700 final int centerX = (keyDrawWidth + padding.left - padding.right) / 2; 701 final float centerY = (key.mHeight + padding.top - padding.bottom) / 2; 702 final int rowHeight = padding.top + key.mHeight; 703 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 704 705 canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop); 706 707 // Draw key background. 708 final int[] drawableState = key.getCurrentDrawableState(); 709 keyBackground.setState(drawableState); 710 final Rect bounds = keyBackground.getBounds(); 711 if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) { 712 keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight); 713 } 714 keyBackground.draw(canvas); 715 716 // Draw key label. 717 int positionX = centerX; 718 if (key.mLabel != null) { 719 // Switch the character to uppercase if shift is pressed 720 final CharSequence label = key.mLabel == null ? null : adjustCase(key.mLabel); 721 // For characters, use large font. For labels like "Done", use smaller font. 722 paint.setTypeface(key.selectTypeface(mKeyTextStyle)); 723 final int labelSize = key.selectTextSize(mKeyLetterSize, mKeyLargeLetterSize, 724 mKeyLabelSize, mKeyHintLabelSize); 725 paint.setTextSize(labelSize); 726 final int labelCharHeight = getLabelCharHeight(paint); 727 final int labelCharWidth = getLabelCharWidth(paint); 728 729 // Vertical label text alignment. 730 final float baseline; 731 // TODO: Generalize the following calculations. 732 if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_BOTTOM) != 0) { 733 baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; 734 } else { // Align center 735 baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; 736 } 737 738 // Horizontal label text alignment 739 if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) { 740 positionX = padding.left + mKeyLabelHorizontalPadding; 741 paint.setTextAlign(Align.LEFT); 742 } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) { 743 positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right; 744 paint.setTextAlign(Align.RIGHT); 745 } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT_OF_CENTER) != 0) { 746 // TODO: Parameterise this? 747 positionX = centerX - labelCharWidth * 7 / 4; 748 paint.setTextAlign(Align.LEFT); 749 } else { 750 positionX = centerX; 751 paint.setTextAlign(Align.CENTER); 752 } 753 if (DEBUG_SHOW_ALIGN) { 754 final Paint line = new Paint(); 755 drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, line); 756 drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, line); 757 } 758 759 if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) { 760 paint.setColor(mKeyTextInactivatedColor); 761 } else { 762 paint.setColor(mKeyTextColor); 763 } 764 if (key.isEnabled()) { 765 // Set a drop shadow for the text 766 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 767 } else { 768 // Make label invisible 769 paint.setColor(Color.TRANSPARENT); 770 } 771 canvas.drawText(label, 0, label.length(), positionX, baseline, paint); 772 // Turn off drop shadow 773 paint.setShadowLayer(0, 0, 0, 0); 774 775 } 776 777 // Draw hint label. 778 if (key.mHintLabel != null) { 779 final CharSequence hint = key.mHintLabel; 780 final int hintColor; 781 final int hintSize; 782 if (key.hasUppercaseLetter()) { 783 hintColor = isManualTemporaryUpperCase ? mKeyUppercaseLetterActivatedColor 784 : mKeyUppercaseLetterInactivatedColor; 785 hintSize = mKeyUppercaseLetterSize; 786 } else if (key.hasHintLabel()) { 787 hintColor = mKeyHintLabelColor; 788 hintSize = mKeyHintLabelSize; 789 paint.setTypeface(Typeface.DEFAULT); 790 } else { 791 hintColor = mKeyHintLetterColor; 792 hintSize = mKeyHintLetterSize; 793 } 794 paint.setColor(hintColor); 795 paint.setTextSize(hintSize); 796 // Note: padding.right for drawX? 797 final float hintX, hintY; 798 if (key.hasHintLabel()) { 799 // TODO: Generalize the following calculations. 800 hintX = positionX + getLabelCharWidth(paint) * 2; 801 hintY = centerY + getLabelCharHeight(paint) / 2; 802 } else { 803 hintX = keyDrawWidth - getLabelCharWidth(paint); 804 hintY = -paint.ascent() + padding.top; 805 } 806 canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint); 807 } 808 809 // Draw key icon. 810 final Drawable icon = key.getIcon(); 811 if (key.mLabel == null && icon != null) { 812 final int iconWidth = icon.getIntrinsicWidth(); 813 final int iconHeight = icon.getIntrinsicHeight(); 814 final int iconX, alignX; 815 final int iconY = (key.mHeight + padding.top - padding.bottom - iconHeight) / 2; 816 if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) { 817 iconX = padding.left + mKeyLabelHorizontalPadding; 818 alignX = iconX; 819 } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) { 820 iconX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding - iconWidth; 821 alignX = iconX + iconWidth; 822 } else { // Align center 823 iconX = (keyDrawWidth + padding.left - padding.right - iconWidth) / 2; 824 alignX = iconX + iconWidth / 2; 825 } 826 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 827 if (DEBUG_SHOW_ALIGN) { 828 final Paint line = new Paint(); 829 drawVerticalLine(canvas, alignX, rowHeight, 0xc0800080, line); 830 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); 831 } 832 } 833 834 // Draw popup hint icon "...". 835 // TODO: Draw "..." by text. 836 if (key.hasPopupHint()) { 837 final int drawableWidth = keyDrawWidth; 838 final int drawableHeight = key.mHeight; 839 final int drawableX = 0; 840 final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; 841 final Drawable hintIcon = mKeyPopupHintIcon; 842 drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight); 843 if (DEBUG_SHOW_ALIGN) { 844 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 845 0x80c0c000, new Paint()); 846 } 847 } 848 849 canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop); 850 } 851 852 // This method is currently being used only by MiniKeyboardBuilder 853 public int getDefaultLabelSizeAndSetPaint(Paint paint) { 854 // For characters, use large font. For labels like "Done", use small font. 855 final int labelSize = mKeyLabelSize; 856 paint.setTextSize(labelSize); 857 paint.setTypeface(mKeyTextStyle); 858 return labelSize; 859 } 860 861 private final Rect mTextBounds = new Rect(); 862 863 private int getLabelCharHeight(Paint paint) { 864 final int labelSize = (int)paint.getTextSize(); 865 final Integer cachedValue = mTextHeightCache.get(labelSize); 866 if (cachedValue != null) 867 return cachedValue; 868 869 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds); 870 final int height = mTextBounds.height(); 871 mTextHeightCache.put(labelSize, height); 872 return height; 873 } 874 875 private int getLabelCharWidth(Paint paint) { 876 final int labelSize = (int)paint.getTextSize(); 877 final Typeface face = paint.getTypeface(); 878 final Integer key; 879 if (face == Typeface.DEFAULT) { 880 key = labelSize; 881 } else if (face == Typeface.DEFAULT_BOLD) { 882 key = labelSize + 1000; 883 } else if (face == Typeface.MONOSPACE) { 884 key = labelSize + 2000; 885 } else { 886 key = labelSize; 887 } 888 889 final Integer cached = mTextWidthCache.get(key); 890 if (cached != null) 891 return cached; 892 893 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds); 894 final int width = mTextBounds.width(); 895 mTextWidthCache.put(key, width); 896 return width; 897 } 898 899 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 900 int height) { 901 canvas.translate(x, y); 902 icon.setBounds(0, 0, width, height); 903 icon.draw(canvas); 904 canvas.translate(-x, -y); 905 } 906 907 private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { 908 paint.setStyle(Paint.Style.STROKE); 909 paint.setStrokeWidth(1.0f); 910 paint.setColor(color); 911 canvas.drawLine(0, y, w, y, paint); 912 } 913 914 private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { 915 paint.setStyle(Paint.Style.STROKE); 916 paint.setStrokeWidth(1.0f); 917 paint.setColor(color); 918 canvas.drawLine(x, 0, x, h, paint); 919 } 920 921 private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, 922 Paint paint) { 923 paint.setStyle(Paint.Style.STROKE); 924 paint.setStrokeWidth(1.0f); 925 paint.setColor(color); 926 canvas.translate(x, y); 927 canvas.drawRect(0, 0, w, h, paint); 928 canvas.translate(-x, -y); 929 } 930 931 public void setForeground(boolean foreground) { 932 mInForeground = foreground; 933 } 934 935 // TODO: clean up this method. 936 private void dismissAllKeyPreviews() { 937 for (PointerTracker tracker : mPointerTrackers) { 938 tracker.setReleasedKeyGraphics(); 939 dismissKeyPreview(tracker); 940 } 941 } 942 943 @Override 944 public void showKeyPreview(int keyIndex, PointerTracker tracker) { 945 if (mShowKeyPreviewPopup) { 946 mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker); 947 } else if (mKeyboard.needSpacebarPreview(keyIndex)) { 948 // Show key preview (in this case, slide language switcher) without any delay. 949 showKey(keyIndex, tracker); 950 } 951 } 952 953 @Override 954 public void dismissKeyPreview(PointerTracker tracker) { 955 if (mShowKeyPreviewPopup) { 956 mHandler.cancelShowKeyPreview(tracker); 957 mHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 958 } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) { 959 // Dismiss key preview (in this case, slide language switcher) without any delay. 960 mPreviewText.setVisibility(View.INVISIBLE); 961 } 962 } 963 964 private void addKeyPreview(TextView keyPreview) { 965 if (mPreviewPlacer == null) { 966 mPreviewPlacer = FrameLayoutCompatUtils.getPlacer( 967 (ViewGroup)getRootView().findViewById(android.R.id.content)); 968 } 969 final ViewGroup placer = mPreviewPlacer; 970 placer.addView(keyPreview, FrameLayoutCompatUtils.newLayoutParam(placer, 0, 0)); 971 } 972 973 // TODO: Introduce minimum duration for displaying key previews 974 // TODO: Display up to two key previews when the user presses two keys at the same time 975 private void showKey(final int keyIndex, PointerTracker tracker) { 976 final TextView previewText = mPreviewText; 977 // If the key preview has no parent view yet, add it to the ViewGroup which can place 978 // key preview absolutely in SoftInputWindow. 979 if (previewText.getParent() == null) { 980 addKeyPreview(previewText); 981 } 982 983 final Key key = tracker.getKey(keyIndex); 984 // If keyIndex is invalid or IME is already closed, we must not show key preview. 985 // Trying to show key preview while root window is closed causes 986 // WindowManager.BadTokenException. 987 if (key == null || !mInForeground) 988 return; 989 990 mHandler.cancelAllDismissKeyPreviews(); 991 992 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 993 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 994 // What we show as preview should match what we show on key top in onBufferDraw(). 995 if (key.mLabel != null) { 996 // TODO Should take care of temporaryShiftLabel here. 997 previewText.setCompoundDrawables(null, null, null, null); 998 if (key.mLabel.length() > 1) { 999 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); 1000 previewText.setTypeface(Typeface.DEFAULT_BOLD); 1001 } else { 1002 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSize); 1003 previewText.setTypeface(mKeyTextStyle); 1004 } 1005 previewText.setText(adjustCase(tracker.getPreviewText(key))); 1006 } else { 1007 final Drawable previewIcon = key.getPreviewIcon(); 1008 previewText.setCompoundDrawables(null, null, null, 1009 previewIcon != null ? previewIcon : key.getIcon()); 1010 previewText.setText(null); 1011 } 1012 if (key.mCode == Keyboard.CODE_SPACE) { 1013 previewText.setBackgroundColor(Color.TRANSPARENT); 1014 } else { 1015 previewText.setBackgroundDrawable(mPreviewBackground); 1016 } 1017 // Set the preview background state 1018 previewText.getBackground().setState( 1019 key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 1020 1021 previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 1022 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 1023 final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth 1024 + previewText.getPaddingLeft() + previewText.getPaddingRight()); 1025 final int previewHeight = mPreviewHeight; 1026 getLocationInWindow(mCoordinates); 1027 final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0]; 1028 final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset; 1029 1030 // Place the key preview. 1031 // TODO: Adjust position of key previews which touch screen edges 1032 FrameLayoutCompatUtils.placeViewAt( 1033 previewText, previewX, previewY, previewWidth, previewHeight); 1034 previewText.setVisibility(VISIBLE); 1035 } 1036 1037 /** 1038 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 1039 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 1040 * draws the cached buffer. 1041 * @see #invalidateKey(Key) 1042 */ 1043 public void invalidateAllKeys() { 1044 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1045 mDrawPending = true; 1046 invalidate(); 1047 } 1048 1049 /** 1050 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 1051 * one key is changing it's content. Any changes that affect the position or size of the key 1052 * may not be honored. 1053 * @param key key in the attached {@link Keyboard}. 1054 * @see #invalidateAllKeys 1055 */ 1056 @Override 1057 public void invalidateKey(Key key) { 1058 if (key == null) 1059 return; 1060 mInvalidatedKey = key; 1061 final int x = key.mX + getPaddingLeft(); 1062 final int y = key.mY + getPaddingTop(); 1063 mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight); 1064 mDirtyRect.union(mInvalidatedKeyRect); 1065 onBufferDraw(); 1066 invalidate(mInvalidatedKeyRect); 1067 } 1068 1069 private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) { 1070 // Check if we have a popup layout specified first. 1071 if (mPopupLayout == 0) { 1072 return false; 1073 } 1074 1075 final Key parentKey = tracker.getKey(keyIndex); 1076 if (parentKey == null) 1077 return false; 1078 boolean result = onLongPress(parentKey, tracker); 1079 if (result) { 1080 dismissAllKeyPreviews(); 1081 tracker.onLongPressed(mPointerQueue); 1082 } 1083 return result; 1084 } 1085 1086 private void onLongPressShiftKey(PointerTracker tracker) { 1087 tracker.onLongPressed(mPointerQueue); 1088 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1089 } 1090 1091 private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { 1092 // When shift key is double tapped, the first tap is correctly processed as usual tap. And 1093 // the second tap is treated as this double tap event, so that we need not mark tracker 1094 // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. 1095 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1096 } 1097 1098 // This default implementation returns a popup mini keyboard panel. 1099 // A derived class may return a language switcher popup panel, for instance. 1100 protected PopupPanel onCreatePopupPanel(Key parentKey) { 1101 if (parentKey.mPopupCharacters == null) 1102 return null; 1103 1104 final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); 1105 if (container == null) 1106 throw new NullPointerException(); 1107 1108 final PopupMiniKeyboardView miniKeyboardView = 1109 (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view); 1110 miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { 1111 @Override 1112 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1113 mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); 1114 dismissMiniKeyboard(); 1115 } 1116 1117 @Override 1118 public void onTextInput(CharSequence text) { 1119 mKeyboardActionListener.onTextInput(text); 1120 dismissMiniKeyboard(); 1121 } 1122 1123 @Override 1124 public void onCancelInput() { 1125 mKeyboardActionListener.onCancelInput(); 1126 dismissMiniKeyboard(); 1127 } 1128 1129 @Override 1130 public void onSwipeDown() { 1131 // Nothing to do. 1132 } 1133 @Override 1134 public void onPress(int primaryCode, boolean withSliding) { 1135 mKeyboardActionListener.onPress(primaryCode, withSliding); 1136 } 1137 @Override 1138 public void onRelease(int primaryCode, boolean withSliding) { 1139 mKeyboardActionListener.onRelease(primaryCode, withSliding); 1140 } 1141 }); 1142 1143 final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(), 1144 parentKey, mKeyboard).build(); 1145 miniKeyboardView.setKeyboard(keyboard); 1146 1147 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1148 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 1149 1150 return miniKeyboardView; 1151 } 1152 1153 /** 1154 * Called when a key is long pressed. By default this will open mini keyboard associated 1155 * with this key. 1156 * @param parentKey the key that was long pressed 1157 * @param tracker the pointer tracker which pressed the parent key 1158 * @return true if the long press is handled, false otherwise. Subclasses should call the 1159 * method on the base class if the subclass doesn't wish to handle the call. 1160 */ 1161 protected boolean onLongPress(Key parentKey, PointerTracker tracker) { 1162 PopupPanel popupPanel = mPopupPanelCache.get(parentKey); 1163 if (popupPanel == null) { 1164 popupPanel = onCreatePopupPanel(parentKey); 1165 if (popupPanel == null) 1166 return false; 1167 mPopupPanelCache.put(parentKey, popupPanel); 1168 } 1169 if (mPopupWindow == null) { 1170 mPopupWindow = new PopupWindow(getContext()); 1171 mPopupWindow.setBackgroundDrawable(null); 1172 mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation); 1173 // Allow popup window to be drawn off the screen. 1174 mPopupWindow.setClippingEnabled(false); 1175 } 1176 mPopupMiniKeyboardPanel = popupPanel; 1177 popupPanel.showPanel(this, parentKey, tracker, mPopupWindow); 1178 1179 invalidateAllKeys(); 1180 return true; 1181 } 1182 1183 private PointerTracker getPointerTracker(final int id) { 1184 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1185 final KeyboardActionListener listener = mKeyboardActionListener; 1186 1187 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1188 for (int i = pointers.size(); i <= id; i++) { 1189 final PointerTracker tracker = 1190 new PointerTracker(i, this, mHandler, mKeyDetector, this); 1191 if (mKeyboard != null) 1192 tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance); 1193 if (listener != null) 1194 tracker.setOnKeyboardActionListener(listener); 1195 pointers.add(tracker); 1196 } 1197 1198 return pointers.get(id); 1199 } 1200 1201 public boolean isInSlidingKeyInput() { 1202 if (mPopupMiniKeyboardPanel != null) { 1203 return mPopupMiniKeyboardPanel.isInSlidingKeyInput(); 1204 } else { 1205 return mPointerQueue.isInSlidingKeyInput(); 1206 } 1207 } 1208 1209 public int getPointerCount() { 1210 return mOldPointerCount; 1211 } 1212 1213 @Override 1214 public boolean onTouchEvent(MotionEvent me) { 1215 final int action = me.getActionMasked(); 1216 final int pointerCount = me.getPointerCount(); 1217 final int oldPointerCount = mOldPointerCount; 1218 mOldPointerCount = pointerCount; 1219 1220 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1221 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1222 // events except a transition from/to single-touch. 1223 if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { 1224 return true; 1225 } 1226 1227 // Track the last few movements to look for spurious swipes. 1228 mSwipeTracker.addMovement(me); 1229 1230 // Gesture detector must be enabled only when mini-keyboard is not on the screen. 1231 if (mPopupMiniKeyboardPanel == null && mGestureDetector != null 1232 && mGestureDetector.onTouchEvent(me)) { 1233 dismissAllKeyPreviews(); 1234 mHandler.cancelKeyTimers(); 1235 return true; 1236 } 1237 1238 final long eventTime = me.getEventTime(); 1239 final int index = me.getActionIndex(); 1240 final int id = me.getPointerId(index); 1241 final int x = (int)me.getX(index); 1242 final int y = (int)me.getY(index); 1243 1244 // Needs to be called after the gesture detector gets a turn, as it may have displayed the 1245 // mini keyboard 1246 if (mPopupMiniKeyboardPanel != null) { 1247 return mPopupMiniKeyboardPanel.onTouchEvent(me); 1248 } 1249 1250 if (mHandler.isInKeyRepeat()) { 1251 final PointerTracker tracker = getPointerTracker(id); 1252 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1253 // event (UP or DOWN) is non-modifier key. 1254 if (pointerCount > 1 && !tracker.isModifier()) { 1255 mHandler.cancelKeyRepeatTimer(); 1256 } 1257 // Up event will pass through. 1258 } 1259 1260 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1261 // Translate mutli-touch event to single-touch events on the device that has no distinct 1262 // multi-touch panel. 1263 if (!mHasDistinctMultitouch) { 1264 // Use only main (id=0) pointer tracker. 1265 PointerTracker tracker = getPointerTracker(0); 1266 if (pointerCount == 1 && oldPointerCount == 2) { 1267 // Multi-touch to single touch transition. 1268 // Send a down event for the latest pointer if the key is different from the 1269 // previous key. 1270 final int newKeyIndex = tracker.getKeyIndexOn(x, y); 1271 if (mOldKeyIndex != newKeyIndex) { 1272 tracker.onDownEvent(x, y, eventTime, null); 1273 if (action == MotionEvent.ACTION_UP) 1274 tracker.onUpEvent(x, y, eventTime, null); 1275 } 1276 } else if (pointerCount == 2 && oldPointerCount == 1) { 1277 // Single-touch to multi-touch transition. 1278 // Send an up event for the last pointer. 1279 final int lastX = tracker.getLastX(); 1280 final int lastY = tracker.getLastY(); 1281 mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY); 1282 tracker.onUpEvent(lastX, lastY, eventTime, null); 1283 } else if (pointerCount == 1 && oldPointerCount == 1) { 1284 tracker.onTouchEvent(action, x, y, eventTime, null); 1285 } else { 1286 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1287 + " (old " + oldPointerCount + ")"); 1288 } 1289 return true; 1290 } 1291 1292 final PointerTrackerQueue queue = mPointerQueue; 1293 if (action == MotionEvent.ACTION_MOVE) { 1294 for (int i = 0; i < pointerCount; i++) { 1295 final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1296 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); 1297 } 1298 } else { 1299 final PointerTracker tracker = getPointerTracker(id); 1300 switch (action) { 1301 case MotionEvent.ACTION_DOWN: 1302 case MotionEvent.ACTION_POINTER_DOWN: 1303 tracker.onDownEvent(x, y, eventTime, queue); 1304 break; 1305 case MotionEvent.ACTION_UP: 1306 case MotionEvent.ACTION_POINTER_UP: 1307 tracker.onUpEvent(x, y, eventTime, queue); 1308 break; 1309 case MotionEvent.ACTION_CANCEL: 1310 tracker.onCancelEvent(x, y, eventTime, queue); 1311 break; 1312 } 1313 } 1314 1315 return true; 1316 } 1317 1318 protected void onSwipeDown() { 1319 mKeyboardActionListener.onSwipeDown(); 1320 } 1321 1322 public void closing() { 1323 mPreviewText.setVisibility(View.GONE); 1324 mHandler.cancelAllMessages(); 1325 1326 dismissMiniKeyboard(); 1327 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1328 mPopupPanelCache.clear(); 1329 requestLayout(); 1330 } 1331 1332 public void purgeKeyboardAndClosing() { 1333 mKeyboard = null; 1334 closing(); 1335 } 1336 1337 @Override 1338 public void onDetachedFromWindow() { 1339 super.onDetachedFromWindow(); 1340 closing(); 1341 } 1342 1343 private boolean dismissMiniKeyboard() { 1344 if (mPopupWindow != null && mPopupWindow.isShowing()) { 1345 mPopupWindow.dismiss(); 1346 mPopupMiniKeyboardPanel = null; 1347 invalidateAllKeys(); 1348 return true; 1349 } 1350 return false; 1351 } 1352 1353 public boolean handleBack() { 1354 return dismissMiniKeyboard(); 1355 } 1356 1357 @Override 1358 public boolean dispatchTouchEvent(MotionEvent event) { 1359 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 1360 return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event) 1361 || super.dispatchTouchEvent(event); 1362 } 1363 1364 return super.dispatchTouchEvent(event); 1365 } 1366 1367 @Override 1368 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 1369 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 1370 final PointerTracker tracker = getPointerTracker(0); 1371 return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent( 1372 event, tracker) || super.dispatchPopulateAccessibilityEvent(event); 1373 } 1374 1375 return super.dispatchPopulateAccessibilityEvent(event); 1376 } 1377 1378 public boolean onHoverEvent(MotionEvent event) { 1379 // Since reflection doesn't support calling superclass methods, this 1380 // method checks for the existence of onHoverEvent() in the View class 1381 // before returning a value. 1382 if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { 1383 final PointerTracker tracker = getPointerTracker(0); 1384 return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker); 1385 } 1386 1387 return false; 1388 } 1389} 1390