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