KeyboardView.java revision a95e1087b2258b118a7ccb2bedb44da359d3abd0
1/* 2 * Copyright (C) 2008-2009 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package android.inputmethodservice; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.Typeface; 27import android.graphics.Paint.Align; 28import android.graphics.Region.Op; 29import android.graphics.drawable.Drawable; 30import android.inputmethodservice.Keyboard.Key; 31import android.media.AudioManager; 32import android.os.Handler; 33import android.os.Message; 34import android.util.AttributeSet; 35import android.util.TypedValue; 36import android.view.GestureDetector; 37import android.view.Gravity; 38import android.view.LayoutInflater; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.ViewConfiguration; 42import android.view.ViewGroup.LayoutParams; 43import android.view.accessibility.AccessibilityEvent; 44import android.view.accessibility.AccessibilityManager; 45import android.widget.PopupWindow; 46import android.widget.TextView; 47 48import com.android.internal.R; 49 50import java.util.Arrays; 51import java.util.HashMap; 52import java.util.List; 53import java.util.Map; 54 55/** 56 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and 57 * detecting key presses and touch movements. 58 * 59 * @attr ref android.R.styleable#KeyboardView_keyBackground 60 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout 61 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset 62 * @attr ref android.R.styleable#KeyboardView_labelTextSize 63 * @attr ref android.R.styleable#KeyboardView_keyTextSize 64 * @attr ref android.R.styleable#KeyboardView_keyTextColor 65 * @attr ref android.R.styleable#KeyboardView_verticalCorrection 66 * @attr ref android.R.styleable#KeyboardView_popupLayout 67 */ 68public class KeyboardView extends View implements View.OnClickListener { 69 70 /** 71 * Listener for virtual keyboard events. 72 */ 73 public interface OnKeyboardActionListener { 74 75 /** 76 * Called when the user presses a key. This is sent before the {@link #onKey} is called. 77 * For keys that repeat, this is only called once. 78 * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid 79 * key, the value will be zero. 80 */ 81 void onPress(int primaryCode); 82 83 /** 84 * Called when the user releases a key. This is sent after the {@link #onKey} is called. 85 * For keys that repeat, this is only called once. 86 * @param primaryCode the code of the key that was released 87 */ 88 void onRelease(int primaryCode); 89 90 /** 91 * Send a key press to the listener. 92 * @param primaryCode this is the key that was pressed 93 * @param keyCodes the codes for all the possible alternative keys 94 * with the primary code being the first. If the primary key code is 95 * a single character such as an alphabet or number or symbol, the alternatives 96 * will include other characters that may be on the same key or adjacent keys. 97 * These codes are useful to correct for accidental presses of a key adjacent to 98 * the intended key. 99 */ 100 void onKey(int primaryCode, int[] keyCodes); 101 102 /** 103 * Sends a sequence of characters to the listener. 104 * @param text the sequence of characters to be displayed. 105 */ 106 void onText(CharSequence text); 107 108 /** 109 * Called when the user quickly moves the finger from right to left. 110 */ 111 void swipeLeft(); 112 113 /** 114 * Called when the user quickly moves the finger from left to right. 115 */ 116 void swipeRight(); 117 118 /** 119 * Called when the user quickly moves the finger from up to down. 120 */ 121 void swipeDown(); 122 123 /** 124 * Called when the user quickly moves the finger from down to up. 125 */ 126 void swipeUp(); 127 } 128 129 private static final boolean DEBUG = false; 130 private static final int NOT_A_KEY = -1; 131 private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; 132 private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; 133 134 private Keyboard mKeyboard; 135 private int mCurrentKeyIndex = NOT_A_KEY; 136 private int mLabelTextSize; 137 private int mKeyTextSize; 138 private int mKeyTextColor; 139 private float mShadowRadius; 140 private int mShadowColor; 141 private float mBackgroundDimAmount; 142 143 private TextView mPreviewText; 144 private PopupWindow mPreviewPopup; 145 private int mPreviewTextSizeLarge; 146 private int mPreviewOffset; 147 private int mPreviewHeight; 148 // Working variable 149 private final int[] mCoordinates = new int[2]; 150 151 private PopupWindow mPopupKeyboard; 152 private View mMiniKeyboardContainer; 153 private KeyboardView mMiniKeyboard; 154 private boolean mMiniKeyboardOnScreen; 155 private View mPopupParent; 156 private int mMiniKeyboardOffsetX; 157 private int mMiniKeyboardOffsetY; 158 private Map<Key,View> mMiniKeyboardCache; 159 private Key[] mKeys; 160 161 /** Listener for {@link OnKeyboardActionListener}. */ 162 private OnKeyboardActionListener mKeyboardActionListener; 163 164 private static final int MSG_SHOW_PREVIEW = 1; 165 private static final int MSG_REMOVE_PREVIEW = 2; 166 private static final int MSG_REPEAT = 3; 167 private static final int MSG_LONGPRESS = 4; 168 169 private static final int DELAY_BEFORE_PREVIEW = 0; 170 private static final int DELAY_AFTER_PREVIEW = 70; 171 private static final int DEBOUNCE_TIME = 70; 172 173 private int mVerticalCorrection; 174 private int mProximityThreshold; 175 176 private boolean mPreviewCentered = false; 177 private boolean mShowPreview = true; 178 private boolean mShowTouchPoints = true; 179 private int mPopupPreviewX; 180 private int mPopupPreviewY; 181 182 private int mLastX; 183 private int mLastY; 184 private int mStartX; 185 private int mStartY; 186 187 private boolean mProximityCorrectOn; 188 189 private Paint mPaint; 190 private Rect mPadding; 191 192 private long mDownTime; 193 private long mLastMoveTime; 194 private int mLastKey; 195 private int mLastCodeX; 196 private int mLastCodeY; 197 private int mCurrentKey = NOT_A_KEY; 198 private int mDownKey = NOT_A_KEY; 199 private long mLastKeyTime; 200 private long mCurrentKeyTime; 201 private int[] mKeyIndices = new int[12]; 202 private GestureDetector mGestureDetector; 203 private int mPopupX; 204 private int mPopupY; 205 private int mRepeatKeyIndex = NOT_A_KEY; 206 private int mPopupLayout; 207 private boolean mAbortKey; 208 private Key mInvalidatedKey; 209 private Rect mClipRegion = new Rect(0, 0, 0, 0); 210 private boolean mPossiblePoly; 211 private SwipeTracker mSwipeTracker = new SwipeTracker(); 212 private int mSwipeThreshold; 213 private boolean mDisambiguateSwipe; 214 215 // Variables for dealing with multiple pointers 216 private int mOldPointerCount = 1; 217 private float mOldPointerX; 218 private float mOldPointerY; 219 220 private Drawable mKeyBackground; 221 222 private static final int REPEAT_INTERVAL = 50; // ~20 keys per second 223 private static final int REPEAT_START_DELAY = 400; 224 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 225 226 private static int MAX_NEARBY_KEYS = 12; 227 private int[] mDistances = new int[MAX_NEARBY_KEYS]; 228 229 // For multi-tap 230 private int mLastSentIndex; 231 private int mTapCount; 232 private long mLastTapTime; 233 private boolean mInMultiTap; 234 private static final int MULTITAP_INTERVAL = 800; // milliseconds 235 private StringBuilder mPreviewLabel = new StringBuilder(1); 236 237 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 238 private boolean mDrawPending; 239 /** The dirty region in the keyboard bitmap */ 240 private Rect mDirtyRect = new Rect(); 241 /** The keyboard bitmap for faster updates */ 242 private Bitmap mBuffer; 243 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ 244 private boolean mKeyboardChanged; 245 /** The canvas for the above mutable keyboard bitmap */ 246 private Canvas mCanvas; 247 /** The accessibility manager for accessibility support */ 248 private AccessibilityManager mAccessibilityManager; 249 /** The audio manager for accessibility support */ 250 private AudioManager mAudioManager; 251 252 Handler mHandler = new Handler() { 253 @Override 254 public void handleMessage(Message msg) { 255 switch (msg.what) { 256 case MSG_SHOW_PREVIEW: 257 showKey(msg.arg1); 258 break; 259 case MSG_REMOVE_PREVIEW: 260 mPreviewText.setVisibility(INVISIBLE); 261 break; 262 case MSG_REPEAT: 263 if (repeatKey()) { 264 Message repeat = Message.obtain(this, MSG_REPEAT); 265 sendMessageDelayed(repeat, REPEAT_INTERVAL); 266 } 267 break; 268 case MSG_LONGPRESS: 269 openPopupIfRequired((MotionEvent) msg.obj); 270 break; 271 } 272 } 273 }; 274 275 public KeyboardView(Context context, AttributeSet attrs) { 276 this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); 277 } 278 279 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 280 super(context, attrs, defStyle); 281 282 TypedArray a = 283 context.obtainStyledAttributes( 284 attrs, android.R.styleable.KeyboardView, defStyle, 0); 285 286 LayoutInflater inflate = 287 (LayoutInflater) context 288 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 289 290 int previewLayout = 0; 291 int keyTextSize = 0; 292 293 int n = a.getIndexCount(); 294 295 for (int i = 0; i < n; i++) { 296 int attr = a.getIndex(i); 297 298 switch (attr) { 299 case com.android.internal.R.styleable.KeyboardView_keyBackground: 300 mKeyBackground = a.getDrawable(attr); 301 break; 302 case com.android.internal.R.styleable.KeyboardView_verticalCorrection: 303 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); 304 break; 305 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: 306 previewLayout = a.getResourceId(attr, 0); 307 break; 308 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: 309 mPreviewOffset = a.getDimensionPixelOffset(attr, 0); 310 break; 311 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: 312 mPreviewHeight = a.getDimensionPixelSize(attr, 80); 313 break; 314 case com.android.internal.R.styleable.KeyboardView_keyTextSize: 315 mKeyTextSize = a.getDimensionPixelSize(attr, 18); 316 break; 317 case com.android.internal.R.styleable.KeyboardView_keyTextColor: 318 mKeyTextColor = a.getColor(attr, 0xFF000000); 319 break; 320 case com.android.internal.R.styleable.KeyboardView_labelTextSize: 321 mLabelTextSize = a.getDimensionPixelSize(attr, 14); 322 break; 323 case com.android.internal.R.styleable.KeyboardView_popupLayout: 324 mPopupLayout = a.getResourceId(attr, 0); 325 break; 326 case com.android.internal.R.styleable.KeyboardView_shadowColor: 327 mShadowColor = a.getColor(attr, 0); 328 break; 329 case com.android.internal.R.styleable.KeyboardView_shadowRadius: 330 mShadowRadius = a.getFloat(attr, 0f); 331 break; 332 } 333 } 334 335 a = mContext.obtainStyledAttributes( 336 com.android.internal.R.styleable.Theme); 337 mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f); 338 339 mPreviewPopup = new PopupWindow(context); 340 if (previewLayout != 0) { 341 mPreviewText = (TextView) inflate.inflate(previewLayout, null); 342 mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); 343 mPreviewPopup.setContentView(mPreviewText); 344 mPreviewPopup.setBackgroundDrawable(null); 345 } else { 346 mShowPreview = false; 347 } 348 349 mPreviewPopup.setTouchable(false); 350 351 mPopupKeyboard = new PopupWindow(context); 352 mPopupKeyboard.setBackgroundDrawable(null); 353 //mPopupKeyboard.setClippingEnabled(false); 354 355 mPopupParent = this; 356 //mPredicting = true; 357 358 mPaint = new Paint(); 359 mPaint.setAntiAlias(true); 360 mPaint.setTextSize(keyTextSize); 361 mPaint.setTextAlign(Align.CENTER); 362 mPaint.setAlpha(255); 363 364 mPadding = new Rect(0, 0, 0, 0); 365 mMiniKeyboardCache = new HashMap<Key,View>(); 366 mKeyBackground.getPadding(mPadding); 367 368 mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); 369 mDisambiguateSwipe = getResources().getBoolean( 370 com.android.internal.R.bool.config_swipeDisambiguation); 371 372 mAccessibilityManager = AccessibilityManager.getInstance(context); 373 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 374 375 resetMultiTap(); 376 initGestureDetector(); 377 } 378 379 380 private void initGestureDetector() { 381 mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 382 @Override 383 public boolean onFling(MotionEvent me1, MotionEvent me2, 384 float velocityX, float velocityY) { 385 if (mPossiblePoly) return false; 386 final float absX = Math.abs(velocityX); 387 final float absY = Math.abs(velocityY); 388 float deltaX = me2.getX() - me1.getX(); 389 float deltaY = me2.getY() - me1.getY(); 390 int travelX = getWidth() / 2; // Half the keyboard width 391 int travelY = getHeight() / 2; // Half the keyboard height 392 mSwipeTracker.computeCurrentVelocity(1000); 393 final float endingVelocityX = mSwipeTracker.getXVelocity(); 394 final float endingVelocityY = mSwipeTracker.getYVelocity(); 395 boolean sendDownKey = false; 396 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { 397 if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { 398 sendDownKey = true; 399 } else { 400 swipeRight(); 401 return true; 402 } 403 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { 404 if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { 405 sendDownKey = true; 406 } else { 407 swipeLeft(); 408 return true; 409 } 410 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { 411 if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { 412 sendDownKey = true; 413 } else { 414 swipeUp(); 415 return true; 416 } 417 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 418 if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { 419 sendDownKey = true; 420 } else { 421 swipeDown(); 422 return true; 423 } 424 } 425 426 if (sendDownKey) { 427 detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); 428 } 429 return false; 430 } 431 }); 432 433 mGestureDetector.setIsLongpressEnabled(false); 434 } 435 436 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { 437 mKeyboardActionListener = listener; 438 } 439 440 /** 441 * Returns the {@link OnKeyboardActionListener} object. 442 * @return the listener attached to this keyboard 443 */ 444 protected OnKeyboardActionListener getOnKeyboardActionListener() { 445 return mKeyboardActionListener; 446 } 447 448 /** 449 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 450 * view will re-layout itself to accommodate the keyboard. 451 * @see Keyboard 452 * @see #getKeyboard() 453 * @param keyboard the keyboard to display in this view 454 */ 455 public void setKeyboard(Keyboard keyboard) { 456 if (mKeyboard != null) { 457 showPreview(NOT_A_KEY); 458 } 459 // Remove any pending messages 460 removeMessages(); 461 mKeyboard = keyboard; 462 List<Key> keys = mKeyboard.getKeys(); 463 mKeys = keys.toArray(new Key[keys.size()]); 464 requestLayout(); 465 // Hint to reallocate the buffer if the size changed 466 mKeyboardChanged = true; 467 invalidateAllKeys(); 468 computeProximityThreshold(keyboard); 469 mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views 470 // Switching to a different keyboard should abort any pending keys so that the key up 471 // doesn't get delivered to the old or new keyboard 472 mAbortKey = true; // Until the next ACTION_DOWN 473 } 474 475 /** 476 * Returns the current keyboard being displayed by this view. 477 * @return the currently attached keyboard 478 * @see #setKeyboard(Keyboard) 479 */ 480 public Keyboard getKeyboard() { 481 return mKeyboard; 482 } 483 484 /** 485 * Sets the state of the shift key of the keyboard, if any. 486 * @param shifted whether or not to enable the state of the shift key 487 * @return true if the shift key state changed, false if there was no change 488 * @see KeyboardView#isShifted() 489 */ 490 public boolean setShifted(boolean shifted) { 491 if (mKeyboard != null) { 492 if (mKeyboard.setShifted(shifted)) { 493 // The whole keyboard probably needs to be redrawn 494 invalidateAllKeys(); 495 return true; 496 } 497 } 498 return false; 499 } 500 501 /** 502 * Returns the state of the shift key of the keyboard, if any. 503 * @return true if the shift is in a pressed state, false otherwise. If there is 504 * no shift key on the keyboard or there is no keyboard attached, it returns false. 505 * @see KeyboardView#setShifted(boolean) 506 */ 507 public boolean isShifted() { 508 if (mKeyboard != null) { 509 return mKeyboard.isShifted(); 510 } 511 return false; 512 } 513 514 /** 515 * Enables or disables the key feedback popup. This is a popup that shows a magnified 516 * version of the depressed key. By default the preview is enabled. 517 * @param previewEnabled whether or not to enable the key feedback popup 518 * @see #isPreviewEnabled() 519 */ 520 public void setPreviewEnabled(boolean previewEnabled) { 521 mShowPreview = previewEnabled; 522 } 523 524 /** 525 * Returns the enabled state of the key feedback popup. 526 * @return whether or not the key feedback popup is enabled 527 * @see #setPreviewEnabled(boolean) 528 */ 529 public boolean isPreviewEnabled() { 530 return mShowPreview; 531 } 532 533 public void setVerticalCorrection(int verticalOffset) { 534 535 } 536 public void setPopupParent(View v) { 537 mPopupParent = v; 538 } 539 540 public void setPopupOffset(int x, int y) { 541 mMiniKeyboardOffsetX = x; 542 mMiniKeyboardOffsetY = y; 543 if (mPreviewPopup.isShowing()) { 544 mPreviewPopup.dismiss(); 545 } 546 } 547 548 /** 549 * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key 550 * codes for adjacent keys. When disabled, only the primary key code will be 551 * reported. 552 * @param enabled whether or not the proximity correction is enabled 553 */ 554 public void setProximityCorrectionEnabled(boolean enabled) { 555 mProximityCorrectOn = enabled; 556 } 557 558 /** 559 * Returns true if proximity correction is enabled. 560 */ 561 public boolean isProximityCorrectionEnabled() { 562 return mProximityCorrectOn; 563 } 564 565 /** 566 * Popup keyboard close button clicked. 567 * @hide 568 */ 569 public void onClick(View v) { 570 dismissPopupKeyboard(); 571 } 572 573 private CharSequence adjustCase(CharSequence label) { 574 if (mKeyboard.isShifted() && label != null && label.length() < 3 575 && Character.isLowerCase(label.charAt(0))) { 576 label = label.toString().toUpperCase(); 577 } 578 return label; 579 } 580 581 @Override 582 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 583 // Round up a little 584 if (mKeyboard == null) { 585 setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); 586 } else { 587 int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; 588 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 589 width = MeasureSpec.getSize(widthMeasureSpec); 590 } 591 setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); 592 } 593 } 594 595 /** 596 * Compute the average distance between adjacent keys (horizontally and vertically) 597 * and square it to get the proximity threshold. We use a square here and in computing 598 * the touch distance from a key's center to avoid taking a square root. 599 * @param keyboard 600 */ 601 private void computeProximityThreshold(Keyboard keyboard) { 602 if (keyboard == null) return; 603 final Key[] keys = mKeys; 604 if (keys == null) return; 605 int length = keys.length; 606 int dimensionSum = 0; 607 for (int i = 0; i < length; i++) { 608 Key key = keys[i]; 609 dimensionSum += Math.min(key.width, key.height) + key.gap; 610 } 611 if (dimensionSum < 0 || length == 0) return; 612 mProximityThreshold = (int) (dimensionSum * 1.4f / length); 613 mProximityThreshold *= mProximityThreshold; // Square it 614 } 615 616 @Override 617 public void onSizeChanged(int w, int h, int oldw, int oldh) { 618 super.onSizeChanged(w, h, oldw, oldh); 619 if (mKeyboard != null) { 620 mKeyboard.resize(w, h); 621 } 622 // Release the buffer, if any and it will be reallocated on the next draw 623 mBuffer = null; 624 } 625 626 @Override 627 public void onDraw(Canvas canvas) { 628 super.onDraw(canvas); 629 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 630 onBufferDraw(); 631 } 632 canvas.drawBitmap(mBuffer, 0, 0, null); 633 } 634 635 private void onBufferDraw() { 636 if (mBuffer == null || mKeyboardChanged) { 637 if (mBuffer == null || mKeyboardChanged && 638 (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { 639 // Make sure our bitmap is at least 1x1 640 final int width = Math.max(1, getWidth()); 641 final int height = Math.max(1, getHeight()); 642 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 643 mCanvas = new Canvas(mBuffer); 644 } 645 invalidateAllKeys(); 646 mKeyboardChanged = false; 647 } 648 final Canvas canvas = mCanvas; 649 canvas.clipRect(mDirtyRect, Op.REPLACE); 650 651 if (mKeyboard == null) return; 652 653 final Paint paint = mPaint; 654 final Drawable keyBackground = mKeyBackground; 655 final Rect clipRegion = mClipRegion; 656 final Rect padding = mPadding; 657 final int kbdPaddingLeft = mPaddingLeft; 658 final int kbdPaddingTop = mPaddingTop; 659 final Key[] keys = mKeys; 660 final Key invalidKey = mInvalidatedKey; 661 662 paint.setColor(mKeyTextColor); 663 boolean drawSingleKey = false; 664 if (invalidKey != null && canvas.getClipBounds(clipRegion)) { 665 // Is clipRegion completely contained within the invalidated key? 666 if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && 667 invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && 668 invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && 669 invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { 670 drawSingleKey = true; 671 } 672 } 673 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 674 final int keyCount = keys.length; 675 for (int i = 0; i < keyCount; i++) { 676 final Key key = keys[i]; 677 if (drawSingleKey && invalidKey != key) { 678 continue; 679 } 680 int[] drawableState = key.getCurrentDrawableState(); 681 keyBackground.setState(drawableState); 682 683 // Switch the character to uppercase if shift is pressed 684 String label = key.label == null? null : adjustCase(key.label).toString(); 685 686 final Rect bounds = keyBackground.getBounds(); 687 if (key.width != bounds.right || 688 key.height != bounds.bottom) { 689 keyBackground.setBounds(0, 0, key.width, key.height); 690 } 691 canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); 692 keyBackground.draw(canvas); 693 694 if (label != null) { 695 // For characters, use large font. For labels like "Done", use small font. 696 if (label.length() > 1 && key.codes.length < 2) { 697 paint.setTextSize(mLabelTextSize); 698 paint.setTypeface(Typeface.DEFAULT_BOLD); 699 } else { 700 paint.setTextSize(mKeyTextSize); 701 paint.setTypeface(Typeface.DEFAULT); 702 } 703 // Draw a drop shadow for the text 704 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 705 // Draw the text 706 canvas.drawText(label, 707 (key.width - padding.left - padding.right) / 2 708 + padding.left, 709 (key.height - padding.top - padding.bottom) / 2 710 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, 711 paint); 712 // Turn off drop shadow 713 paint.setShadowLayer(0, 0, 0, 0); 714 } else if (key.icon != null) { 715 final int drawableX = (key.width - padding.left - padding.right 716 - key.icon.getIntrinsicWidth()) / 2 + padding.left; 717 final int drawableY = (key.height - padding.top - padding.bottom 718 - key.icon.getIntrinsicHeight()) / 2 + padding.top; 719 canvas.translate(drawableX, drawableY); 720 key.icon.setBounds(0, 0, 721 key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); 722 key.icon.draw(canvas); 723 canvas.translate(-drawableX, -drawableY); 724 } 725 canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); 726 } 727 mInvalidatedKey = null; 728 // Overlay a dark rectangle to dim the keyboard 729 if (mMiniKeyboardOnScreen) { 730 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 731 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 732 } 733 734 if (DEBUG && mShowTouchPoints) { 735 paint.setAlpha(128); 736 paint.setColor(0xFFFF0000); 737 canvas.drawCircle(mStartX, mStartY, 3, paint); 738 canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); 739 paint.setColor(0xFF0000FF); 740 canvas.drawCircle(mLastX, mLastY, 3, paint); 741 paint.setColor(0xFF00FF00); 742 canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); 743 } 744 745 mDrawPending = false; 746 mDirtyRect.setEmpty(); 747 } 748 749 private int getKeyIndices(int x, int y, int[] allKeys) { 750 final Key[] keys = mKeys; 751 int primaryIndex = NOT_A_KEY; 752 int closestKey = NOT_A_KEY; 753 int closestKeyDist = mProximityThreshold + 1; 754 java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); 755 int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); 756 final int keyCount = nearestKeyIndices.length; 757 for (int i = 0; i < keyCount; i++) { 758 final Key key = keys[nearestKeyIndices[i]]; 759 int dist = 0; 760 boolean isInside = key.isInside(x,y); 761 if (isInside) { 762 primaryIndex = nearestKeyIndices[i]; 763 } 764 765 if (((mProximityCorrectOn 766 && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) 767 || isInside) 768 && key.codes[0] > 32) { 769 // Find insertion point 770 final int nCodes = key.codes.length; 771 if (dist < closestKeyDist) { 772 closestKeyDist = dist; 773 closestKey = nearestKeyIndices[i]; 774 } 775 776 if (allKeys == null) continue; 777 778 for (int j = 0; j < mDistances.length; j++) { 779 if (mDistances[j] > dist) { 780 // Make space for nCodes codes 781 System.arraycopy(mDistances, j, mDistances, j + nCodes, 782 mDistances.length - j - nCodes); 783 System.arraycopy(allKeys, j, allKeys, j + nCodes, 784 allKeys.length - j - nCodes); 785 for (int c = 0; c < nCodes; c++) { 786 allKeys[j + c] = key.codes[c]; 787 mDistances[j + c] = dist; 788 } 789 break; 790 } 791 } 792 } 793 } 794 if (primaryIndex == NOT_A_KEY) { 795 primaryIndex = closestKey; 796 } 797 return primaryIndex; 798 } 799 800 private void detectAndSendKey(int index, int x, int y, long eventTime) { 801 if (index != NOT_A_KEY && index < mKeys.length) { 802 final Key key = mKeys[index]; 803 if (key.text != null) { 804 mKeyboardActionListener.onText(key.text); 805 mKeyboardActionListener.onRelease(NOT_A_KEY); 806 } else { 807 int code = key.codes[0]; 808 //TextEntryState.keyPressedAt(key, x, y); 809 int[] codes = new int[MAX_NEARBY_KEYS]; 810 Arrays.fill(codes, NOT_A_KEY); 811 getKeyIndices(x, y, codes); 812 // Multi-tap 813 if (mInMultiTap) { 814 if (mTapCount != -1) { 815 mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); 816 } else { 817 mTapCount = 0; 818 } 819 code = key.codes[mTapCount]; 820 } 821 mKeyboardActionListener.onKey(code, codes); 822 mKeyboardActionListener.onRelease(code); 823 } 824 mLastSentIndex = index; 825 mLastTapTime = eventTime; 826 } 827 } 828 829 /** 830 * Handle multi-tap keys by producing the key label for the current multi-tap state. 831 */ 832 private CharSequence getPreviewText(Key key) { 833 if (mInMultiTap) { 834 // Multi-tap 835 mPreviewLabel.setLength(0); 836 mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); 837 return adjustCase(mPreviewLabel); 838 } else { 839 return adjustCase(key.label); 840 } 841 } 842 843 private void showPreview(int keyIndex) { 844 int oldKeyIndex = mCurrentKeyIndex; 845 final PopupWindow previewPopup = mPreviewPopup; 846 847 mCurrentKeyIndex = keyIndex; 848 // Release the old key and press the new key 849 final Key[] keys = mKeys; 850 if (oldKeyIndex != mCurrentKeyIndex) { 851 if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { 852 Key oldKey = keys[oldKeyIndex]; 853 oldKey.onReleased(mCurrentKeyIndex == NOT_A_KEY); 854 invalidateKey(oldKeyIndex); 855 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT, oldKey.codes[0]); 856 } 857 if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { 858 Key newKey = keys[mCurrentKeyIndex]; 859 newKey.onPressed(); 860 invalidateKey(mCurrentKeyIndex); 861 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, newKey.codes[0]); 862 } 863 } 864 // If key changed and preview is on ... 865 if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { 866 mHandler.removeMessages(MSG_SHOW_PREVIEW); 867 if (previewPopup.isShowing()) { 868 if (keyIndex == NOT_A_KEY) { 869 mHandler.sendMessageDelayed(mHandler 870 .obtainMessage(MSG_REMOVE_PREVIEW), 871 DELAY_AFTER_PREVIEW); 872 } 873 } 874 if (keyIndex != NOT_A_KEY) { 875 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { 876 // Show right away, if it's already visible and finger is moving around 877 showKey(keyIndex); 878 } else { 879 mHandler.sendMessageDelayed( 880 mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), 881 DELAY_BEFORE_PREVIEW); 882 } 883 } 884 } 885 } 886 887 private void showKey(final int keyIndex) { 888 final PopupWindow previewPopup = mPreviewPopup; 889 final Key[] keys = mKeys; 890 if (keyIndex < 0 || keyIndex >= mKeys.length) return; 891 Key key = keys[keyIndex]; 892 if (key.icon != null) { 893 mPreviewText.setCompoundDrawables(null, null, null, 894 key.iconPreview != null ? key.iconPreview : key.icon); 895 mPreviewText.setText(null); 896 } else { 897 mPreviewText.setCompoundDrawables(null, null, null, null); 898 mPreviewText.setText(getPreviewText(key)); 899 if (key.label.length() > 1 && key.codes.length < 2) { 900 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); 901 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 902 } else { 903 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 904 mPreviewText.setTypeface(Typeface.DEFAULT); 905 } 906 } 907 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 908 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 909 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width 910 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 911 final int popupHeight = mPreviewHeight; 912 LayoutParams lp = mPreviewText.getLayoutParams(); 913 if (lp != null) { 914 lp.width = popupWidth; 915 lp.height = popupHeight; 916 } 917 if (!mPreviewCentered) { 918 mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; 919 mPopupPreviewY = key.y - popupHeight + mPreviewOffset; 920 } else { 921 // TODO: Fix this if centering is brought back 922 mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; 923 mPopupPreviewY = - mPreviewText.getMeasuredHeight(); 924 } 925 mHandler.removeMessages(MSG_REMOVE_PREVIEW); 926 getLocationInWindow(mCoordinates); 927 mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero 928 mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero 929 930 // Set the preview background state 931 mPreviewText.getBackground().setState( 932 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 933 mPopupPreviewX += mCoordinates[0]; 934 mPopupPreviewY += mCoordinates[1]; 935 936 // If the popup cannot be shown above the key, put it on the side 937 getLocationOnScreen(mCoordinates); 938 if (mPopupPreviewY + mCoordinates[1] < 0) { 939 // If the key you're pressing is on the left side of the keyboard, show the popup on 940 // the right, offset by enough to see at least one key to the left/right. 941 if (key.x + key.width <= getWidth() / 2) { 942 mPopupPreviewX += (int) (key.width * 2.5); 943 } else { 944 mPopupPreviewX -= (int) (key.width * 2.5); 945 } 946 mPopupPreviewY += popupHeight; 947 } 948 949 if (previewPopup.isShowing()) { 950 previewPopup.update(mPopupPreviewX, mPopupPreviewY, 951 popupWidth, popupHeight); 952 } else { 953 previewPopup.setWidth(popupWidth); 954 previewPopup.setHeight(popupHeight); 955 previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, 956 mPopupPreviewX, mPopupPreviewY); 957 } 958 mPreviewText.setVisibility(VISIBLE); 959 } 960 961 private void sendAccessibilityEvent(int eventType, int code) { 962 if (mAccessibilityManager.isEnabled()) { 963 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 964 onInitializeAccessibilityEvent(event); 965 // Add text only if headset is used to avoid leaking passwords. 966 if (mAudioManager.isBluetoothA2dpOn() || mAudioManager.isWiredHeadsetOn()) { 967 String text = null; 968 switch (code) { 969 case Keyboard.KEYCODE_ALT: 970 text = mContext.getString(R.string.keyboardview_keycode_alt); 971 break; 972 case Keyboard.KEYCODE_CANCEL: 973 text = mContext.getString(R.string.keyboardview_keycode_cancel); 974 break; 975 case Keyboard.KEYCODE_DELETE: 976 text = mContext.getString(R.string.keyboardview_keycode_delete); 977 break; 978 case Keyboard.KEYCODE_DONE: 979 text = mContext.getString(R.string.keyboardview_keycode_done); 980 break; 981 case Keyboard.KEYCODE_MODE_CHANGE: 982 text = mContext.getString(R.string.keyboardview_keycode_mode_change); 983 break; 984 case Keyboard.KEYCODE_SHIFT: 985 text = mContext.getString(R.string.keyboardview_keycode_shift); 986 break; 987 case '\n': 988 text = mContext.getString(R.string.keyboardview_keycode_enter); 989 break; 990 default: 991 text = String.valueOf((char) code); 992 } 993 event.getText().add(text); 994 } else { 995 event.getText().add(mContext.getString( 996 R.string.keyboard_headset_required_to_hear_password)); 997 } 998 mAccessibilityManager.sendAccessibilityEvent(event); 999 } 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(int) 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 keyIndex the index of the key in the attached {@link Keyboard}. 1019 * @see #invalidateAllKeys 1020 */ 1021 public void invalidateKey(int keyIndex) { 1022 if (mKeys == null) return; 1023 if (keyIndex < 0 || keyIndex >= mKeys.length) { 1024 return; 1025 } 1026 final Key key = mKeys[keyIndex]; 1027 mInvalidatedKey = key; 1028 mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop, 1029 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 1030 onBufferDraw(); 1031 invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, 1032 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 1033 } 1034 1035 private boolean openPopupIfRequired(MotionEvent me) { 1036 // Check if we have a popup layout specified first. 1037 if (mPopupLayout == 0) { 1038 return false; 1039 } 1040 if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { 1041 return false; 1042 } 1043 1044 Key popupKey = mKeys[mCurrentKey]; 1045 boolean result = onLongPress(popupKey); 1046 if (result) { 1047 mAbortKey = true; 1048 showPreview(NOT_A_KEY); 1049 } 1050 return result; 1051 } 1052 1053 /** 1054 * Called when a key is long pressed. By default this will open any popup keyboard associated 1055 * with this key through the attributes popupLayout and popupCharacters. 1056 * @param popupKey the key that was long pressed 1057 * @return true if the long press is handled, false otherwise. Subclasses should call the 1058 * method on the base class if the subclass doesn't wish to handle the call. 1059 */ 1060 protected boolean onLongPress(Key popupKey) { 1061 int popupKeyboardId = popupKey.popupResId; 1062 1063 if (popupKeyboardId != 0) { 1064 mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); 1065 if (mMiniKeyboardContainer == null) { 1066 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 1067 Context.LAYOUT_INFLATER_SERVICE); 1068 mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); 1069 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 1070 com.android.internal.R.id.keyboardView); 1071 View closeButton = mMiniKeyboardContainer.findViewById( 1072 com.android.internal.R.id.closeButton); 1073 if (closeButton != null) closeButton.setOnClickListener(this); 1074 mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { 1075 public void onKey(int primaryCode, int[] keyCodes) { 1076 mKeyboardActionListener.onKey(primaryCode, keyCodes); 1077 dismissPopupKeyboard(); 1078 } 1079 1080 public void onText(CharSequence text) { 1081 mKeyboardActionListener.onText(text); 1082 dismissPopupKeyboard(); 1083 } 1084 1085 public void swipeLeft() { } 1086 public void swipeRight() { } 1087 public void swipeUp() { } 1088 public void swipeDown() { } 1089 public void onPress(int primaryCode) { 1090 mKeyboardActionListener.onPress(primaryCode); 1091 } 1092 public void onRelease(int primaryCode) { 1093 mKeyboardActionListener.onRelease(primaryCode); 1094 } 1095 }); 1096 //mInputView.setSuggest(mSuggest); 1097 Keyboard keyboard; 1098 if (popupKey.popupCharacters != null) { 1099 keyboard = new Keyboard(getContext(), popupKeyboardId, 1100 popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); 1101 } else { 1102 keyboard = new Keyboard(getContext(), popupKeyboardId); 1103 } 1104 mMiniKeyboard.setKeyboard(keyboard); 1105 mMiniKeyboard.setPopupParent(this); 1106 mMiniKeyboardContainer.measure( 1107 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1108 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1109 1110 mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); 1111 } else { 1112 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 1113 com.android.internal.R.id.keyboardView); 1114 } 1115 getLocationInWindow(mCoordinates); 1116 mPopupX = popupKey.x + mPaddingLeft; 1117 mPopupY = popupKey.y + mPaddingTop; 1118 mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); 1119 mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); 1120 final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0]; 1121 final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1]; 1122 mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); 1123 mMiniKeyboard.setShifted(isShifted()); 1124 mPopupKeyboard.setContentView(mMiniKeyboardContainer); 1125 mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); 1126 mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); 1127 mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1128 mMiniKeyboardOnScreen = true; 1129 //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); 1130 invalidateAllKeys(); 1131 return true; 1132 } 1133 return false; 1134 } 1135 1136 @Override 1137 protected boolean dispatchHoverEvent(MotionEvent event) { 1138 // If touch exploring is enabled we ignore touch events and transform 1139 // the stream of hover events as touch events. This allows one consistent 1140 // event stream to drive the keyboard since during touch exploring the 1141 // first touch generates only hover events and tapping on the same 1142 // location generates hover and touch events. 1143 if (mAccessibilityManager.isEnabled() 1144 && mAccessibilityManager.isTouchExplorationEnabled() 1145 && event.getPointerCount() == 1) { 1146 final int action = event.getAction(); 1147 switch (action) { 1148 case MotionEvent.ACTION_HOVER_ENTER: 1149 event.setAction(MotionEvent.ACTION_DOWN); 1150 break; 1151 case MotionEvent.ACTION_HOVER_MOVE: 1152 event.setAction(MotionEvent.ACTION_MOVE); 1153 break; 1154 case MotionEvent.ACTION_HOVER_EXIT: 1155 event.setAction(MotionEvent.ACTION_UP); 1156 break; 1157 } 1158 onTouchEventInternal(event); 1159 return true; 1160 } 1161 return super.dispatchHoverEvent(event); 1162 } 1163 1164 @Override 1165 public boolean onTouchEvent(MotionEvent event) { 1166 // If touch exploring is enabled we ignore touch events and transform 1167 // the stream of hover events as touch events. This allows one consistent 1168 // event stream to drive the keyboard since during touch exploring the 1169 // first touch generates only hover events and tapping on the same 1170 // location generates hover and touch events. 1171 if (mAccessibilityManager.isEnabled() 1172 && mAccessibilityManager.isTouchExplorationEnabled()) { 1173 return true; 1174 } 1175 return onTouchEventInternal(event); 1176 } 1177 1178 private boolean onTouchEventInternal(MotionEvent me) { 1179 // Convert multi-pointer up/down events to single up/down events to 1180 // deal with the typical multi-pointer behavior of two-thumb typing 1181 final int pointerCount = me.getPointerCount(); 1182 final int action = me.getAction(); 1183 boolean result = false; 1184 final long now = me.getEventTime(); 1185 1186 if (pointerCount != mOldPointerCount) { 1187 if (pointerCount == 1) { 1188 // Send a down event for the latest pointer 1189 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1190 me.getX(), me.getY(), me.getMetaState()); 1191 result = onModifiedTouchEvent(down, false); 1192 down.recycle(); 1193 // If it's an up action, then deliver the up as well. 1194 if (action == MotionEvent.ACTION_UP) { 1195 result = onModifiedTouchEvent(me, true); 1196 } 1197 } else { 1198 // Send an up event for the last pointer 1199 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 1200 mOldPointerX, mOldPointerY, me.getMetaState()); 1201 result = onModifiedTouchEvent(up, true); 1202 up.recycle(); 1203 } 1204 } else { 1205 if (pointerCount == 1) { 1206 result = onModifiedTouchEvent(me, false); 1207 mOldPointerX = me.getX(); 1208 mOldPointerY = me.getY(); 1209 } else { 1210 // Don't do anything when 2 pointers are down and moving. 1211 result = true; 1212 } 1213 } 1214 mOldPointerCount = pointerCount; 1215 1216 return result; 1217 } 1218 1219 private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { 1220 int touchX = (int) me.getX() - mPaddingLeft; 1221 int touchY = (int) me.getY() - mPaddingTop; 1222 if (touchY >= -mVerticalCorrection) 1223 touchY += mVerticalCorrection; 1224 final int action = me.getAction(); 1225 final long eventTime = me.getEventTime(); 1226 int keyIndex = getKeyIndices(touchX, touchY, null); 1227 mPossiblePoly = possiblePoly; 1228 1229 // Track the last few movements to look for spurious swipes. 1230 if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); 1231 mSwipeTracker.addMovement(me); 1232 1233 // Ignore all motion events until a DOWN. 1234 if (mAbortKey 1235 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { 1236 return true; 1237 } 1238 1239 if (mGestureDetector.onTouchEvent(me)) { 1240 showPreview(NOT_A_KEY); 1241 mHandler.removeMessages(MSG_REPEAT); 1242 mHandler.removeMessages(MSG_LONGPRESS); 1243 return true; 1244 } 1245 1246 // Needs to be called after the gesture detector gets a turn, as it may have 1247 // displayed the mini keyboard 1248 if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { 1249 return true; 1250 } 1251 1252 switch (action) { 1253 case MotionEvent.ACTION_DOWN: 1254 mAbortKey = false; 1255 mStartX = touchX; 1256 mStartY = touchY; 1257 mLastCodeX = touchX; 1258 mLastCodeY = touchY; 1259 mLastKeyTime = 0; 1260 mCurrentKeyTime = 0; 1261 mLastKey = NOT_A_KEY; 1262 mCurrentKey = keyIndex; 1263 mDownKey = keyIndex; 1264 mDownTime = me.getEventTime(); 1265 mLastMoveTime = mDownTime; 1266 checkMultiTap(eventTime, keyIndex); 1267 mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? 1268 mKeys[keyIndex].codes[0] : 0); 1269 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { 1270 mRepeatKeyIndex = mCurrentKey; 1271 Message msg = mHandler.obtainMessage(MSG_REPEAT); 1272 mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); 1273 repeatKey(); 1274 // Delivering the key could have caused an abort 1275 if (mAbortKey) { 1276 mRepeatKeyIndex = NOT_A_KEY; 1277 break; 1278 } 1279 } 1280 if (mCurrentKey != NOT_A_KEY) { 1281 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1282 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1283 } 1284 showPreview(keyIndex); 1285 break; 1286 1287 case MotionEvent.ACTION_MOVE: 1288 boolean continueLongPress = false; 1289 if (keyIndex != NOT_A_KEY) { 1290 if (mCurrentKey == NOT_A_KEY) { 1291 mCurrentKey = keyIndex; 1292 mCurrentKeyTime = eventTime - mDownTime; 1293 } else { 1294 if (keyIndex == mCurrentKey) { 1295 mCurrentKeyTime += eventTime - mLastMoveTime; 1296 continueLongPress = true; 1297 } else if (mRepeatKeyIndex == NOT_A_KEY) { 1298 resetMultiTap(); 1299 mLastKey = mCurrentKey; 1300 mLastCodeX = mLastX; 1301 mLastCodeY = mLastY; 1302 mLastKeyTime = 1303 mCurrentKeyTime + eventTime - mLastMoveTime; 1304 mCurrentKey = keyIndex; 1305 mCurrentKeyTime = 0; 1306 } 1307 } 1308 } 1309 if (!continueLongPress) { 1310 // Cancel old longpress 1311 mHandler.removeMessages(MSG_LONGPRESS); 1312 // Start new longpress if key has changed 1313 if (keyIndex != NOT_A_KEY) { 1314 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1315 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1316 } 1317 } 1318 showPreview(mCurrentKey); 1319 mLastMoveTime = eventTime; 1320 break; 1321 1322 case MotionEvent.ACTION_UP: 1323 removeMessages(); 1324 if (keyIndex == mCurrentKey) { 1325 mCurrentKeyTime += eventTime - mLastMoveTime; 1326 } else { 1327 resetMultiTap(); 1328 mLastKey = mCurrentKey; 1329 mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; 1330 mCurrentKey = keyIndex; 1331 mCurrentKeyTime = 0; 1332 } 1333 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME 1334 && mLastKey != NOT_A_KEY) { 1335 mCurrentKey = mLastKey; 1336 touchX = mLastCodeX; 1337 touchY = mLastCodeY; 1338 } 1339 showPreview(NOT_A_KEY); 1340 Arrays.fill(mKeyIndices, NOT_A_KEY); 1341 // If we're not on a repeating key (which sends on a DOWN event) 1342 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { 1343 detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); 1344 } 1345 invalidateKey(keyIndex); 1346 mRepeatKeyIndex = NOT_A_KEY; 1347 break; 1348 case MotionEvent.ACTION_CANCEL: 1349 removeMessages(); 1350 dismissPopupKeyboard(); 1351 mAbortKey = true; 1352 showPreview(NOT_A_KEY); 1353 invalidateKey(mCurrentKey); 1354 break; 1355 } 1356 mLastX = touchX; 1357 mLastY = touchY; 1358 return true; 1359 } 1360 1361 private boolean repeatKey() { 1362 Key key = mKeys[mRepeatKeyIndex]; 1363 detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); 1364 return true; 1365 } 1366 1367 protected void swipeRight() { 1368 mKeyboardActionListener.swipeRight(); 1369 } 1370 1371 protected void swipeLeft() { 1372 mKeyboardActionListener.swipeLeft(); 1373 } 1374 1375 protected void swipeUp() { 1376 mKeyboardActionListener.swipeUp(); 1377 } 1378 1379 protected void swipeDown() { 1380 mKeyboardActionListener.swipeDown(); 1381 } 1382 1383 public void closing() { 1384 if (mPreviewPopup.isShowing()) { 1385 mPreviewPopup.dismiss(); 1386 } 1387 removeMessages(); 1388 1389 dismissPopupKeyboard(); 1390 mBuffer = null; 1391 mCanvas = null; 1392 mMiniKeyboardCache.clear(); 1393 } 1394 1395 private void removeMessages() { 1396 mHandler.removeMessages(MSG_REPEAT); 1397 mHandler.removeMessages(MSG_LONGPRESS); 1398 mHandler.removeMessages(MSG_SHOW_PREVIEW); 1399 } 1400 1401 @Override 1402 public void onDetachedFromWindow() { 1403 super.onDetachedFromWindow(); 1404 closing(); 1405 } 1406 1407 private void dismissPopupKeyboard() { 1408 if (mPopupKeyboard.isShowing()) { 1409 mPopupKeyboard.dismiss(); 1410 mMiniKeyboardOnScreen = false; 1411 invalidateAllKeys(); 1412 } 1413 } 1414 1415 public boolean handleBack() { 1416 if (mPopupKeyboard.isShowing()) { 1417 dismissPopupKeyboard(); 1418 return true; 1419 } 1420 return false; 1421 } 1422 1423 private void resetMultiTap() { 1424 mLastSentIndex = NOT_A_KEY; 1425 mTapCount = 0; 1426 mLastTapTime = -1; 1427 mInMultiTap = false; 1428 } 1429 1430 private void checkMultiTap(long eventTime, int keyIndex) { 1431 if (keyIndex == NOT_A_KEY) return; 1432 Key key = mKeys[keyIndex]; 1433 if (key.codes.length > 1) { 1434 mInMultiTap = true; 1435 if (eventTime < mLastTapTime + MULTITAP_INTERVAL 1436 && keyIndex == mLastSentIndex) { 1437 mTapCount = (mTapCount + 1) % key.codes.length; 1438 return; 1439 } else { 1440 mTapCount = -1; 1441 return; 1442 } 1443 } 1444 if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { 1445 resetMultiTap(); 1446 } 1447 } 1448 1449 private static class SwipeTracker { 1450 1451 static final int NUM_PAST = 4; 1452 static final int LONGEST_PAST_TIME = 200; 1453 1454 final float mPastX[] = new float[NUM_PAST]; 1455 final float mPastY[] = new float[NUM_PAST]; 1456 final long mPastTime[] = new long[NUM_PAST]; 1457 1458 float mYVelocity; 1459 float mXVelocity; 1460 1461 public void clear() { 1462 mPastTime[0] = 0; 1463 } 1464 1465 public void addMovement(MotionEvent ev) { 1466 long time = ev.getEventTime(); 1467 final int N = ev.getHistorySize(); 1468 for (int i=0; i<N; i++) { 1469 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), 1470 ev.getHistoricalEventTime(i)); 1471 } 1472 addPoint(ev.getX(), ev.getY(), time); 1473 } 1474 1475 private void addPoint(float x, float y, long time) { 1476 int drop = -1; 1477 int i; 1478 final long[] pastTime = mPastTime; 1479 for (i=0; i<NUM_PAST; i++) { 1480 if (pastTime[i] == 0) { 1481 break; 1482 } else if (pastTime[i] < time-LONGEST_PAST_TIME) { 1483 drop = i; 1484 } 1485 } 1486 if (i == NUM_PAST && drop < 0) { 1487 drop = 0; 1488 } 1489 if (drop == i) drop--; 1490 final float[] pastX = mPastX; 1491 final float[] pastY = mPastY; 1492 if (drop >= 0) { 1493 final int start = drop+1; 1494 final int count = NUM_PAST-drop-1; 1495 System.arraycopy(pastX, start, pastX, 0, count); 1496 System.arraycopy(pastY, start, pastY, 0, count); 1497 System.arraycopy(pastTime, start, pastTime, 0, count); 1498 i -= (drop+1); 1499 } 1500 pastX[i] = x; 1501 pastY[i] = y; 1502 pastTime[i] = time; 1503 i++; 1504 if (i < NUM_PAST) { 1505 pastTime[i] = 0; 1506 } 1507 } 1508 1509 public void computeCurrentVelocity(int units) { 1510 computeCurrentVelocity(units, Float.MAX_VALUE); 1511 } 1512 1513 public void computeCurrentVelocity(int units, float maxVelocity) { 1514 final float[] pastX = mPastX; 1515 final float[] pastY = mPastY; 1516 final long[] pastTime = mPastTime; 1517 1518 final float oldestX = pastX[0]; 1519 final float oldestY = pastY[0]; 1520 final long oldestTime = pastTime[0]; 1521 float accumX = 0; 1522 float accumY = 0; 1523 int N=0; 1524 while (N < NUM_PAST) { 1525 if (pastTime[N] == 0) { 1526 break; 1527 } 1528 N++; 1529 } 1530 1531 for (int i=1; i < N; i++) { 1532 final int dur = (int)(pastTime[i] - oldestTime); 1533 if (dur == 0) continue; 1534 float dist = pastX[i] - oldestX; 1535 float vel = (dist/dur) * units; // pixels/frame. 1536 if (accumX == 0) accumX = vel; 1537 else accumX = (accumX + vel) * .5f; 1538 1539 dist = pastY[i] - oldestY; 1540 vel = (dist/dur) * units; // pixels/frame. 1541 if (accumY == 0) accumY = vel; 1542 else accumY = (accumY + vel) * .5f; 1543 } 1544 mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) 1545 : Math.min(accumX, maxVelocity); 1546 mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) 1547 : Math.min(accumY, maxVelocity); 1548 } 1549 1550 public float getXVelocity() { 1551 return mXVelocity; 1552 } 1553 1554 public float getYVelocity() { 1555 return mYVelocity; 1556 } 1557 } 1558} 1559