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