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