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