KeyboardView.java revision ebe3d518ffaeb14f3f2e0e74cdbc15fd2ddf4453
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.os.Handler; 32import android.os.Message; 33import android.util.AttributeSet; 34import android.util.TypedValue; 35import android.view.GestureDetector; 36import android.view.Gravity; 37import android.view.LayoutInflater; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.ViewConfiguration; 41import android.view.ViewGroup.LayoutParams; 42import android.widget.PopupWindow; 43import android.widget.TextView; 44 45import com.android.internal.R; 46 47import java.util.Arrays; 48import java.util.HashMap; 49import java.util.List; 50import java.util.Map; 51 52/** 53 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and 54 * detecting key presses and touch movements. 55 * 56 * @attr ref android.R.styleable#KeyboardView_keyBackground 57 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout 58 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset 59 * @attr ref android.R.styleable#KeyboardView_labelTextSize 60 * @attr ref android.R.styleable#KeyboardView_keyTextSize 61 * @attr ref android.R.styleable#KeyboardView_keyTextColor 62 * @attr ref android.R.styleable#KeyboardView_verticalCorrection 63 * @attr ref android.R.styleable#KeyboardView_popupLayout 64 */ 65public class KeyboardView extends View implements View.OnClickListener { 66 67 /** 68 * Listener for virtual keyboard events. 69 */ 70 public interface OnKeyboardActionListener { 71 72 /** 73 * Called when the user presses a key. This is sent before the {@link #onKey} is called. 74 * For keys that repeat, this is only called once. 75 * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid 76 * key, the value will be zero. 77 */ 78 void onPress(int primaryCode); 79 80 /** 81 * Called when the user releases a key. This is sent after the {@link #onKey} is called. 82 * For keys that repeat, this is only called once. 83 * @param primaryCode the code of the key that was released 84 */ 85 void onRelease(int primaryCode); 86 87 /** 88 * Send a key press to the listener. 89 * @param primaryCode this is the key that was pressed 90 * @param keyCodes the codes for all the possible alternative keys 91 * with the primary code being the first. If the primary key code is 92 * a single character such as an alphabet or number or symbol, the alternatives 93 * will include other characters that may be on the same key or adjacent keys. 94 * These codes are useful to correct for accidental presses of a key adjacent to 95 * the intended key. 96 */ 97 void onKey(int primaryCode, int[] keyCodes); 98 99 /** 100 * Sends a sequence of characters to the listener. 101 * @param text the sequence of characters to be displayed. 102 */ 103 void onText(CharSequence text); 104 105 /** 106 * Called when the user quickly moves the finger from right to left. 107 */ 108 void swipeLeft(); 109 110 /** 111 * Called when the user quickly moves the finger from left to right. 112 */ 113 void swipeRight(); 114 115 /** 116 * Called when the user quickly moves the finger from up to down. 117 */ 118 void swipeDown(); 119 120 /** 121 * Called when the user quickly moves the finger from down to up. 122 */ 123 void swipeUp(); 124 } 125 126 private static final boolean DEBUG = false; 127 private static final int NOT_A_KEY = -1; 128 private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; 129 private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; 130 131 private Keyboard mKeyboard; 132 private int mCurrentKeyIndex = NOT_A_KEY; 133 private int mLabelTextSize; 134 private int mKeyTextSize; 135 private int mKeyTextColor; 136 private float mShadowRadius; 137 private int mShadowColor; 138 private float mBackgroundDimAmount; 139 140 private TextView mPreviewText; 141 private PopupWindow mPreviewPopup; 142 private int mPreviewTextSizeLarge; 143 private int mPreviewOffset; 144 private int mPreviewHeight; 145 private int[] mOffsetInWindow; 146 147 private PopupWindow mPopupKeyboard; 148 private View mMiniKeyboardContainer; 149 private KeyboardView mMiniKeyboard; 150 private boolean mMiniKeyboardOnScreen; 151 private View mPopupParent; 152 private int mMiniKeyboardOffsetX; 153 private int mMiniKeyboardOffsetY; 154 private Map<Key,View> mMiniKeyboardCache; 155 private int[] mWindowOffset; 156 private Key[] mKeys; 157 158 /** Listener for {@link OnKeyboardActionListener}. */ 159 private OnKeyboardActionListener mKeyboardActionListener; 160 161 private static final int MSG_SHOW_PREVIEW = 1; 162 private static final int MSG_REMOVE_PREVIEW = 2; 163 private static final int MSG_REPEAT = 3; 164 private static final int MSG_LONGPRESS = 4; 165 166 private static final int DELAY_BEFORE_PREVIEW = 0; 167 private static final int DELAY_AFTER_PREVIEW = 70; 168 169 private int mVerticalCorrection; 170 private int mProximityThreshold; 171 172 private boolean mPreviewCentered = false; 173 private boolean mShowPreview = true; 174 private boolean mShowTouchPoints = true; 175 private int mPopupPreviewX; 176 private int mPopupPreviewY; 177 178 private int mLastX; 179 private int mLastY; 180 private int mStartX; 181 private int mStartY; 182 183 private boolean mProximityCorrectOn; 184 185 private Paint mPaint; 186 private Rect mPadding; 187 188 private long mDownTime; 189 private long mLastMoveTime; 190 private int mLastKey; 191 private int mLastCodeX; 192 private int mLastCodeY; 193 private int mCurrentKey = NOT_A_KEY; 194 private long mLastKeyTime; 195 private long mCurrentKeyTime; 196 private int[] mKeyIndices = new int[12]; 197 private GestureDetector mGestureDetector; 198 private int mPopupX; 199 private int mPopupY; 200 private int mRepeatKeyIndex = NOT_A_KEY; 201 private int mPopupLayout; 202 private boolean mAbortKey; 203 private Key mInvalidatedKey; 204 private Rect mClipRegion = new Rect(0, 0, 0, 0); 205 206 // Variables for dealing with multiple pointers 207 private int mOldPointerCount = 1; 208 private float mOldPointerX; 209 private float mOldPointerY; 210 211 private Drawable mKeyBackground; 212 213 private static final int REPEAT_INTERVAL = 50; // ~20 keys per second 214 private static final int REPEAT_START_DELAY = 400; 215 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 216 217 private static int MAX_NEARBY_KEYS = 12; 218 private int[] mDistances = new int[MAX_NEARBY_KEYS]; 219 220 // For multi-tap 221 private int mLastSentIndex; 222 private int mTapCount; 223 private long mLastTapTime; 224 private boolean mInMultiTap; 225 private static final int MULTITAP_INTERVAL = 800; // milliseconds 226 private StringBuilder mPreviewLabel = new StringBuilder(1); 227 228 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 229 private boolean mDrawPending; 230 /** The dirty region in the keyboard bitmap */ 231 private Rect mDirtyRect = new Rect(); 232 /** The keyboard bitmap for faster updates */ 233 private Bitmap mBuffer; 234 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ 235 private boolean mKeyboardChanged; 236 /** The canvas for the above mutable keyboard bitmap */ 237 private Canvas mCanvas; 238 239 Handler mHandler = new Handler() { 240 @Override 241 public void handleMessage(Message msg) { 242 switch (msg.what) { 243 case MSG_SHOW_PREVIEW: 244 showKey(msg.arg1); 245 break; 246 case MSG_REMOVE_PREVIEW: 247 mPreviewText.setVisibility(INVISIBLE); 248 break; 249 case MSG_REPEAT: 250 if (repeatKey()) { 251 Message repeat = Message.obtain(this, MSG_REPEAT); 252 sendMessageDelayed(repeat, REPEAT_INTERVAL); 253 } 254 break; 255 case MSG_LONGPRESS: 256 openPopupIfRequired((MotionEvent) msg.obj); 257 break; 258 } 259 } 260 }; 261 262 public KeyboardView(Context context, AttributeSet attrs) { 263 this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); 264 } 265 266 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 267 super(context, attrs, defStyle); 268 269 TypedArray a = 270 context.obtainStyledAttributes( 271 attrs, android.R.styleable.KeyboardView, defStyle, 0); 272 273 LayoutInflater inflate = 274 (LayoutInflater) context 275 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 276 277 int previewLayout = 0; 278 int keyTextSize = 0; 279 280 int n = a.getIndexCount(); 281 282 for (int i = 0; i < n; i++) { 283 int attr = a.getIndex(i); 284 285 switch (attr) { 286 case com.android.internal.R.styleable.KeyboardView_keyBackground: 287 mKeyBackground = a.getDrawable(attr); 288 break; 289 case com.android.internal.R.styleable.KeyboardView_verticalCorrection: 290 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); 291 break; 292 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: 293 previewLayout = a.getResourceId(attr, 0); 294 break; 295 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: 296 mPreviewOffset = a.getDimensionPixelOffset(attr, 0); 297 break; 298 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: 299 mPreviewHeight = a.getDimensionPixelSize(attr, 80); 300 break; 301 case com.android.internal.R.styleable.KeyboardView_keyTextSize: 302 mKeyTextSize = a.getDimensionPixelSize(attr, 18); 303 break; 304 case com.android.internal.R.styleable.KeyboardView_keyTextColor: 305 mKeyTextColor = a.getColor(attr, 0xFF000000); 306 break; 307 case com.android.internal.R.styleable.KeyboardView_labelTextSize: 308 mLabelTextSize = a.getDimensionPixelSize(attr, 14); 309 break; 310 case com.android.internal.R.styleable.KeyboardView_popupLayout: 311 mPopupLayout = a.getResourceId(attr, 0); 312 break; 313 case com.android.internal.R.styleable.KeyboardView_shadowColor: 314 mShadowColor = a.getColor(attr, 0); 315 break; 316 case com.android.internal.R.styleable.KeyboardView_shadowRadius: 317 mShadowRadius = a.getFloat(attr, 0f); 318 break; 319 } 320 } 321 322 a = mContext.obtainStyledAttributes( 323 com.android.internal.R.styleable.Theme); 324 mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f); 325 326 mPreviewPopup = new PopupWindow(context); 327 if (previewLayout != 0) { 328 mPreviewText = (TextView) inflate.inflate(previewLayout, null); 329 mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); 330 mPreviewPopup.setContentView(mPreviewText); 331 mPreviewPopup.setBackgroundDrawable(null); 332 } else { 333 mShowPreview = false; 334 } 335 336 mPreviewPopup.setTouchable(false); 337 338 mPopupKeyboard = new PopupWindow(context); 339 mPopupKeyboard.setBackgroundDrawable(null); 340 //mPopupKeyboard.setClippingEnabled(false); 341 342 mPopupParent = this; 343 //mPredicting = true; 344 345 mPaint = new Paint(); 346 mPaint.setAntiAlias(true); 347 mPaint.setTextSize(keyTextSize); 348 mPaint.setTextAlign(Align.CENTER); 349 mPaint.setAlpha(255); 350 351 mPadding = new Rect(0, 0, 0, 0); 352 mMiniKeyboardCache = new HashMap<Key,View>(); 353 mKeyBackground.getPadding(mPadding); 354 355 resetMultiTap(); 356 initGestureDetector(); 357 } 358 359 private void initGestureDetector() { 360 mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 361 @Override 362 public boolean onFling(MotionEvent me1, MotionEvent me2, 363 float velocityX, float velocityY) { 364 final float absX = Math.abs(velocityX); 365 final float absY = Math.abs(velocityY); 366 if (velocityX > 500 && absY < absX) { 367 swipeRight(); 368 return true; 369 } else if (velocityX < -500 && absY < absX) { 370 swipeLeft(); 371 return true; 372 } else if (velocityY < -500 && absX < absY) { 373 swipeUp(); 374 return true; 375 } else if (velocityY > 500 && absX < 200) { 376 swipeDown(); 377 return true; 378 } else if (absX > 800 || absY > 800) { 379 return true; 380 } 381 return false; 382 } 383 }); 384 385 mGestureDetector.setIsLongpressEnabled(false); 386 } 387 388 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { 389 mKeyboardActionListener = listener; 390 } 391 392 /** 393 * Returns the {@link OnKeyboardActionListener} object. 394 * @return the listener attached to this keyboard 395 */ 396 protected OnKeyboardActionListener getOnKeyboardActionListener() { 397 return mKeyboardActionListener; 398 } 399 400 /** 401 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 402 * view will re-layout itself to accommodate the keyboard. 403 * @see Keyboard 404 * @see #getKeyboard() 405 * @param keyboard the keyboard to display in this view 406 */ 407 public void setKeyboard(Keyboard keyboard) { 408 if (mKeyboard != null) { 409 showPreview(NOT_A_KEY); 410 } 411 // Remove any pending messages 412 removeMessages(); 413 mKeyboard = keyboard; 414 List<Key> keys = mKeyboard.getKeys(); 415 mKeys = keys.toArray(new Key[keys.size()]); 416 requestLayout(); 417 // Hint to reallocate the buffer if the size changed 418 mKeyboardChanged = true; 419 invalidateAllKeys(); 420 computeProximityThreshold(keyboard); 421 mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views 422 // Switching to a different keyboard should abort any pending keys so that the key up 423 // doesn't get delivered to the old or new keyboard 424 mAbortKey = true; // Until the next ACTION_DOWN 425 } 426 427 /** 428 * Returns the current keyboard being displayed by this view. 429 * @return the currently attached keyboard 430 * @see #setKeyboard(Keyboard) 431 */ 432 public Keyboard getKeyboard() { 433 return mKeyboard; 434 } 435 436 /** 437 * Sets the state of the shift key of the keyboard, if any. 438 * @param shifted whether or not to enable the state of the shift key 439 * @return true if the shift key state changed, false if there was no change 440 * @see KeyboardView#isShifted() 441 */ 442 public boolean setShifted(boolean shifted) { 443 if (mKeyboard != null) { 444 if (mKeyboard.setShifted(shifted)) { 445 // The whole keyboard probably needs to be redrawn 446 invalidateAllKeys(); 447 return true; 448 } 449 } 450 return false; 451 } 452 453 /** 454 * Returns the state of the shift key of the keyboard, if any. 455 * @return true if the shift is in a pressed state, false otherwise. If there is 456 * no shift key on the keyboard or there is no keyboard attached, it returns false. 457 * @see KeyboardView#setShifted(boolean) 458 */ 459 public boolean isShifted() { 460 if (mKeyboard != null) { 461 return mKeyboard.isShifted(); 462 } 463 return false; 464 } 465 466 /** 467 * Enables or disables the key feedback popup. This is a popup that shows a magnified 468 * version of the depressed key. By default the preview is enabled. 469 * @param previewEnabled whether or not to enable the key feedback popup 470 * @see #isPreviewEnabled() 471 */ 472 public void setPreviewEnabled(boolean previewEnabled) { 473 mShowPreview = previewEnabled; 474 } 475 476 /** 477 * Returns the enabled state of the key feedback popup. 478 * @return whether or not the key feedback popup is enabled 479 * @see #setPreviewEnabled(boolean) 480 */ 481 public boolean isPreviewEnabled() { 482 return mShowPreview; 483 } 484 485 public void setVerticalCorrection(int verticalOffset) { 486 487 } 488 public void setPopupParent(View v) { 489 mPopupParent = v; 490 } 491 492 public void setPopupOffset(int x, int y) { 493 mMiniKeyboardOffsetX = x; 494 mMiniKeyboardOffsetY = y; 495 if (mPreviewPopup.isShowing()) { 496 mPreviewPopup.dismiss(); 497 } 498 } 499 500 /** 501 * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key 502 * codes for adjacent keys. When disabled, only the primary key code will be 503 * reported. 504 * @param enabled whether or not the proximity correction is enabled 505 */ 506 public void setProximityCorrectionEnabled(boolean enabled) { 507 mProximityCorrectOn = enabled; 508 } 509 510 /** 511 * Returns true if proximity correction is enabled. 512 */ 513 public boolean isProximityCorrectionEnabled() { 514 return mProximityCorrectOn; 515 } 516 517 /** 518 * Popup keyboard close button clicked. 519 * @hide 520 */ 521 public void onClick(View v) { 522 dismissPopupKeyboard(); 523 } 524 525 private CharSequence adjustCase(CharSequence label) { 526 if (mKeyboard.isShifted() && label != null && label.length() < 3 527 && Character.isLowerCase(label.charAt(0))) { 528 label = label.toString().toUpperCase(); 529 } 530 return label; 531 } 532 533 @Override 534 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 535 // Round up a little 536 if (mKeyboard == null) { 537 setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); 538 } else { 539 int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; 540 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 541 width = MeasureSpec.getSize(widthMeasureSpec); 542 } 543 setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); 544 } 545 } 546 547 /** 548 * Compute the average distance between adjacent keys (horizontally and vertically) 549 * and square it to get the proximity threshold. We use a square here and in computing 550 * the touch distance from a key's center to avoid taking a square root. 551 * @param keyboard 552 */ 553 private void computeProximityThreshold(Keyboard keyboard) { 554 if (keyboard == null) return; 555 final Key[] keys = mKeys; 556 if (keys == null) return; 557 int length = keys.length; 558 int dimensionSum = 0; 559 for (int i = 0; i < length; i++) { 560 Key key = keys[i]; 561 dimensionSum += Math.min(key.width, key.height) + key.gap; 562 } 563 if (dimensionSum < 0 || length == 0) return; 564 mProximityThreshold = (int) (dimensionSum * 1.4f / length); 565 mProximityThreshold *= mProximityThreshold; // Square it 566 } 567 568 @Override 569 public void onSizeChanged(int w, int h, int oldw, int oldh) { 570 super.onSizeChanged(w, h, oldw, oldh); 571 // Release the buffer, if any and it will be reallocated on the next draw 572 mBuffer = null; 573 } 574 575 @Override 576 public void onDraw(Canvas canvas) { 577 super.onDraw(canvas); 578 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 579 onBufferDraw(); 580 } 581 canvas.drawBitmap(mBuffer, 0, 0, null); 582 } 583 584 private void onBufferDraw() { 585 if (mBuffer == null || mKeyboardChanged) { 586 if (mBuffer == null || mKeyboardChanged && 587 (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { 588 mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 589 mCanvas = new Canvas(mBuffer); 590 } 591 invalidateAllKeys(); 592 mKeyboardChanged = false; 593 } 594 final Canvas canvas = mCanvas; 595 canvas.clipRect(mDirtyRect, Op.REPLACE); 596 597 if (mKeyboard == null) return; 598 599 final Paint paint = mPaint; 600 final Drawable keyBackground = mKeyBackground; 601 final Rect clipRegion = mClipRegion; 602 final Rect padding = mPadding; 603 final int kbdPaddingLeft = mPaddingLeft; 604 final int kbdPaddingTop = mPaddingTop; 605 final Key[] keys = mKeys; 606 final Key invalidKey = mInvalidatedKey; 607 608 paint.setColor(mKeyTextColor); 609 boolean drawSingleKey = false; 610 if (invalidKey != null && canvas.getClipBounds(clipRegion)) { 611 // Is clipRegion completely contained within the invalidated key? 612 if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && 613 invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && 614 invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && 615 invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { 616 drawSingleKey = true; 617 } 618 } 619 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 620 final int keyCount = keys.length; 621 for (int i = 0; i < keyCount; i++) { 622 final Key key = keys[i]; 623 if (drawSingleKey && invalidKey != key) { 624 continue; 625 } 626 int[] drawableState = key.getCurrentDrawableState(); 627 keyBackground.setState(drawableState); 628 629 // Switch the character to uppercase if shift is pressed 630 String label = key.label == null? null : adjustCase(key.label).toString(); 631 632 final Rect bounds = keyBackground.getBounds(); 633 if (key.width != bounds.right || 634 key.height != bounds.bottom) { 635 keyBackground.setBounds(0, 0, key.width, key.height); 636 } 637 canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); 638 keyBackground.draw(canvas); 639 640 if (label != null) { 641 // For characters, use large font. For labels like "Done", use small font. 642 if (label.length() > 1 && key.codes.length < 2) { 643 paint.setTextSize(mLabelTextSize); 644 paint.setTypeface(Typeface.DEFAULT_BOLD); 645 } else { 646 paint.setTextSize(mKeyTextSize); 647 paint.setTypeface(Typeface.DEFAULT); 648 } 649 // Draw a drop shadow for the text 650 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 651 // Draw the text 652 canvas.drawText(label, 653 (key.width - padding.left - padding.right) / 2 654 + padding.left, 655 (key.height - padding.top - padding.bottom) / 2 656 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, 657 paint); 658 // Turn off drop shadow 659 paint.setShadowLayer(0, 0, 0, 0); 660 } else if (key.icon != null) { 661 final int drawableX = (key.width - padding.left - padding.right 662 - key.icon.getIntrinsicWidth()) / 2 + padding.left; 663 final int drawableY = (key.height - padding.top - padding.bottom 664 - key.icon.getIntrinsicHeight()) / 2 + padding.top; 665 canvas.translate(drawableX, drawableY); 666 key.icon.setBounds(0, 0, 667 key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); 668 key.icon.draw(canvas); 669 canvas.translate(-drawableX, -drawableY); 670 } 671 canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); 672 } 673 mInvalidatedKey = null; 674 // Overlay a dark rectangle to dim the keyboard 675 if (mMiniKeyboardOnScreen) { 676 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 677 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 678 } 679 680 if (DEBUG && mShowTouchPoints) { 681 paint.setAlpha(128); 682 paint.setColor(0xFFFF0000); 683 canvas.drawCircle(mStartX, mStartY, 3, paint); 684 canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); 685 paint.setColor(0xFF0000FF); 686 canvas.drawCircle(mLastX, mLastY, 3, paint); 687 paint.setColor(0xFF00FF00); 688 canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); 689 } 690 691 mDrawPending = false; 692 mDirtyRect.setEmpty(); 693 } 694 695 private int getKeyIndices(int x, int y, int[] allKeys) { 696 final Key[] keys = mKeys; 697 int primaryIndex = NOT_A_KEY; 698 int closestKey = NOT_A_KEY; 699 int closestKeyDist = mProximityThreshold + 1; 700 java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); 701 int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); 702 final int keyCount = nearestKeyIndices.length; 703 for (int i = 0; i < keyCount; i++) { 704 final Key key = keys[nearestKeyIndices[i]]; 705 int dist = 0; 706 boolean isInside = key.isInside(x,y); 707 if (((mProximityCorrectOn 708 && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) 709 || isInside) 710 && key.codes[0] > 32) { 711 // Find insertion point 712 final int nCodes = key.codes.length; 713 if (dist < closestKeyDist) { 714 closestKeyDist = dist; 715 closestKey = nearestKeyIndices[i]; 716 } 717 718 if (allKeys == null) continue; 719 720 for (int j = 0; j < mDistances.length; j++) { 721 if (mDistances[j] > dist) { 722 // Make space for nCodes codes 723 System.arraycopy(mDistances, j, mDistances, j + nCodes, 724 mDistances.length - j - nCodes); 725 System.arraycopy(allKeys, j, allKeys, j + nCodes, 726 allKeys.length - j - nCodes); 727 for (int c = 0; c < nCodes; c++) { 728 allKeys[j + c] = key.codes[c]; 729 mDistances[j + c] = dist; 730 } 731 break; 732 } 733 } 734 } 735 736 if (isInside) { 737 primaryIndex = nearestKeyIndices[i]; 738 } 739 } 740 if (primaryIndex == NOT_A_KEY) { 741 primaryIndex = closestKey; 742 } 743 return primaryIndex; 744 } 745 746 private void detectAndSendKey(int x, int y, long eventTime) { 747 int index = mCurrentKey; 748 if (index != NOT_A_KEY && index < mKeys.length) { 749 final Key key = mKeys[index]; 750 if (key.text != null) { 751 mKeyboardActionListener.onText(key.text); 752 mKeyboardActionListener.onRelease(NOT_A_KEY); 753 } else { 754 int code = key.codes[0]; 755 //TextEntryState.keyPressedAt(key, x, y); 756 int[] codes = new int[MAX_NEARBY_KEYS]; 757 Arrays.fill(codes, NOT_A_KEY); 758 getKeyIndices(x, y, codes); 759 // Multi-tap 760 if (mInMultiTap) { 761 if (mTapCount != -1) { 762 mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); 763 } else { 764 mTapCount = 0; 765 } 766 code = key.codes[mTapCount]; 767 } 768 mKeyboardActionListener.onKey(code, codes); 769 mKeyboardActionListener.onRelease(code); 770 } 771 mLastSentIndex = index; 772 mLastTapTime = eventTime; 773 } 774 } 775 776 /** 777 * Handle multi-tap keys by producing the key label for the current multi-tap state. 778 */ 779 private CharSequence getPreviewText(Key key) { 780 if (mInMultiTap) { 781 // Multi-tap 782 mPreviewLabel.setLength(0); 783 mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); 784 return adjustCase(mPreviewLabel); 785 } else { 786 return adjustCase(key.label); 787 } 788 } 789 790 private void showPreview(int keyIndex) { 791 int oldKeyIndex = mCurrentKeyIndex; 792 final PopupWindow previewPopup = mPreviewPopup; 793 794 mCurrentKeyIndex = keyIndex; 795 // Release the old key and press the new key 796 final Key[] keys = mKeys; 797 if (oldKeyIndex != mCurrentKeyIndex) { 798 if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { 799 keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY); 800 invalidateKey(oldKeyIndex); 801 } 802 if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { 803 keys[mCurrentKeyIndex].onPressed(); 804 invalidateKey(mCurrentKeyIndex); 805 } 806 } 807 // If key changed and preview is on ... 808 if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { 809 mHandler.removeMessages(MSG_SHOW_PREVIEW); 810 if (previewPopup.isShowing()) { 811 if (keyIndex == NOT_A_KEY) { 812 mHandler.sendMessageDelayed(mHandler 813 .obtainMessage(MSG_REMOVE_PREVIEW), 814 DELAY_AFTER_PREVIEW); 815 } 816 } 817 if (keyIndex != NOT_A_KEY) { 818 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { 819 // Show right away, if it's already visible and finger is moving around 820 showKey(keyIndex); 821 } else { 822 mHandler.sendMessageDelayed( 823 mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), 824 DELAY_BEFORE_PREVIEW); 825 } 826 } 827 } 828 } 829 830 private void showKey(final int keyIndex) { 831 final PopupWindow previewPopup = mPreviewPopup; 832 final Key[] keys = mKeys; 833 if (keyIndex < 0 || keyIndex >= mKeys.length) return; 834 Key key = keys[keyIndex]; 835 if (key.icon != null) { 836 mPreviewText.setCompoundDrawables(null, null, null, 837 key.iconPreview != null ? key.iconPreview : key.icon); 838 mPreviewText.setText(null); 839 } else { 840 mPreviewText.setCompoundDrawables(null, null, null, null); 841 mPreviewText.setText(getPreviewText(key)); 842 if (key.label.length() > 1 && key.codes.length < 2) { 843 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); 844 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 845 } else { 846 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 847 mPreviewText.setTypeface(Typeface.DEFAULT); 848 } 849 } 850 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 851 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 852 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width 853 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 854 final int popupHeight = mPreviewHeight; 855 LayoutParams lp = mPreviewText.getLayoutParams(); 856 if (lp != null) { 857 lp.width = popupWidth; 858 lp.height = popupHeight; 859 } 860 if (!mPreviewCentered) { 861 mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; 862 mPopupPreviewY = key.y - popupHeight + mPreviewOffset; 863 } else { 864 // TODO: Fix this if centering is brought back 865 mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; 866 mPopupPreviewY = - mPreviewText.getMeasuredHeight(); 867 } 868 mHandler.removeMessages(MSG_REMOVE_PREVIEW); 869 if (mOffsetInWindow == null) { 870 mOffsetInWindow = new int[2]; 871 getLocationInWindow(mOffsetInWindow); 872 mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero 873 mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero 874 } 875 // Set the preview background state 876 mPreviewText.getBackground().setState( 877 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 878 if (previewPopup.isShowing()) { 879 previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], 880 mPopupPreviewY + mOffsetInWindow[1], 881 popupWidth, popupHeight); 882 } else { 883 previewPopup.setWidth(popupWidth); 884 previewPopup.setHeight(popupHeight); 885 previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, 886 mPopupPreviewX + mOffsetInWindow[0], 887 mPopupPreviewY + mOffsetInWindow[1]); 888 } 889 mPreviewText.setVisibility(VISIBLE); 890 } 891 892 /** 893 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 894 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 895 * draws the cached buffer. 896 * @see #invalidateKey(int) 897 */ 898 public void invalidateAllKeys() { 899 mDirtyRect.union(0, 0, getWidth(), getHeight()); 900 mDrawPending = true; 901 invalidate(); 902 } 903 904 /** 905 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 906 * one key is changing it's content. Any changes that affect the position or size of the key 907 * may not be honored. 908 * @param keyIndex the index of the key in the attached {@link Keyboard}. 909 * @see #invalidateAllKeys 910 */ 911 public void invalidateKey(int keyIndex) { 912 if (mKeys == null) return; 913 if (keyIndex < 0 || keyIndex >= mKeys.length) { 914 return; 915 } 916 final Key key = mKeys[keyIndex]; 917 mInvalidatedKey = key; 918 mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop, 919 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 920 onBufferDraw(); 921 invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, 922 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 923 } 924 925 private boolean openPopupIfRequired(MotionEvent me) { 926 // Check if we have a popup layout specified first. 927 if (mPopupLayout == 0) { 928 return false; 929 } 930 if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { 931 return false; 932 } 933 934 Key popupKey = mKeys[mCurrentKey]; 935 boolean result = onLongPress(popupKey); 936 if (result) { 937 mAbortKey = true; 938 showPreview(NOT_A_KEY); 939 } 940 return result; 941 } 942 943 /** 944 * Called when a key is long pressed. By default this will open any popup keyboard associated 945 * with this key through the attributes popupLayout and popupCharacters. 946 * @param popupKey the key that was long pressed 947 * @return true if the long press is handled, false otherwise. Subclasses should call the 948 * method on the base class if the subclass doesn't wish to handle the call. 949 */ 950 protected boolean onLongPress(Key popupKey) { 951 int popupKeyboardId = popupKey.popupResId; 952 953 if (popupKeyboardId != 0) { 954 mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); 955 if (mMiniKeyboardContainer == null) { 956 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 957 Context.LAYOUT_INFLATER_SERVICE); 958 mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); 959 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 960 com.android.internal.R.id.keyboardView); 961 View closeButton = mMiniKeyboardContainer.findViewById( 962 com.android.internal.R.id.closeButton); 963 if (closeButton != null) closeButton.setOnClickListener(this); 964 mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { 965 public void onKey(int primaryCode, int[] keyCodes) { 966 mKeyboardActionListener.onKey(primaryCode, keyCodes); 967 dismissPopupKeyboard(); 968 } 969 970 public void onText(CharSequence text) { 971 mKeyboardActionListener.onText(text); 972 dismissPopupKeyboard(); 973 } 974 975 public void swipeLeft() { } 976 public void swipeRight() { } 977 public void swipeUp() { } 978 public void swipeDown() { } 979 public void onPress(int primaryCode) { 980 mKeyboardActionListener.onPress(primaryCode); 981 } 982 public void onRelease(int primaryCode) { 983 mKeyboardActionListener.onRelease(primaryCode); 984 } 985 }); 986 //mInputView.setSuggest(mSuggest); 987 Keyboard keyboard; 988 if (popupKey.popupCharacters != null) { 989 keyboard = new Keyboard(getContext(), popupKeyboardId, 990 popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); 991 } else { 992 keyboard = new Keyboard(getContext(), popupKeyboardId); 993 } 994 mMiniKeyboard.setKeyboard(keyboard); 995 mMiniKeyboard.setPopupParent(this); 996 mMiniKeyboardContainer.measure( 997 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 998 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 999 1000 mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); 1001 } else { 1002 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 1003 com.android.internal.R.id.keyboardView); 1004 } 1005 if (mWindowOffset == null) { 1006 mWindowOffset = new int[2]; 1007 getLocationInWindow(mWindowOffset); 1008 } 1009 mPopupX = popupKey.x + mPaddingLeft; 1010 mPopupY = popupKey.y + mPaddingTop; 1011 mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); 1012 mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); 1013 final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; 1014 final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; 1015 mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); 1016 mMiniKeyboard.setShifted(isShifted()); 1017 mPopupKeyboard.setContentView(mMiniKeyboardContainer); 1018 mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); 1019 mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); 1020 mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1021 mMiniKeyboardOnScreen = true; 1022 //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); 1023 invalidateAllKeys(); 1024 return true; 1025 } 1026 return false; 1027 } 1028 1029 @Override 1030 public boolean onTouchEvent(MotionEvent me) { 1031 // Convert multi-pointer up/down events to single up/down events to 1032 // deal with the typical multi-pointer behavior of two-thumb typing 1033 int pointerCount = me.getPointerCount(); 1034 boolean result = false; 1035 if (pointerCount != mOldPointerCount) { 1036 long now = me.getEventTime(); 1037 if (pointerCount == 1) { 1038 // Send a down event for the latest pointer 1039 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 1040 me.getX(), me.getY(), me.getMetaState()); 1041 result = onModifiedTouchEvent(down); 1042 down.recycle(); 1043 // If it's an up action, then deliver the up as well. 1044 if (me.getAction() == MotionEvent.ACTION_UP) { 1045 result = onModifiedTouchEvent(me); 1046 } 1047 } else { 1048 // Send an up event for the last pointer 1049 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 1050 mOldPointerX, mOldPointerY, me.getMetaState()); 1051 result = onModifiedTouchEvent(up); 1052 up.recycle(); 1053 } 1054 } else { 1055 if (pointerCount == 1) { 1056 mOldPointerX = me.getX(); 1057 mOldPointerY = me.getY(); 1058 result = onModifiedTouchEvent(me); 1059 } else { 1060 // Don't do anything when 2 pointers are down and moving. 1061 result = true; 1062 } 1063 } 1064 mOldPointerCount = pointerCount; 1065 return result; 1066 } 1067 1068 private boolean onModifiedTouchEvent(MotionEvent me) { 1069 int touchX = (int) me.getX() - mPaddingLeft; 1070 int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; 1071 int action = me.getAction(); 1072 long eventTime = me.getEventTime(); 1073 int keyIndex = getKeyIndices(touchX, touchY, null); 1074 if (mGestureDetector.onTouchEvent(me)) { 1075 showPreview(NOT_A_KEY); 1076 mHandler.removeMessages(MSG_REPEAT); 1077 mHandler.removeMessages(MSG_LONGPRESS); 1078 return true; 1079 } 1080 1081 // Needs to be called after the gesture detector gets a turn, as it may have 1082 // displayed the mini keyboard 1083 if (mMiniKeyboardOnScreen) { 1084 return true; 1085 } 1086 1087 switch (action) { 1088 case MotionEvent.ACTION_DOWN: 1089 mAbortKey = false; 1090 mStartX = touchX; 1091 mStartY = touchY; 1092 mLastCodeX = touchX; 1093 mLastCodeY = touchY; 1094 mLastKeyTime = 0; 1095 mCurrentKeyTime = 0; 1096 mLastKey = NOT_A_KEY; 1097 mCurrentKey = keyIndex; 1098 mDownTime = me.getEventTime(); 1099 mLastMoveTime = mDownTime; 1100 checkMultiTap(eventTime, keyIndex); 1101 mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? 1102 mKeys[keyIndex].codes[0] : 0); 1103 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { 1104 mRepeatKeyIndex = mCurrentKey; 1105 repeatKey(); 1106 Message msg = mHandler.obtainMessage(MSG_REPEAT); 1107 mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); 1108 } 1109 if (mCurrentKey != NOT_A_KEY) { 1110 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1111 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1112 } 1113 showPreview(keyIndex); 1114 break; 1115 1116 case MotionEvent.ACTION_MOVE: 1117 boolean continueLongPress = false; 1118 if (keyIndex != NOT_A_KEY) { 1119 if (mCurrentKey == NOT_A_KEY) { 1120 mCurrentKey = keyIndex; 1121 mCurrentKeyTime = eventTime - mDownTime; 1122 } else { 1123 if (keyIndex == mCurrentKey) { 1124 mCurrentKeyTime += eventTime - mLastMoveTime; 1125 continueLongPress = true; 1126 } else if (mRepeatKeyIndex == NOT_A_KEY) { 1127 resetMultiTap(); 1128 mLastKey = mCurrentKey; 1129 mLastCodeX = mLastX; 1130 mLastCodeY = mLastY; 1131 mLastKeyTime = 1132 mCurrentKeyTime + eventTime - mLastMoveTime; 1133 mCurrentKey = keyIndex; 1134 mCurrentKeyTime = 0; 1135 } 1136 } 1137 } 1138 if (!continueLongPress) { 1139 // Cancel old longpress 1140 mHandler.removeMessages(MSG_LONGPRESS); 1141 // Start new longpress if key has changed 1142 if (keyIndex != NOT_A_KEY) { 1143 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1144 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1145 } 1146 } 1147 showPreview(mCurrentKey); 1148 break; 1149 1150 case MotionEvent.ACTION_UP: 1151 removeMessages(); 1152 if (keyIndex == mCurrentKey) { 1153 mCurrentKeyTime += eventTime - mLastMoveTime; 1154 } else { 1155 resetMultiTap(); 1156 mLastKey = mCurrentKey; 1157 mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; 1158 mCurrentKey = keyIndex; 1159 mCurrentKeyTime = 0; 1160 } 1161 if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) { 1162 mCurrentKey = mLastKey; 1163 touchX = mLastCodeX; 1164 touchY = mLastCodeY; 1165 } 1166 showPreview(NOT_A_KEY); 1167 Arrays.fill(mKeyIndices, NOT_A_KEY); 1168 // If we're not on a repeating key (which sends on a DOWN event) 1169 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { 1170 detectAndSendKey(touchX, touchY, eventTime); 1171 } 1172 invalidateKey(keyIndex); 1173 mRepeatKeyIndex = NOT_A_KEY; 1174 break; 1175 } 1176 mLastX = touchX; 1177 mLastY = touchY; 1178 return true; 1179 } 1180 1181 private boolean repeatKey() { 1182 Key key = mKeys[mRepeatKeyIndex]; 1183 detectAndSendKey(key.x, key.y, mLastTapTime); 1184 return true; 1185 } 1186 1187 protected void swipeRight() { 1188 mKeyboardActionListener.swipeRight(); 1189 } 1190 1191 protected void swipeLeft() { 1192 mKeyboardActionListener.swipeLeft(); 1193 } 1194 1195 protected void swipeUp() { 1196 mKeyboardActionListener.swipeUp(); 1197 } 1198 1199 protected void swipeDown() { 1200 mKeyboardActionListener.swipeDown(); 1201 } 1202 1203 public void closing() { 1204 if (mPreviewPopup.isShowing()) { 1205 mPreviewPopup.dismiss(); 1206 } 1207 removeMessages(); 1208 1209 dismissPopupKeyboard(); 1210 mBuffer = null; 1211 mCanvas = null; 1212 mMiniKeyboardCache.clear(); 1213 } 1214 1215 private void removeMessages() { 1216 mHandler.removeMessages(MSG_REPEAT); 1217 mHandler.removeMessages(MSG_LONGPRESS); 1218 mHandler.removeMessages(MSG_SHOW_PREVIEW); 1219 } 1220 1221 @Override 1222 public void onDetachedFromWindow() { 1223 super.onDetachedFromWindow(); 1224 closing(); 1225 } 1226 1227 private void dismissPopupKeyboard() { 1228 if (mPopupKeyboard.isShowing()) { 1229 mPopupKeyboard.dismiss(); 1230 mMiniKeyboardOnScreen = false; 1231 invalidateAllKeys(); 1232 } 1233 } 1234 1235 public boolean handleBack() { 1236 if (mPopupKeyboard.isShowing()) { 1237 dismissPopupKeyboard(); 1238 return true; 1239 } 1240 return false; 1241 } 1242 1243 private void resetMultiTap() { 1244 mLastSentIndex = NOT_A_KEY; 1245 mTapCount = 0; 1246 mLastTapTime = -1; 1247 mInMultiTap = false; 1248 } 1249 1250 private void checkMultiTap(long eventTime, int keyIndex) { 1251 if (keyIndex == NOT_A_KEY) return; 1252 Key key = mKeys[keyIndex]; 1253 if (key.codes.length > 1) { 1254 mInMultiTap = true; 1255 if (eventTime < mLastTapTime + MULTITAP_INTERVAL 1256 && keyIndex == mLastSentIndex) { 1257 mTapCount = (mTapCount + 1) % key.codes.length; 1258 return; 1259 } else { 1260 mTapCount = -1; 1261 return; 1262 } 1263 } 1264 if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { 1265 resetMultiTap(); 1266 } 1267 } 1268} 1269