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