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