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