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