KeyboardView.java revision 56abeabcedf68f4640bb4ebd4af3ead1e3916c3c
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 int keyDrawX = key.mX + key.mVisualInsetsLeft; 681 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 682 final Rect bounds = keyBackground.getBounds(); 683 if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) { 684 keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight); 685 } 686 canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop); 687 keyBackground.draw(canvas); 688 689 final int rowHeight = padding.top + key.mHeight; 690 // Draw key label 691 if (label != null) { 692 // For characters, use large font. For labels like "Done", use small font. 693 final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint); 694 final int labelCharHeight = getLabelCharHeight(labelSize, paint); 695 696 // Vertical label text alignment. 697 final float baseline; 698 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) { 699 baseline = key.mHeight - 700 + labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; 701 if (DEBUG_SHOW_ALIGN) 702 drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, 703 new Paint()); 704 } else { // Align center 705 final float centerY = (key.mHeight + padding.top - padding.bottom) / 2; 706 baseline = centerY 707 + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; 708 if (DEBUG_SHOW_ALIGN) 709 drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, 710 new Paint()); 711 } 712 // Horizontal label text alignment 713 final int positionX; 714 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 715 positionX = mKeyLabelHorizontalPadding + padding.left; 716 paint.setTextAlign(Align.LEFT); 717 if (DEBUG_SHOW_ALIGN) 718 drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint()); 719 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 720 positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right; 721 paint.setTextAlign(Align.RIGHT); 722 if (DEBUG_SHOW_ALIGN) 723 drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint()); 724 } else { 725 positionX = (keyDrawWidth + padding.left - padding.right) / 2; 726 paint.setTextAlign(Align.CENTER); 727 if (DEBUG_SHOW_ALIGN) { 728 if (label.length() > 1) 729 drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint()); 730 } 731 } 732 if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) { 733 paint.setColor(mKeyTextColorDisabled); 734 } else { 735 paint.setColor(mKeyTextColor); 736 } 737 if (key.mEnabled) { 738 // Set a drop shadow for the text 739 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 740 } else { 741 // Make label invisible 742 paint.setColor(Color.TRANSPARENT); 743 } 744 canvas.drawText(label, positionX, baseline, paint); 745 // Turn off drop shadow 746 paint.setShadowLayer(0, 0, 0, 0); 747 } 748 // Draw key icon 749 final Drawable icon = key.getIcon(); 750 if (key.mLabel == null && icon != null) { 751 final int drawableWidth = icon.getIntrinsicWidth(); 752 final int drawableHeight = icon.getIntrinsicHeight(); 753 final int drawableX; 754 final int drawableY = ( 755 key.mHeight + padding.top - padding.bottom - drawableHeight) / 2; 756 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 757 drawableX = padding.left + mKeyLabelHorizontalPadding; 758 if (DEBUG_SHOW_ALIGN) 759 drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint()); 760 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 761 drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding 762 - drawableWidth; 763 if (DEBUG_SHOW_ALIGN) 764 drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight, 765 0xc0808000, new Paint()); 766 } else { // Align center 767 drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2; 768 if (DEBUG_SHOW_ALIGN) 769 drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight, 770 0xc0008080, new Paint()); 771 } 772 drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight); 773 if (DEBUG_SHOW_ALIGN) 774 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 775 0x80c00000, new Paint()); 776 } 777 if (key.mHintIcon != null) { 778 final int drawableWidth = keyDrawWidth; 779 final int drawableHeight = key.mHeight; 780 final int drawableX = 0; 781 final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; 782 Drawable hintIcon = (isManualTemporaryUpperCase 783 && key.mManualTemporaryUpperCaseHintIcon != null) 784 ? key.mManualTemporaryUpperCaseHintIcon : key.mHintIcon; 785 drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight); 786 if (DEBUG_SHOW_ALIGN) 787 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 788 0x80c0c000, new Paint()); 789 } 790 canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop); 791 } 792 793 // TODO: Move this function to ProximityInfo for getting rid of public declarations for 794 // GRID_WIDTH and GRID_HEIGHT 795 if (DEBUG_KEYBOARD_GRID) { 796 Paint p = new Paint(); 797 p.setStyle(Paint.Style.STROKE); 798 p.setStrokeWidth(1.0f); 799 p.setColor(0x800000c0); 800 int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) / mKeyboard.GRID_WIDTH; 801 int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) / mKeyboard.GRID_HEIGHT; 802 for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) 803 canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); 804 for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) 805 canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); 806 } 807 808 // Overlay a dark rectangle to dim the keyboard 809 if (mMiniKeyboardView != null) { 810 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 811 canvas.drawRect(0, 0, width, height, paint); 812 } 813 814 mInvalidatedKey = null; 815 mDrawPending = false; 816 mDirtyRect.setEmpty(); 817 } 818 819 public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) { 820 // For characters, use large font. For labels like "Done", use small font. 821 final int labelSize; 822 final Typeface labelStyle; 823 if (label.length() > 1) { 824 labelSize = mLabelTextSize; 825 if ((keyLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) { 826 labelStyle = Typeface.DEFAULT; 827 } else { 828 labelStyle = Typeface.DEFAULT_BOLD; 829 } 830 } else { 831 labelSize = mKeyLetterSize; 832 labelStyle = mKeyLetterStyle; 833 } 834 paint.setTextSize(labelSize); 835 paint.setTypeface(labelStyle); 836 return labelSize; 837 } 838 839 private int getLabelCharHeight(int labelSize, Paint paint) { 840 Integer labelHeightValue = mTextHeightCache.get(labelSize); 841 final int labelCharHeight; 842 if (labelHeightValue != null) { 843 labelCharHeight = labelHeightValue; 844 } else { 845 Rect textBounds = new Rect(); 846 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds); 847 labelCharHeight = textBounds.height(); 848 mTextHeightCache.put(labelSize, labelCharHeight); 849 } 850 return labelCharHeight; 851 } 852 853 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 854 int height) { 855 canvas.translate(x, y); 856 icon.setBounds(0, 0, width, height); 857 icon.draw(canvas); 858 canvas.translate(-x, -y); 859 } 860 861 private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { 862 paint.setStyle(Paint.Style.STROKE); 863 paint.setStrokeWidth(1.0f); 864 paint.setColor(color); 865 canvas.drawLine(0, y, w, y, paint); 866 } 867 868 private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { 869 paint.setStyle(Paint.Style.STROKE); 870 paint.setStrokeWidth(1.0f); 871 paint.setColor(color); 872 canvas.drawLine(x, 0, x, h, paint); 873 } 874 875 private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, 876 Paint paint) { 877 paint.setStyle(Paint.Style.STROKE); 878 paint.setStrokeWidth(1.0f); 879 paint.setColor(color); 880 canvas.translate(x, y); 881 canvas.drawRect(0, 0, w, h, paint); 882 canvas.translate(-x, -y); 883 } 884 885 public void setForeground(boolean foreground) { 886 mInForeground = foreground; 887 } 888 889 // TODO: clean up this method. 890 private void dismissKeyPreview() { 891 for (PointerTracker tracker : mPointerTrackers) 892 tracker.releaseKey(); 893 showPreview(KeyDetector.NOT_A_KEY, null); 894 } 895 896 @Override 897 public void showPreview(int keyIndex, PointerTracker tracker) { 898 int oldKeyIndex = mOldPreviewKeyIndex; 899 mOldPreviewKeyIndex = keyIndex; 900 // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show 901 // the space key preview and 3) pointer moves off the space key to other letter key, we 902 // should hide the preview of the previous key. 903 final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) 904 || (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher() 905 && SubtypeSwitcher.getInstance().needsToDisplayLanguage() 906 && (tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex))); 907 // If key changed and preview is on or the key is space (language switch is enabled) 908 if (oldKeyIndex != keyIndex && (mShowPreview || (hidePreviewOrShowSpaceKeyPreview))) { 909 if (keyIndex == KeyDetector.NOT_A_KEY) { 910 mHandler.cancelPopupPreview(); 911 mHandler.dismissPreview(mDelayAfterPreview); 912 } else if (tracker != null) { 913 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker); 914 } 915 } 916 } 917 918 // TODO Must fix popup preview on xlarge layout 919 private void showKey(final int keyIndex, PointerTracker tracker) { 920 Key key = tracker.getKey(keyIndex); 921 // If keyIndex is invalid or IME is already closed, we must not show key preview. 922 // Trying to show preview PopupWindow while root window is closed causes 923 // WindowManager.BadTokenException. 924 if (key == null || !mInForeground) 925 return; 926 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 927 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 928 // What we show as preview should match what we show on key top in onBufferDraw(). 929 if (key.mLabel != null) { 930 // TODO Should take care of temporaryShiftLabel here. 931 mPreviewText.setCompoundDrawables(null, null, null, null); 932 mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); 933 if (key.mLabel.length() > 1) { 934 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); 935 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 936 } else { 937 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 938 mPreviewText.setTypeface(mKeyLetterStyle); 939 } 940 } else { 941 final Drawable previewIcon = key.getPreviewIcon(); 942 mPreviewText.setCompoundDrawables(null, null, null, 943 previewIcon != null ? previewIcon : key.getIcon()); 944 mPreviewText.setText(null); 945 } 946 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 947 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 948 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), keyDrawWidth 949 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 950 final int popupHeight = mPreviewHeight; 951 LayoutParams lp = mPreviewText.getLayoutParams(); 952 if (lp != null) { 953 lp.width = popupWidth; 954 lp.height = popupHeight; 955 } 956 957 int popupPreviewX = keyDrawX - (popupWidth - keyDrawWidth) / 2; 958 int popupPreviewY = key.mY - popupHeight + mPreviewOffset; 959 960 mHandler.cancelDismissPreview(); 961 if (mOffsetInWindow == null) { 962 mOffsetInWindow = new int[2]; 963 getLocationInWindow(mOffsetInWindow); 964 mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero 965 mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero 966 int[] windowLocation = new int[2]; 967 getLocationOnScreen(windowLocation); 968 mWindowY = windowLocation[1]; 969 } 970 // Set the preview background state 971 mPreviewText.getBackground().setState( 972 key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 973 popupPreviewX += mOffsetInWindow[0]; 974 popupPreviewY += mOffsetInWindow[1]; 975 976 // If the popup cannot be shown above the key, put it on the side 977 if (popupPreviewY + mWindowY < 0) { 978 // If the key you're pressing is on the left side of the keyboard, show the popup on 979 // the right, offset by enough to see at least one key to the left/right. 980 if (keyDrawX + keyDrawWidth <= getWidth() / 2) { 981 popupPreviewX += (int) (keyDrawWidth * 2.5); 982 } else { 983 popupPreviewX -= (int) (keyDrawWidth * 2.5); 984 } 985 popupPreviewY += popupHeight; 986 } 987 988 try { 989 if (mPreviewPopup.isShowing()) { 990 mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); 991 } else { 992 mPreviewPopup.setWidth(popupWidth); 993 mPreviewPopup.setHeight(popupHeight); 994 mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, 995 popupPreviewX, popupPreviewY); 996 } 997 } catch (WindowManager.BadTokenException e) { 998 // Swallow the exception which will be happened when IME is already closed. 999 Log.w(TAG, "LatinIME is already closed when tried showing key preview."); 1000 } 1001 // Record popup preview position to display mini-keyboard later at the same positon 1002 mPopupPreviewDisplayedY = popupPreviewY; 1003 mPreviewText.setVisibility(VISIBLE); 1004 } 1005 1006 /** 1007 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 1008 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 1009 * draws the cached buffer. 1010 * @see #invalidateKey(Key) 1011 */ 1012 public void invalidateAllKeys() { 1013 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1014 mDrawPending = true; 1015 invalidate(); 1016 } 1017 1018 /** 1019 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 1020 * one key is changing it's content. Any changes that affect the position or size of the key 1021 * may not be honored. 1022 * @param key key in the attached {@link Keyboard}. 1023 * @see #invalidateAllKeys 1024 */ 1025 @Override 1026 public void invalidateKey(Key key) { 1027 if (key == null) 1028 return; 1029 mInvalidatedKey = key; 1030 mInvalidatedKeyRect.set(0, 0, key.mWidth, key.mHeight); 1031 mInvalidatedKeyRect.offset(key.mX + getPaddingLeft(), key.mY + getPaddingTop()); 1032 mDirtyRect.union(mInvalidatedKeyRect); 1033 onBufferDraw(); 1034 invalidate(mInvalidatedKeyRect); 1035 } 1036 1037 private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) { 1038 // Check if we have a popup layout specified first. 1039 if (mPopupLayout == 0) { 1040 return false; 1041 } 1042 1043 Key popupKey = tracker.getKey(keyIndex); 1044 if (popupKey == null) 1045 return false; 1046 boolean result = onLongPress(popupKey, tracker); 1047 if (result) { 1048 dismissKeyPreview(); 1049 mMiniKeyboardTrackerId = tracker.mPointerId; 1050 // Mark this tracker "already processed" and remove it from the pointer queue 1051 tracker.setAlreadyProcessed(); 1052 mPointerQueue.remove(tracker); 1053 } 1054 return result; 1055 } 1056 1057 private void onLongPressShiftKey(PointerTracker tracker) { 1058 tracker.setAlreadyProcessed(); 1059 mPointerQueue.remove(tracker); 1060 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1061 } 1062 1063 private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { 1064 // When shift key is double tapped, the first tap is correctly processed as usual tap. And 1065 // the second tap is treated as this double tap event, so that we need not mark tracker 1066 // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. 1067 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1068 } 1069 1070 private View inflateMiniKeyboardContainer(Key popupKey) { 1071 final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); 1072 if (container == null) 1073 throw new NullPointerException(); 1074 1075 final KeyboardView miniKeyboardView = 1076 (KeyboardView)container.findViewById(R.id.KeyboardView); 1077 miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { 1078 @Override 1079 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1080 mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); 1081 dismissPopupKeyboard(); 1082 } 1083 1084 @Override 1085 public void onTextInput(CharSequence text) { 1086 mKeyboardActionListener.onTextInput(text); 1087 dismissPopupKeyboard(); 1088 } 1089 1090 @Override 1091 public void onCancelInput() { 1092 mKeyboardActionListener.onCancelInput(); 1093 dismissPopupKeyboard(); 1094 } 1095 1096 @Override 1097 public void onSwipeDown() { 1098 // Nothing to do. 1099 } 1100 @Override 1101 public void onPress(int primaryCode, boolean withSliding) { 1102 mKeyboardActionListener.onPress(primaryCode, withSliding); 1103 } 1104 @Override 1105 public void onRelease(int primaryCode, boolean withSliding) { 1106 mKeyboardActionListener.onRelease(primaryCode, withSliding); 1107 } 1108 }); 1109 // Override default ProximityKeyDetector. 1110 miniKeyboardView.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance); 1111 // Remove gesture detector on mini-keyboard 1112 miniKeyboardView.mGestureDetector = null; 1113 1114 final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(), 1115 popupKey).build(); 1116 miniKeyboardView.setKeyboard(keyboard); 1117 miniKeyboardView.mMiniKeyboardParent = this; 1118 1119 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1120 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1121 1122 return container; 1123 } 1124 1125 private static boolean isOneRowKeys(List<Key> keys) { 1126 if (keys.size() == 0) return false; 1127 final int edgeFlags = keys.get(0).mEdgeFlags; 1128 // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows, 1129 // does not have both top and bottom edge flags on at the same time. On the other hand, 1130 // the first key of mini keyboard that was created with popupCharacters must have both top 1131 // and bottom edge flags on. 1132 // When you want to use one row mini-keyboard from xml file, make sure that the row has 1133 // both top and bottom edge flags set. 1134 return (edgeFlags & Keyboard.EDGE_TOP) != 0 1135 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; 1136 } 1137 1138 /** 1139 * Called when a key is long pressed. By default this will open any popup keyboard associated 1140 * with this key through the attributes popupLayout and popupCharacters. 1141 * @param popupKey the key that was long pressed 1142 * @return true if the long press is handled, false otherwise. Subclasses should call the 1143 * method on the base class if the subclass doesn't wish to handle the call. 1144 */ 1145 protected boolean onLongPress(Key popupKey, PointerTracker tracker) { 1146 if (popupKey.mPopupCharacters == null) 1147 return false; 1148 1149 View container = mMiniKeyboardCache.get(popupKey); 1150 if (container == null) { 1151 container = inflateMiniKeyboardContainer(popupKey); 1152 mMiniKeyboardCache.put(popupKey, container); 1153 } 1154 mMiniKeyboardView = (KeyboardView)container.findViewById(R.id.KeyboardView); 1155 final MiniKeyboard miniKeyboard = (MiniKeyboard)mMiniKeyboardView.getKeyboard(); 1156 1157 if (mWindowOffset == null) { 1158 mWindowOffset = new int[2]; 1159 getLocationInWindow(mWindowOffset); 1160 } 1161 final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX() 1162 : popupKey.mX + popupKey.mWidth / 2; 1163 final int popupX = pointX - miniKeyboard.getDefaultCoordX() 1164 - container.getPaddingLeft() 1165 + getPaddingLeft() + mWindowOffset[0]; 1166 final int popupY = popupKey.mY - mKeyboard.getVerticalGap() 1167 - (container.getMeasuredHeight() - container.getPaddingBottom()) 1168 + getPaddingTop() + mWindowOffset[1]; 1169 final int x = popupX; 1170 final int y = mShowPreview && isOneRowKeys(miniKeyboard.getKeys()) 1171 ? mPopupPreviewDisplayedY : popupY; 1172 1173 mMiniKeyboardOriginX = x + container.getPaddingLeft() - mWindowOffset[0]; 1174 mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; 1175 mMiniKeyboardView.setPopupOffset(x, y); 1176 if (miniKeyboard.setShifted( 1177 mKeyboard == null ? false : mKeyboard.isShiftedOrShiftLocked())) { 1178 mMiniKeyboardView.invalidateAllKeys(); 1179 } 1180 // Mini keyboard needs no pop-up key preview displayed. 1181 mMiniKeyboardView.setPreviewEnabled(false); 1182 mMiniKeyboardPopup.setContentView(container); 1183 mMiniKeyboardPopup.setWidth(container.getMeasuredWidth()); 1184 mMiniKeyboardPopup.setHeight(container.getMeasuredHeight()); 1185 mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1186 1187 // Inject down event on the key to mini keyboard. 1188 final long eventTime = SystemClock.uptimeMillis(); 1189 mMiniKeyboardPopupTime = eventTime; 1190 final MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, 1191 pointX, popupKey.mY + popupKey.mHeight / 2, eventTime); 1192 mMiniKeyboardView.onTouchEvent(downEvent); 1193 downEvent.recycle(); 1194 1195 invalidateAllKeys(); 1196 return true; 1197 } 1198 1199 private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) { 1200 return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action, 1201 x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0); 1202 } 1203 1204 private PointerTracker getPointerTracker(final int id) { 1205 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1206 final Key[] keys = mKeys; 1207 final KeyboardActionListener listener = mKeyboardActionListener; 1208 1209 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1210 for (int i = pointers.size(); i <= id; i++) { 1211 final PointerTracker tracker = 1212 new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); 1213 if (keys != null) 1214 tracker.setKeyboard(mKeyboard, keys, mKeyHysteresisDistance); 1215 if (listener != null) 1216 tracker.setOnKeyboardActionListener(listener); 1217 pointers.add(tracker); 1218 } 1219 1220 return pointers.get(id); 1221 } 1222 1223 public boolean isInSlidingKeyInput() { 1224 if (mMiniKeyboardView != null) { 1225 return mMiniKeyboardView.isInSlidingKeyInput(); 1226 } else { 1227 return mPointerQueue.isInSlidingKeyInput(); 1228 } 1229 } 1230 1231 public int getPointerCount() { 1232 return mOldPointerCount; 1233 } 1234 1235 @Override 1236 public boolean onTouchEvent(MotionEvent me) { 1237 final int action = me.getActionMasked(); 1238 final int pointerCount = me.getPointerCount(); 1239 final int oldPointerCount = mOldPointerCount; 1240 mOldPointerCount = pointerCount; 1241 1242 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1243 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1244 // events except a transition from/to single-touch. 1245 if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled) 1246 && pointerCount > 1 && oldPointerCount > 1) { 1247 return true; 1248 } 1249 1250 // Track the last few movements to look for spurious swipes. 1251 mSwipeTracker.addMovement(me); 1252 1253 // Gesture detector must be enabled only when mini-keyboard is not on the screen and 1254 // accessibility is not enabled. 1255 // TODO: Reconcile gesture detection and accessibility features. 1256 if (mMiniKeyboardView == null && !mIsAccessibilityEnabled 1257 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { 1258 dismissKeyPreview(); 1259 mHandler.cancelKeyTimers(); 1260 return true; 1261 } 1262 1263 final long eventTime = me.getEventTime(); 1264 final int index = me.getActionIndex(); 1265 final int id = me.getPointerId(index); 1266 final int x = (int)me.getX(index); 1267 final int y = (int)me.getY(index); 1268 1269 // Needs to be called after the gesture detector gets a turn, as it may have 1270 // displayed the mini keyboard 1271 if (mMiniKeyboardView != null) { 1272 final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId); 1273 if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) { 1274 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex); 1275 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex); 1276 MotionEvent translated = generateMiniKeyboardMotionEvent(action, 1277 miniKeyboardX, miniKeyboardY, eventTime); 1278 mMiniKeyboardView.onTouchEvent(translated); 1279 translated.recycle(); 1280 } 1281 return true; 1282 } 1283 1284 if (mHandler.isInKeyRepeat()) { 1285 // It will keep being in the key repeating mode while the key is being pressed. 1286 if (action == MotionEvent.ACTION_MOVE) { 1287 return true; 1288 } 1289 final PointerTracker tracker = getPointerTracker(id); 1290 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1291 // event (UP or DOWN) is non-modifier key. 1292 if (pointerCount > 1 && !tracker.isModifier()) { 1293 mHandler.cancelKeyRepeatTimer(); 1294 } 1295 // Up event will pass through. 1296 } 1297 1298 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1299 // Translate mutli-touch event to single-touch events on the device that has no distinct 1300 // multi-touch panel. 1301 if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) { 1302 // Use only main (id=0) pointer tracker. 1303 PointerTracker tracker = getPointerTracker(0); 1304 if (pointerCount == 1 && oldPointerCount == 2) { 1305 // Multi-touch to single touch transition. 1306 // Send a down event for the latest pointer. 1307 tracker.onDownEvent(x, y, eventTime, null); 1308 } else if (pointerCount == 2 && oldPointerCount == 1) { 1309 // Single-touch to multi-touch transition. 1310 // Send an up event for the last pointer. 1311 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime, null); 1312 } else if (pointerCount == 1 && oldPointerCount == 1) { 1313 tracker.onTouchEvent(action, x, y, eventTime, null); 1314 } else { 1315 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1316 + " (old " + oldPointerCount + ")"); 1317 } 1318 return true; 1319 } 1320 1321 final PointerTrackerQueue queue = mPointerQueue; 1322 if (action == MotionEvent.ACTION_MOVE) { 1323 for (int i = 0; i < pointerCount; i++) { 1324 final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1325 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); 1326 } 1327 } else { 1328 final PointerTracker tracker = getPointerTracker(id); 1329 switch (action) { 1330 case MotionEvent.ACTION_DOWN: 1331 case MotionEvent.ACTION_POINTER_DOWN: 1332 tracker.onDownEvent(x, y, eventTime, queue); 1333 break; 1334 case MotionEvent.ACTION_UP: 1335 case MotionEvent.ACTION_POINTER_UP: 1336 tracker.onUpEvent(x, y, eventTime, queue); 1337 break; 1338 case MotionEvent.ACTION_CANCEL: 1339 tracker.onCancelEvent(x, y, eventTime, queue); 1340 break; 1341 } 1342 } 1343 1344 return true; 1345 } 1346 1347 protected void onSwipeDown() { 1348 mKeyboardActionListener.onSwipeDown(); 1349 } 1350 1351 public void closing() { 1352 mPreviewPopup.dismiss(); 1353 mHandler.cancelAllMessages(); 1354 1355 dismissPopupKeyboard(); 1356 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1357 mMiniKeyboardCache.clear(); 1358 requestLayout(); 1359 } 1360 1361 public void purgeKeyboardAndClosing() { 1362 mKeyboard = null; 1363 closing(); 1364 } 1365 1366 @Override 1367 public void onDetachedFromWindow() { 1368 super.onDetachedFromWindow(); 1369 closing(); 1370 } 1371 1372 private void dismissPopupKeyboard() { 1373 if (mMiniKeyboardPopup.isShowing()) { 1374 mMiniKeyboardPopup.dismiss(); 1375 mMiniKeyboardView = null; 1376 mMiniKeyboardOriginX = 0; 1377 mMiniKeyboardOriginY = 0; 1378 invalidateAllKeys(); 1379 } 1380 } 1381 1382 public boolean handleBack() { 1383 if (mMiniKeyboardPopup.isShowing()) { 1384 dismissPopupKeyboard(); 1385 return true; 1386 } 1387 return false; 1388 } 1389} 1390