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