KeyboardView.java revision da4bfbbbb3d3aa5dce8f0507d3f3b07ea3e0f26f
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 mPreviewPopup.setClippingEnabled(false); 381 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 382 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 383 mKeyLabelHorizontalPadding = (int)res.getDimension( 384 R.dimen.key_label_horizontal_alignment_padding); 385 386 mMiniKeyboardParent = this; 387 mMiniKeyboardPopup = new PopupWindow(context); 388 mMiniKeyboardPopup.setBackgroundDrawable(null); 389 mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation); 390 // Allow popup window to be drawn off the screen. 391 mMiniKeyboardPopup.setClippingEnabled(false); 392 393 mPaint = new Paint(); 394 mPaint.setAntiAlias(true); 395 mPaint.setTextSize(keyTextSize); 396 mPaint.setTextAlign(Align.CENTER); 397 mPaint.setAlpha(255); 398 399 mPadding = new Rect(0, 0, 0, 0); 400 mKeyBackground.getPadding(mPadding); 401 402 mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); 403 // TODO: Refer frameworks/base/core/res/res/values/config.xml 404 mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); 405 mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance); 406 mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean( 407 R.bool.config_show_mini_keyboard_at_touched_point); 408 409 GestureDetector.SimpleOnGestureListener listener = 410 new GestureDetector.SimpleOnGestureListener() { 411 private boolean mProcessingShiftDoubleTapEvent = false; 412 413 @Override 414 public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, 415 float velocityY) { 416 final float absX = Math.abs(velocityX); 417 final float absY = Math.abs(velocityY); 418 float deltaY = me2.getY() - me1.getY(); 419 int travelY = getHeight() / 2; // Half the keyboard height 420 mSwipeTracker.computeCurrentVelocity(1000); 421 final float endingVelocityY = mSwipeTracker.getYVelocity(); 422 if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 423 if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { 424 onSwipeDown(); 425 return true; 426 } 427 } 428 return false; 429 } 430 431 @Override 432 public boolean onDoubleTap(MotionEvent firstDown) { 433 if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard 434 && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) { 435 final int pointerIndex = firstDown.getActionIndex(); 436 final int id = firstDown.getPointerId(pointerIndex); 437 final PointerTracker tracker = getPointerTracker(id); 438 // If the first down event is on shift key. 439 if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) { 440 mProcessingShiftDoubleTapEvent = true; 441 return true; 442 } 443 } 444 mProcessingShiftDoubleTapEvent = false; 445 return false; 446 } 447 448 @Override 449 public boolean onDoubleTapEvent(MotionEvent secondTap) { 450 if (mProcessingShiftDoubleTapEvent 451 && secondTap.getAction() == MotionEvent.ACTION_DOWN) { 452 final MotionEvent secondDown = secondTap; 453 final int pointerIndex = secondDown.getActionIndex(); 454 final int id = secondDown.getPointerId(pointerIndex); 455 final PointerTracker tracker = getPointerTracker(id); 456 // If the second down event is also on shift key. 457 if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) { 458 onDoubleTapShiftKey(tracker); 459 return true; 460 } 461 // Otherwise these events should not be handled as double tap. 462 mProcessingShiftDoubleTapEvent = false; 463 } 464 return mProcessingShiftDoubleTapEvent; 465 } 466 }; 467 468 final boolean ignoreMultitouch = true; 469 mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); 470 mGestureDetector.setIsLongpressEnabled(false); 471 472 mHasDistinctMultitouch = context.getPackageManager() 473 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 474 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 475 } 476 477 public void setOnKeyboardActionListener(KeyboardActionListener listener) { 478 mKeyboardActionListener = listener; 479 for (PointerTracker tracker : mPointerTrackers) { 480 tracker.setOnKeyboardActionListener(listener); 481 } 482 } 483 484 /** 485 * Returns the {@link KeyboardActionListener} object. 486 * @return the listener attached to this keyboard 487 */ 488 protected KeyboardActionListener getOnKeyboardActionListener() { 489 return mKeyboardActionListener; 490 } 491 492 /** 493 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 494 * view will re-layout itself to accommodate the keyboard. 495 * @see Keyboard 496 * @see #getKeyboard() 497 * @param keyboard the keyboard to display in this view 498 */ 499 public void setKeyboard(Keyboard keyboard) { 500 if (mKeyboard != null) { 501 dismissKeyPreview(); 502 } 503 // Remove any pending messages, except dismissing preview 504 mHandler.cancelKeyTimers(); 505 mHandler.cancelPopupPreview(); 506 mKeyboard = keyboard; 507 LatinImeLogger.onSetKeyboard(keyboard); 508 mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), 509 -getPaddingTop() + mVerticalCorrection); 510 for (PointerTracker tracker : mPointerTrackers) { 511 tracker.setKeyboard(keyboard, mKeys, mKeyHysteresisDistance); 512 } 513 requestLayout(); 514 mKeyboardChanged = true; 515 invalidateAllKeys(); 516 mKeyDetector.setProximityThreshold(KeyDetector.getMostCommonKeyWidth(keyboard)); 517 mMiniKeyboardCache.clear(); 518 } 519 520 /** 521 * Returns the current keyboard being displayed by this view. 522 * @return the currently attached keyboard 523 * @see #setKeyboard(Keyboard) 524 */ 525 public Keyboard getKeyboard() { 526 return mKeyboard; 527 } 528 529 /** 530 * Returns whether the device has distinct multi-touch panel. 531 * @return true if the device has distinct multi-touch panel. 532 */ 533 @Override 534 public boolean hasDistinctMultitouch() { 535 return mHasDistinctMultitouch; 536 } 537 538 /** 539 * Enables or disables accessibility. 540 * @param accessibilityEnabled whether or not to enable accessibility 541 */ 542 public void setAccessibilityEnabled(boolean accessibilityEnabled) { 543 mIsAccessibilityEnabled = accessibilityEnabled; 544 545 // Propagate this change to all existing pointer trackers. 546 for (PointerTracker tracker : mPointerTrackers) { 547 tracker.setAccessibilityEnabled(accessibilityEnabled); 548 } 549 } 550 551 /** 552 * Returns whether the device has accessibility enabled. 553 * @return true if the device has accessibility enabled. 554 */ 555 @Override 556 public boolean isAccessibilityEnabled() { 557 return mIsAccessibilityEnabled; 558 } 559 560 /** 561 * Enables or disables the key feedback popup. This is a popup that shows a magnified 562 * version of the depressed key. By default the preview is enabled. 563 * @param previewEnabled whether or not to enable the key feedback popup 564 * @see #isPreviewEnabled() 565 */ 566 public void setPreviewEnabled(boolean previewEnabled) { 567 mShowPreview = previewEnabled; 568 } 569 570 /** 571 * Returns the enabled state of the key feedback popup. 572 * @return whether or not the key feedback popup is enabled 573 * @see #setPreviewEnabled(boolean) 574 */ 575 public boolean isPreviewEnabled() { 576 return mShowPreview; 577 } 578 579 public int getColorScheme() { 580 return mColorScheme; 581 } 582 583 public void setPopupOffset(int x, int y) { 584 mPopupPreviewOffsetX = x; 585 mPopupPreviewOffsetY = y; 586 mPreviewPopup.dismiss(); 587 } 588 589 /** 590 * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key 591 * codes for adjacent keys. When disabled, only the primary key code will be 592 * reported. 593 * @param enabled whether or not the proximity correction is enabled 594 */ 595 public void setProximityCorrectionEnabled(boolean enabled) { 596 mKeyDetector.setProximityCorrectionEnabled(enabled); 597 } 598 599 /** 600 * Returns true if proximity correction is enabled. 601 */ 602 public boolean isProximityCorrectionEnabled() { 603 return mKeyDetector.isProximityCorrectionEnabled(); 604 } 605 606 protected CharSequence adjustCase(CharSequence label) { 607 if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3 608 && Character.isLowerCase(label.charAt(0))) { 609 return label.toString().toUpperCase(); 610 } 611 return label; 612 } 613 614 @Override 615 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 616 // Round up a little 617 if (mKeyboard == null) { 618 setMeasuredDimension( 619 getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); 620 } else { 621 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 622 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 623 width = MeasureSpec.getSize(widthMeasureSpec); 624 } 625 setMeasuredDimension( 626 width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 627 } 628 } 629 630 @Override 631 public void onDraw(Canvas canvas) { 632 super.onDraw(canvas); 633 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 634 onBufferDraw(); 635 } 636 canvas.drawBitmap(mBuffer, 0, 0, null); 637 } 638 639 private void onBufferDraw() { 640 final int width = getWidth(); 641 final int height = getHeight(); 642 if (width == 0 || height == 0) 643 return; 644 if (mBuffer == null || mKeyboardChanged) { 645 mKeyboardChanged = false; 646 mDirtyRect.union(0, 0, width, height); 647 } 648 if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { 649 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 650 mCanvas = new Canvas(mBuffer); 651 } 652 final Canvas canvas = mCanvas; 653 canvas.clipRect(mDirtyRect, Op.REPLACE); 654 655 if (mKeyboard == null) return; 656 657 final Paint paint = mPaint; 658 final Drawable keyBackground = mKeyBackground; 659 final Rect padding = mPadding; 660 final int kbdPaddingLeft = getPaddingLeft(); 661 final int kbdPaddingTop = getPaddingTop(); 662 final Key[] keys = mKeys; 663 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 664 final boolean drawSingleKey = (mInvalidatedKey != null 665 && mInvalidatedKeyRect.contains(mDirtyRect)); 666 667 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 668 final int keyCount = keys.length; 669 for (int i = 0; i < keyCount; i++) { 670 final Key key = keys[i]; 671 if (drawSingleKey && key != mInvalidatedKey) { 672 continue; 673 } 674 int[] drawableState = key.getCurrentDrawableState(); 675 keyBackground.setState(drawableState); 676 677 // Switch the character to uppercase if shift is pressed 678 String label = key.mLabel == null? null : adjustCase(key.mLabel).toString(); 679 680 final Rect bounds = keyBackground.getBounds(); 681 if (key.mWidth != bounds.right || key.mHeight != bounds.bottom) { 682 keyBackground.setBounds(0, 0, key.mWidth, key.mHeight); 683 } 684 canvas.translate(key.mX + kbdPaddingLeft, key.mY + kbdPaddingTop); 685 keyBackground.draw(canvas); 686 687 final int rowHeight = padding.top + key.mHeight; 688 // Draw key label 689 if (label != null) { 690 // For characters, use large font. For labels like "Done", use small font. 691 final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint); 692 final int labelCharHeight = getLabelCharHeight(labelSize, paint); 693 694 // Vertical label text alignment. 695 final float baseline; 696 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) { 697 baseline = key.mHeight - 698 + labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; 699 if (DEBUG_SHOW_ALIGN) 700 drawHorizontalLine(canvas, (int)baseline, key.mWidth, 0xc0008000, 701 new Paint()); 702 } else { // Align center 703 final float centerY = (key.mHeight + padding.top - padding.bottom) / 2; 704 baseline = centerY 705 + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; 706 if (DEBUG_SHOW_ALIGN) 707 drawHorizontalLine(canvas, (int)baseline, key.mWidth, 0xc0008000, 708 new Paint()); 709 } 710 // Horizontal label text alignment 711 final int positionX; 712 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 713 positionX = mKeyLabelHorizontalPadding + padding.left; 714 paint.setTextAlign(Align.LEFT); 715 if (DEBUG_SHOW_ALIGN) 716 drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint()); 717 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 718 positionX = key.mWidth - mKeyLabelHorizontalPadding - padding.right; 719 paint.setTextAlign(Align.RIGHT); 720 if (DEBUG_SHOW_ALIGN) 721 drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint()); 722 } else { 723 positionX = (key.mWidth + padding.left - padding.right) / 2; 724 paint.setTextAlign(Align.CENTER); 725 if (DEBUG_SHOW_ALIGN) { 726 if (label.length() > 1) 727 drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint()); 728 } 729 } 730 if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) { 731 paint.setColor(mKeyTextColorDisabled); 732 } else { 733 paint.setColor(mKeyTextColor); 734 } 735 if (key.mEnabled) { 736 // Set a drop shadow for the text 737 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 738 } else { 739 // Make label invisible 740 paint.setColor(Color.TRANSPARENT); 741 } 742 canvas.drawText(label, positionX, baseline, paint); 743 // Turn off drop shadow 744 paint.setShadowLayer(0, 0, 0, 0); 745 } 746 // Draw key icon 747 final Drawable icon = key.getIcon(); 748 if (key.mLabel == null && icon != null) { 749 final int drawableWidth = icon.getIntrinsicWidth(); 750 final int drawableHeight = icon.getIntrinsicHeight(); 751 final int drawableX; 752 final int drawableY = ( 753 key.mHeight + padding.top - padding.bottom - drawableHeight) / 2; 754 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 755 drawableX = padding.left + mKeyLabelHorizontalPadding; 756 if (DEBUG_SHOW_ALIGN) 757 drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint()); 758 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 759 drawableX = key.mWidth - padding.right - mKeyLabelHorizontalPadding 760 - drawableWidth; 761 if (DEBUG_SHOW_ALIGN) 762 drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight, 763 0xc0808000, new Paint()); 764 } else { // Align center 765 drawableX = (key.mWidth + padding.left - padding.right - drawableWidth) / 2; 766 if (DEBUG_SHOW_ALIGN) 767 drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight, 768 0xc0008080, new Paint()); 769 } 770 drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight); 771 if (DEBUG_SHOW_ALIGN) 772 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 773 0x80c00000, new Paint()); 774 } 775 if (key.mHintIcon != null) { 776 final int drawableWidth = key.mWidth; 777 final int drawableHeight = key.mHeight; 778 final int drawableX = 0; 779 final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; 780 Drawable hintIcon = (isManualTemporaryUpperCase 781 && key.mManualTemporaryUpperCaseHintIcon != null) 782 ? key.mManualTemporaryUpperCaseHintIcon : key.mHintIcon; 783 drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight); 784 if (DEBUG_SHOW_ALIGN) 785 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 786 0x80c0c000, new Paint()); 787 } 788 canvas.translate(-key.mX - kbdPaddingLeft, -key.mY - kbdPaddingTop); 789 } 790 791 // TODO: Move this function to ProximityInfo for getting rid of public declarations for 792 // GRID_WIDTH and GRID_HEIGHT 793 if (DEBUG_KEYBOARD_GRID) { 794 Paint p = new Paint(); 795 p.setStyle(Paint.Style.STROKE); 796 p.setStrokeWidth(1.0f); 797 p.setColor(0x800000c0); 798 int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) / mKeyboard.GRID_WIDTH; 799 int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) / mKeyboard.GRID_HEIGHT; 800 for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) 801 canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); 802 for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) 803 canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); 804 } 805 806 // Overlay a dark rectangle to dim the keyboard 807 if (mMiniKeyboardView != null) { 808 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 809 canvas.drawRect(0, 0, width, height, paint); 810 } 811 812 mInvalidatedKey = null; 813 mDrawPending = false; 814 mDirtyRect.setEmpty(); 815 } 816 817 public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) { 818 // For characters, use large font. For labels like "Done", use small font. 819 final int labelSize; 820 final Typeface labelStyle; 821 if (label.length() > 1) { 822 labelSize = mLabelTextSize; 823 if ((keyLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) { 824 labelStyle = Typeface.DEFAULT; 825 } else { 826 labelStyle = Typeface.DEFAULT_BOLD; 827 } 828 } else { 829 labelSize = mKeyLetterSize; 830 labelStyle = mKeyLetterStyle; 831 } 832 paint.setTextSize(labelSize); 833 paint.setTypeface(labelStyle); 834 return labelSize; 835 } 836 837 private int getLabelCharHeight(int labelSize, Paint paint) { 838 Integer labelHeightValue = mTextHeightCache.get(labelSize); 839 final int labelCharHeight; 840 if (labelHeightValue != null) { 841 labelCharHeight = labelHeightValue; 842 } else { 843 Rect textBounds = new Rect(); 844 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds); 845 labelCharHeight = textBounds.height(); 846 mTextHeightCache.put(labelSize, labelCharHeight); 847 } 848 return labelCharHeight; 849 } 850 851 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 852 int height) { 853 canvas.translate(x, y); 854 icon.setBounds(0, 0, width, height); 855 icon.draw(canvas); 856 canvas.translate(-x, -y); 857 } 858 859 private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { 860 paint.setStyle(Paint.Style.STROKE); 861 paint.setStrokeWidth(1.0f); 862 paint.setColor(color); 863 canvas.drawLine(0, y, w, y, paint); 864 } 865 866 private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { 867 paint.setStyle(Paint.Style.STROKE); 868 paint.setStrokeWidth(1.0f); 869 paint.setColor(color); 870 canvas.drawLine(x, 0, x, h, paint); 871 } 872 873 private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, 874 Paint paint) { 875 paint.setStyle(Paint.Style.STROKE); 876 paint.setStrokeWidth(1.0f); 877 paint.setColor(color); 878 canvas.translate(x, y); 879 canvas.drawRect(0, 0, w, h, paint); 880 canvas.translate(-x, -y); 881 } 882 883 public void setForeground(boolean foreground) { 884 mInForeground = foreground; 885 } 886 887 // TODO: clean up this method. 888 private void dismissKeyPreview() { 889 for (PointerTracker tracker : mPointerTrackers) 890 tracker.releaseKey(); 891 showPreview(KeyDetector.NOT_A_KEY, null); 892 } 893 894 @Override 895 public void showPreview(int keyIndex, PointerTracker tracker) { 896 int oldKeyIndex = mOldPreviewKeyIndex; 897 mOldPreviewKeyIndex = keyIndex; 898 // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show 899 // the space key preview and 3) pointer moves off the space key to other letter key, we 900 // should hide the preview of the previous key. 901 final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) 902 || (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher() 903 && SubtypeSwitcher.getInstance().needsToDisplayLanguage() 904 && (tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex))); 905 // If key changed and preview is on or the key is space (language switch is enabled) 906 if (oldKeyIndex != keyIndex && (mShowPreview || (hidePreviewOrShowSpaceKeyPreview))) { 907 if (keyIndex == KeyDetector.NOT_A_KEY) { 908 mHandler.cancelPopupPreview(); 909 mHandler.dismissPreview(mDelayAfterPreview); 910 } else if (tracker != null) { 911 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker); 912 } 913 } 914 } 915 916 // TODO Must fix popup preview on xlarge layout 917 private void showKey(final int keyIndex, PointerTracker tracker) { 918 Key key = tracker.getKey(keyIndex); 919 // If keyIndex is invalid or IME is already closed, we must not show key preview. 920 // Trying to show preview PopupWindow while root window is closed causes 921 // WindowManager.BadTokenException. 922 if (key == null || !mInForeground) 923 return; 924 // What we show as preview should match what we show on key top in onBufferDraw(). 925 if (key.mLabel != null) { 926 // TODO Should take care of temporaryShiftLabel here. 927 mPreviewText.setCompoundDrawables(null, null, null, null); 928 mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); 929 if (key.mLabel.length() > 1) { 930 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); 931 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 932 } else { 933 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 934 mPreviewText.setTypeface(mKeyLetterStyle); 935 } 936 } else { 937 final Drawable previewIcon = key.getPreviewIcon(); 938 mPreviewText.setCompoundDrawables(null, null, null, 939 previewIcon != null ? previewIcon : key.getIcon()); 940 mPreviewText.setText(null); 941 } 942 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 943 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 944 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.mWidth 945 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 946 final int popupHeight = mPreviewHeight; 947 LayoutParams lp = mPreviewText.getLayoutParams(); 948 if (lp != null) { 949 lp.width = popupWidth; 950 lp.height = popupHeight; 951 } 952 953 int popupPreviewX = key.mX - (popupWidth - key.mWidth) / 2; 954 int popupPreviewY = key.mY - popupHeight + mPreviewOffset; 955 956 mHandler.cancelDismissPreview(); 957 if (mOffsetInWindow == null) { 958 mOffsetInWindow = new int[2]; 959 getLocationInWindow(mOffsetInWindow); 960 mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero 961 mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero 962 int[] windowLocation = new int[2]; 963 getLocationOnScreen(windowLocation); 964 mWindowY = windowLocation[1]; 965 } 966 // Set the preview background state 967 mPreviewText.getBackground().setState( 968 key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 969 popupPreviewX += mOffsetInWindow[0]; 970 popupPreviewY += mOffsetInWindow[1]; 971 972 // If the popup cannot be shown above the key, put it on the side 973 if (popupPreviewY + mWindowY < 0) { 974 // If the key you're pressing is on the left side of the keyboard, show the popup on 975 // the right, offset by enough to see at least one key to the left/right. 976 if (key.mX + key.mWidth <= getWidth() / 2) { 977 popupPreviewX += (int) (key.mWidth * 2.5); 978 } else { 979 popupPreviewX -= (int) (key.mWidth * 2.5); 980 } 981 popupPreviewY += popupHeight; 982 } 983 984 try { 985 if (mPreviewPopup.isShowing()) { 986 mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); 987 } else { 988 mPreviewPopup.setWidth(popupWidth); 989 mPreviewPopup.setHeight(popupHeight); 990 mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, 991 popupPreviewX, popupPreviewY); 992 } 993 } catch (WindowManager.BadTokenException e) { 994 // Swallow the exception which will be happened when IME is already closed. 995 Log.w(TAG, "LatinIME is already closed when tried showing key preview."); 996 } 997 // Record popup preview position to display mini-keyboard later at the same positon 998 mPopupPreviewDisplayedY = popupPreviewY; 999 mPreviewText.setVisibility(VISIBLE); 1000 } 1001 1002 /** 1003 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 1004 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 1005 * draws the cached buffer. 1006 * @see #invalidateKey(Key) 1007 */ 1008 public void invalidateAllKeys() { 1009 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1010 mDrawPending = true; 1011 invalidate(); 1012 } 1013 1014 /** 1015 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 1016 * one key is changing it's content. Any changes that affect the position or size of the key 1017 * may not be honored. 1018 * @param key key in the attached {@link Keyboard}. 1019 * @see #invalidateAllKeys 1020 */ 1021 @Override 1022 public void invalidateKey(Key key) { 1023 if (key == null) 1024 return; 1025 mInvalidatedKey = key; 1026 mInvalidatedKeyRect.set(0, 0, key.mWidth, key.mHeight); 1027 mInvalidatedKeyRect.offset(key.mX + getPaddingLeft(), key.mY + getPaddingTop()); 1028 mDirtyRect.union(mInvalidatedKeyRect); 1029 onBufferDraw(); 1030 invalidate(mInvalidatedKeyRect); 1031 } 1032 1033 private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) { 1034 // Check if we have a popup layout specified first. 1035 if (mPopupLayout == 0) { 1036 return false; 1037 } 1038 1039 Key popupKey = tracker.getKey(keyIndex); 1040 if (popupKey == null) 1041 return false; 1042 boolean result = onLongPress(popupKey, tracker); 1043 if (result) { 1044 dismissKeyPreview(); 1045 mMiniKeyboardTrackerId = tracker.mPointerId; 1046 // Mark this tracker "already processed" and remove it from the pointer queue 1047 tracker.setAlreadyProcessed(); 1048 mPointerQueue.remove(tracker); 1049 } 1050 return result; 1051 } 1052 1053 private void onLongPressShiftKey(PointerTracker tracker) { 1054 tracker.setAlreadyProcessed(); 1055 mPointerQueue.remove(tracker); 1056 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1057 } 1058 1059 private void onDoubleTapShiftKey(PointerTracker tracker) { 1060 // When shift key is double tapped, the first tap is correctly processed as usual tap. And 1061 // the second tap is treated as this double tap event, so that we need not mark tracker 1062 // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. 1063 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1064 } 1065 1066 private View inflateMiniKeyboardContainer(Key popupKey) { 1067 final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); 1068 if (container == null) 1069 throw new NullPointerException(); 1070 1071 final KeyboardView miniKeyboardView = 1072 (KeyboardView)container.findViewById(R.id.KeyboardView); 1073 miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { 1074 @Override 1075 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1076 mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); 1077 dismissPopupKeyboard(); 1078 } 1079 1080 @Override 1081 public void onTextInput(CharSequence text) { 1082 mKeyboardActionListener.onTextInput(text); 1083 dismissPopupKeyboard(); 1084 } 1085 1086 @Override 1087 public void onCancelInput() { 1088 mKeyboardActionListener.onCancelInput(); 1089 dismissPopupKeyboard(); 1090 } 1091 1092 @Override 1093 public void onSwipeDown() { 1094 // Nothing to do. 1095 } 1096 @Override 1097 public void onPress(int primaryCode, boolean withSliding) { 1098 mKeyboardActionListener.onPress(primaryCode, withSliding); 1099 } 1100 @Override 1101 public void onRelease(int primaryCode, boolean withSliding) { 1102 mKeyboardActionListener.onRelease(primaryCode, withSliding); 1103 } 1104 }); 1105 // Override default ProximityKeyDetector. 1106 miniKeyboardView.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance); 1107 // Remove gesture detector on mini-keyboard 1108 miniKeyboardView.mGestureDetector = null; 1109 1110 final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(), 1111 popupKey).build(); 1112 miniKeyboardView.setKeyboard(keyboard); 1113 miniKeyboardView.mMiniKeyboardParent = this; 1114 1115 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1116 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1117 1118 return container; 1119 } 1120 1121 private static boolean isOneRowKeys(List<Key> keys) { 1122 if (keys.size() == 0) return false; 1123 final int edgeFlags = keys.get(0).mEdgeFlags; 1124 // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows, 1125 // does not have both top and bottom edge flags on at the same time. On the other hand, 1126 // the first key of mini keyboard that was created with popupCharacters must have both top 1127 // and bottom edge flags on. 1128 // When you want to use one row mini-keyboard from xml file, make sure that the row has 1129 // both top and bottom edge flags set. 1130 return (edgeFlags & Keyboard.EDGE_TOP) != 0 1131 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; 1132 } 1133 1134 /** 1135 * Called when a key is long pressed. By default this will open any popup keyboard associated 1136 * with this key through the attributes popupLayout and popupCharacters. 1137 * @param popupKey the key that was long pressed 1138 * @return true if the long press is handled, false otherwise. Subclasses should call the 1139 * method on the base class if the subclass doesn't wish to handle the call. 1140 */ 1141 protected boolean onLongPress(Key popupKey, PointerTracker tracker) { 1142 if (popupKey.mPopupCharacters == null) 1143 return false; 1144 1145 View container = mMiniKeyboardCache.get(popupKey); 1146 if (container == null) { 1147 container = inflateMiniKeyboardContainer(popupKey); 1148 mMiniKeyboardCache.put(popupKey, container); 1149 } 1150 mMiniKeyboardView = (KeyboardView)container.findViewById(R.id.KeyboardView); 1151 final MiniKeyboard miniKeyboard = (MiniKeyboard)mMiniKeyboardView.getKeyboard(); 1152 1153 if (mWindowOffset == null) { 1154 mWindowOffset = new int[2]; 1155 getLocationInWindow(mWindowOffset); 1156 } 1157 final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX() 1158 : popupKey.mX + popupKey.mWidth / 2; 1159 final int popupX = pointX - miniKeyboard.getDefaultCoordX() 1160 - container.getPaddingLeft() 1161 + getPaddingLeft() + mWindowOffset[0]; 1162 final int popupY = popupKey.mY - mKeyboard.getVerticalGap() 1163 - (container.getMeasuredHeight() - container.getPaddingBottom()) 1164 + getPaddingTop() + mWindowOffset[1]; 1165 final int x = popupX; 1166 final int y = mShowPreview && isOneRowKeys(miniKeyboard.getKeys()) 1167 ? mPopupPreviewDisplayedY : popupY; 1168 1169 mMiniKeyboardOriginX = x + container.getPaddingLeft() - mWindowOffset[0]; 1170 mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; 1171 mMiniKeyboardView.setPopupOffset(x, y); 1172 if (miniKeyboard.setShifted( 1173 mKeyboard == null ? false : mKeyboard.isShiftedOrShiftLocked())) { 1174 mMiniKeyboardView.invalidateAllKeys(); 1175 } 1176 // Mini keyboard needs no pop-up key preview displayed. 1177 mMiniKeyboardView.setPreviewEnabled(false); 1178 mMiniKeyboardPopup.setContentView(container); 1179 mMiniKeyboardPopup.setWidth(container.getMeasuredWidth()); 1180 mMiniKeyboardPopup.setHeight(container.getMeasuredHeight()); 1181 mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1182 1183 // Inject down event on the key to mini keyboard. 1184 final long eventTime = SystemClock.uptimeMillis(); 1185 mMiniKeyboardPopupTime = eventTime; 1186 final MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, 1187 pointX, popupKey.mY + popupKey.mHeight / 2, eventTime); 1188 mMiniKeyboardView.onTouchEvent(downEvent); 1189 downEvent.recycle(); 1190 1191 invalidateAllKeys(); 1192 return true; 1193 } 1194 1195 private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) { 1196 return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action, 1197 x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0); 1198 } 1199 1200 private PointerTracker getPointerTracker(final int id) { 1201 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1202 final Key[] keys = mKeys; 1203 final KeyboardActionListener listener = mKeyboardActionListener; 1204 1205 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1206 for (int i = pointers.size(); i <= id; i++) { 1207 final PointerTracker tracker = 1208 new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); 1209 if (keys != null) 1210 tracker.setKeyboard(mKeyboard, keys, mKeyHysteresisDistance); 1211 if (listener != null) 1212 tracker.setOnKeyboardActionListener(listener); 1213 pointers.add(tracker); 1214 } 1215 1216 return pointers.get(id); 1217 } 1218 1219 public boolean isInSlidingKeyInput() { 1220 if (mMiniKeyboardView != null) { 1221 return mMiniKeyboardView.isInSlidingKeyInput(); 1222 } else { 1223 return mPointerQueue.isInSlidingKeyInput(); 1224 } 1225 } 1226 1227 public int getPointerCount() { 1228 return mOldPointerCount; 1229 } 1230 1231 @Override 1232 public boolean onTouchEvent(MotionEvent me) { 1233 final int action = me.getActionMasked(); 1234 final int pointerCount = me.getPointerCount(); 1235 final int oldPointerCount = mOldPointerCount; 1236 mOldPointerCount = pointerCount; 1237 1238 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1239 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1240 // events except a transition from/to single-touch. 1241 if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled) 1242 && pointerCount > 1 && oldPointerCount > 1) { 1243 return true; 1244 } 1245 1246 // Track the last few movements to look for spurious swipes. 1247 mSwipeTracker.addMovement(me); 1248 1249 // Gesture detector must be enabled only when mini-keyboard is not on the screen and 1250 // accessibility is not enabled. 1251 // TODO: Reconcile gesture detection and accessibility features. 1252 if (mMiniKeyboardView == null && !mIsAccessibilityEnabled 1253 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { 1254 dismissKeyPreview(); 1255 mHandler.cancelKeyTimers(); 1256 return true; 1257 } 1258 1259 final long eventTime = me.getEventTime(); 1260 final int index = me.getActionIndex(); 1261 final int id = me.getPointerId(index); 1262 final int x = (int)me.getX(index); 1263 final int y = (int)me.getY(index); 1264 1265 // Needs to be called after the gesture detector gets a turn, as it may have 1266 // displayed the mini keyboard 1267 if (mMiniKeyboardView != null) { 1268 final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId); 1269 if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) { 1270 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex); 1271 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex); 1272 MotionEvent translated = generateMiniKeyboardMotionEvent(action, 1273 miniKeyboardX, miniKeyboardY, eventTime); 1274 mMiniKeyboardView.onTouchEvent(translated); 1275 translated.recycle(); 1276 } 1277 return true; 1278 } 1279 1280 if (mHandler.isInKeyRepeat()) { 1281 // It will keep being in the key repeating mode while the key is being pressed. 1282 if (action == MotionEvent.ACTION_MOVE) { 1283 return true; 1284 } 1285 final PointerTracker tracker = getPointerTracker(id); 1286 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1287 // event (UP or DOWN) is non-modifier key. 1288 if (pointerCount > 1 && !tracker.isModifier()) { 1289 mHandler.cancelKeyRepeatTimer(); 1290 } 1291 // Up event will pass through. 1292 } 1293 1294 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1295 // Translate mutli-touch event to single-touch events on the device that has no distinct 1296 // multi-touch panel. 1297 if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) { 1298 // Use only main (id=0) pointer tracker. 1299 PointerTracker tracker = getPointerTracker(0); 1300 if (pointerCount == 1 && oldPointerCount == 2) { 1301 // Multi-touch to single touch transition. 1302 // Send a down event for the latest pointer. 1303 tracker.onDownEvent(x, y, eventTime, null); 1304 } else if (pointerCount == 2 && oldPointerCount == 1) { 1305 // Single-touch to multi-touch transition. 1306 // Send an up event for the last pointer. 1307 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime, null); 1308 } else if (pointerCount == 1 && oldPointerCount == 1) { 1309 tracker.onTouchEvent(action, x, y, eventTime, null); 1310 } else { 1311 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1312 + " (old " + oldPointerCount + ")"); 1313 } 1314 return true; 1315 } 1316 1317 final PointerTrackerQueue queue = mPointerQueue; 1318 if (action == MotionEvent.ACTION_MOVE) { 1319 for (int i = 0; i < pointerCount; i++) { 1320 final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1321 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); 1322 } 1323 } else { 1324 final PointerTracker tracker = getPointerTracker(id); 1325 switch (action) { 1326 case MotionEvent.ACTION_DOWN: 1327 case MotionEvent.ACTION_POINTER_DOWN: 1328 tracker.onDownEvent(x, y, eventTime, queue); 1329 break; 1330 case MotionEvent.ACTION_UP: 1331 case MotionEvent.ACTION_POINTER_UP: 1332 tracker.onUpEvent(x, y, eventTime, queue); 1333 break; 1334 case MotionEvent.ACTION_CANCEL: 1335 tracker.onCancelEvent(x, y, eventTime, queue); 1336 break; 1337 } 1338 } 1339 1340 return true; 1341 } 1342 1343 protected void onSwipeDown() { 1344 mKeyboardActionListener.onSwipeDown(); 1345 } 1346 1347 public void closing() { 1348 mPreviewPopup.dismiss(); 1349 mHandler.cancelAllMessages(); 1350 1351 dismissPopupKeyboard(); 1352 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1353 mMiniKeyboardCache.clear(); 1354 requestLayout(); 1355 } 1356 1357 public void purgeKeyboardAndClosing() { 1358 mKeyboard = null; 1359 closing(); 1360 } 1361 1362 @Override 1363 public void onDetachedFromWindow() { 1364 super.onDetachedFromWindow(); 1365 closing(); 1366 } 1367 1368 private void dismissPopupKeyboard() { 1369 if (mMiniKeyboardPopup.isShowing()) { 1370 mMiniKeyboardPopup.dismiss(); 1371 mMiniKeyboardView = null; 1372 mMiniKeyboardOriginX = 0; 1373 mMiniKeyboardOriginY = 0; 1374 invalidateAllKeys(); 1375 } 1376 } 1377 1378 public boolean handleBack() { 1379 if (mMiniKeyboardPopup.isShowing()) { 1380 dismissPopupKeyboard(); 1381 return true; 1382 } 1383 return false; 1384 } 1385} 1386