KeyboardView.java revision b974c7a7495849beac1a904fae0839080cd382dc
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 17package android.inputmethodservice; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.PorterDuff; 25import android.graphics.Rect; 26import android.graphics.Typeface; 27import android.graphics.Paint.Align; 28import android.graphics.Region.Op; 29import android.graphics.drawable.Drawable; 30import android.inputmethodservice.Keyboard.Key; 31import android.os.Handler; 32import android.os.Message; 33import android.util.AttributeSet; 34import android.view.GestureDetector; 35import android.view.Gravity; 36import android.view.LayoutInflater; 37import android.view.MotionEvent; 38import android.view.View; 39import android.view.ViewConfiguration; 40import android.view.ViewGroup.LayoutParams; 41import android.widget.PopupWindow; 42import android.widget.TextView; 43 44import com.android.internal.R; 45 46import java.util.Arrays; 47import java.util.HashMap; 48import java.util.List; 49import java.util.Map; 50 51/** 52 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and 53 * detecting key presses and touch movements. 54 * 55 * @attr ref android.R.styleable#KeyboardView_keyBackground 56 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout 57 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset 58 * @attr ref android.R.styleable#KeyboardView_labelTextSize 59 * @attr ref android.R.styleable#KeyboardView_keyTextSize 60 * @attr ref android.R.styleable#KeyboardView_keyTextColor 61 * @attr ref android.R.styleable#KeyboardView_verticalCorrection 62 * @attr ref android.R.styleable#KeyboardView_popupLayout 63 */ 64public class KeyboardView extends View implements View.OnClickListener { 65 66 /** 67 * Listener for virtual keyboard events. 68 */ 69 public interface OnKeyboardActionListener { 70 71 /** 72 * Called when the user presses a key. This is sent before the {@link #onKey} is called. 73 * For keys that repeat, this is only called once. 74 * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid 75 * key, the value will be zero. 76 */ 77 void onPress(int primaryCode); 78 79 /** 80 * Called when the user releases a key. This is sent after the {@link #onKey} is called. 81 * For keys that repeat, this is only called once. 82 * @param primaryCode the code of the key that was released 83 */ 84 void onRelease(int primaryCode); 85 86 /** 87 * Send a key press to the listener. 88 * @param primaryCode this is the key that was pressed 89 * @param keyCodes the codes for all the possible alternative keys 90 * with the primary code being the first. If the primary key code is 91 * a single character such as an alphabet or number or symbol, the alternatives 92 * will include other characters that may be on the same key or adjacent keys. 93 * These codes are useful to correct for accidental presses of a key adjacent to 94 * the intended key. 95 */ 96 void onKey(int primaryCode, int[] keyCodes); 97 98 /** 99 * Sends a sequence of characters to the listener. 100 * @param text the sequence of characters to be displayed. 101 */ 102 void onText(CharSequence text); 103 104 /** 105 * Called when the user quickly moves the finger from right to left. 106 */ 107 void swipeLeft(); 108 109 /** 110 * Called when the user quickly moves the finger from left to right. 111 */ 112 void swipeRight(); 113 114 /** 115 * Called when the user quickly moves the finger from up to down. 116 */ 117 void swipeDown(); 118 119 /** 120 * Called when the user quickly moves the finger from down to up. 121 */ 122 void swipeUp(); 123 } 124 125 private static final boolean DEBUG = false; 126 private static final int NOT_A_KEY = -1; 127 private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; 128 private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable }; 129 130 private Keyboard mKeyboard; 131 private int mCurrentKeyIndex = NOT_A_KEY; 132 private int mLabelTextSize; 133 private int mKeyTextSize; 134 private int mKeyTextColor; 135 private float mShadowRadius; 136 private int mShadowColor; 137 private float mBackgroundDimAmount; 138 139 private TextView mPreviewText; 140 private PopupWindow mPreviewPopup; 141 private int mPreviewTextSizeLarge; 142 private int mPreviewOffset; 143 private int mPreviewHeight; 144 private int[] mOffsetInWindow; 145 146 private PopupWindow mPopupKeyboard; 147 private View mMiniKeyboardContainer; 148 private KeyboardView mMiniKeyboard; 149 private boolean mMiniKeyboardOnScreen; 150 private View mPopupParent; 151 private int mMiniKeyboardOffsetX; 152 private int mMiniKeyboardOffsetY; 153 private Map<Key,View> mMiniKeyboardCache; 154 private int[] mWindowOffset; 155 private Key[] mKeys; 156 157 /** Listener for {@link OnKeyboardActionListener}. */ 158 private OnKeyboardActionListener mKeyboardActionListener; 159 160 private static final int MSG_SHOW_PREVIEW = 1; 161 private static final int MSG_REMOVE_PREVIEW = 2; 162 private static final int MSG_REPEAT = 3; 163 private static final int MSG_LONGPRESS = 4; 164 165 private static final int DELAY_BEFORE_PREVIEW = 70; 166 private static final int DELAY_AFTER_PREVIEW = 60; 167 168 private int mVerticalCorrection; 169 private int mProximityThreshold; 170 171 private boolean mPreviewCentered = false; 172 private boolean mShowPreview = true; 173 private boolean mShowTouchPoints = true; 174 private int mPopupPreviewX; 175 private int mPopupPreviewY; 176 177 private int mLastX; 178 private int mLastY; 179 private int mStartX; 180 private int mStartY; 181 182 private boolean mProximityCorrectOn; 183 184 private Paint mPaint; 185 private Rect mPadding; 186 187 private long mDownTime; 188 private long mLastMoveTime; 189 private int mLastKey; 190 private int mLastCodeX; 191 private int mLastCodeY; 192 private int mCurrentKey = NOT_A_KEY; 193 private long mLastKeyTime; 194 private long mCurrentKeyTime; 195 private int[] mKeyIndices = new int[12]; 196 private GestureDetector mGestureDetector; 197 private int mPopupX; 198 private int mPopupY; 199 private int mRepeatKeyIndex = NOT_A_KEY; 200 private int mPopupLayout; 201 private boolean mAbortKey; 202 private Key mInvalidatedKey; 203 private Rect mClipRegion = new Rect(0, 0, 0, 0); 204 205 private Drawable mKeyBackground; 206 207 private static final int REPEAT_INTERVAL = 50; // ~20 keys per second 208 private static final int REPEAT_START_DELAY = 400; 209 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 210 211 private static int MAX_NEARBY_KEYS = 12; 212 private int[] mDistances = new int[MAX_NEARBY_KEYS]; 213 214 // For multi-tap 215 private int mLastSentIndex; 216 private int mTapCount; 217 private long mLastTapTime; 218 private boolean mInMultiTap; 219 private static final int MULTITAP_INTERVAL = 800; // milliseconds 220 private StringBuilder mPreviewLabel = new StringBuilder(1); 221 222 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 223 private boolean mDrawPending; 224 /** The dirty region in the keyboard bitmap */ 225 private Rect mDirtyRect = new Rect(); 226 /** The keyboard bitmap for faster updates */ 227 private Bitmap mBuffer; 228 /** The canvas for the above mutable keyboard bitmap */ 229 private Canvas mCanvas; 230 231 Handler mHandler = new Handler() { 232 @Override 233 public void handleMessage(Message msg) { 234 switch (msg.what) { 235 case MSG_SHOW_PREVIEW: 236 showKey(msg.arg1); 237 break; 238 case MSG_REMOVE_PREVIEW: 239 mPreviewText.setVisibility(INVISIBLE); 240 break; 241 case MSG_REPEAT: 242 if (repeatKey()) { 243 Message repeat = Message.obtain(this, MSG_REPEAT); 244 sendMessageDelayed(repeat, REPEAT_INTERVAL); 245 } 246 break; 247 case MSG_LONGPRESS: 248 openPopupIfRequired((MotionEvent) msg.obj); 249 break; 250 } 251 } 252 }; 253 254 public KeyboardView(Context context, AttributeSet attrs) { 255 this(context, attrs, com.android.internal.R.attr.keyboardViewStyle); 256 } 257 258 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 259 super(context, attrs, defStyle); 260 261 TypedArray a = 262 context.obtainStyledAttributes( 263 attrs, android.R.styleable.KeyboardView, defStyle, 0); 264 265 LayoutInflater inflate = 266 (LayoutInflater) context 267 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 268 269 int previewLayout = 0; 270 int keyTextSize = 0; 271 272 int n = a.getIndexCount(); 273 274 for (int i = 0; i < n; i++) { 275 int attr = a.getIndex(i); 276 277 switch (attr) { 278 case com.android.internal.R.styleable.KeyboardView_keyBackground: 279 mKeyBackground = a.getDrawable(attr); 280 break; 281 case com.android.internal.R.styleable.KeyboardView_verticalCorrection: 282 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); 283 break; 284 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout: 285 previewLayout = a.getResourceId(attr, 0); 286 break; 287 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset: 288 mPreviewOffset = a.getDimensionPixelOffset(attr, 0); 289 break; 290 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight: 291 mPreviewHeight = a.getDimensionPixelSize(attr, 80); 292 break; 293 case com.android.internal.R.styleable.KeyboardView_keyTextSize: 294 mKeyTextSize = a.getDimensionPixelSize(attr, 18); 295 break; 296 case com.android.internal.R.styleable.KeyboardView_keyTextColor: 297 mKeyTextColor = a.getColor(attr, 0xFF000000); 298 break; 299 case com.android.internal.R.styleable.KeyboardView_labelTextSize: 300 mLabelTextSize = a.getDimensionPixelSize(attr, 14); 301 break; 302 case com.android.internal.R.styleable.KeyboardView_popupLayout: 303 mPopupLayout = a.getResourceId(attr, 0); 304 break; 305 case com.android.internal.R.styleable.KeyboardView_shadowColor: 306 mShadowColor = a.getColor(attr, 0); 307 break; 308 case com.android.internal.R.styleable.KeyboardView_shadowRadius: 309 mShadowRadius = a.getFloat(attr, 0f); 310 break; 311 } 312 } 313 314 a = mContext.obtainStyledAttributes( 315 com.android.internal.R.styleable.Theme); 316 mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f); 317 318 mPreviewPopup = new PopupWindow(context); 319 if (previewLayout != 0) { 320 mPreviewText = (TextView) inflate.inflate(previewLayout, null); 321 mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); 322 mPreviewPopup.setContentView(mPreviewText); 323 mPreviewPopup.setBackgroundDrawable(null); 324 } else { 325 mShowPreview = false; 326 } 327 328 mPreviewPopup.setTouchable(false); 329 330 mPopupKeyboard = new PopupWindow(context); 331 mPopupKeyboard.setBackgroundDrawable(null); 332 //mPopupKeyboard.setClippingEnabled(false); 333 334 mPopupParent = this; 335 //mPredicting = true; 336 337 mPaint = new Paint(); 338 mPaint.setAntiAlias(true); 339 mPaint.setTextSize(keyTextSize); 340 mPaint.setTextAlign(Align.CENTER); 341 342 mPadding = new Rect(0, 0, 0, 0); 343 mMiniKeyboardCache = new HashMap<Key,View>(); 344 mKeyBackground.getPadding(mPadding); 345 346 resetMultiTap(); 347 initGestureDetector(); 348 } 349 350 private void initGestureDetector() { 351 mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 352 @Override 353 public boolean onFling(MotionEvent me1, MotionEvent me2, 354 float velocityX, float velocityY) { 355 final float absX = Math.abs(velocityX); 356 final float absY = Math.abs(velocityY); 357 if (velocityX > 500 && absY < absX) { 358 swipeRight(); 359 return true; 360 } else if (velocityX < -500 && absY < absX) { 361 swipeLeft(); 362 return true; 363 } else if (velocityY < -500 && absX < absY) { 364 swipeUp(); 365 return true; 366 } else if (velocityY > 500 && absX < 200) { 367 swipeDown(); 368 return true; 369 } else if (absX > 800 || absY > 800) { 370 return true; 371 } 372 return false; 373 } 374 }); 375 376 mGestureDetector.setIsLongpressEnabled(false); 377 } 378 379 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { 380 mKeyboardActionListener = listener; 381 } 382 383 /** 384 * Returns the {@link OnKeyboardActionListener} object. 385 * @return the listener attached to this keyboard 386 */ 387 protected OnKeyboardActionListener getOnKeyboardActionListener() { 388 return mKeyboardActionListener; 389 } 390 391 /** 392 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 393 * view will re-layout itself to accommodate the keyboard. 394 * @see Keyboard 395 * @see #getKeyboard() 396 * @param keyboard the keyboard to display in this view 397 */ 398 public void setKeyboard(Keyboard keyboard) { 399 if (mKeyboard != null) { 400 showPreview(NOT_A_KEY); 401 } 402 mKeyboard = keyboard; 403 List<Key> keys = mKeyboard.getKeys(); 404 mKeys = keys.toArray(new Key[keys.size()]); 405 requestLayout(); 406 // Release buffer, just in case the new keyboard has a different size. 407 // It will be reallocated on the next draw. 408 mBuffer = null; 409 invalidateAllKeys(); 410 computeProximityThreshold(keyboard); 411 mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views 412 // Switching to a different keyboard should abort any pending keys so that the key up 413 // doesn't get delivered to the old or new keyboard 414 mAbortKey = true; // Until the next ACTION_DOWN 415 } 416 417 /** 418 * Returns the current keyboard being displayed by this view. 419 * @return the currently attached keyboard 420 * @see #setKeyboard(Keyboard) 421 */ 422 public Keyboard getKeyboard() { 423 return mKeyboard; 424 } 425 426 /** 427 * Sets the state of the shift key of the keyboard, if any. 428 * @param shifted whether or not to enable the state of the shift key 429 * @return true if the shift key state changed, false if there was no change 430 * @see KeyboardView#isShifted() 431 */ 432 public boolean setShifted(boolean shifted) { 433 if (mKeyboard != null) { 434 if (mKeyboard.setShifted(shifted)) { 435 // The whole keyboard probably needs to be redrawn 436 invalidateAllKeys(); 437 return true; 438 } 439 } 440 return false; 441 } 442 443 /** 444 * Returns the state of the shift key of the keyboard, if any. 445 * @return true if the shift is in a pressed state, false otherwise. If there is 446 * no shift key on the keyboard or there is no keyboard attached, it returns false. 447 * @see KeyboardView#setShifted(boolean) 448 */ 449 public boolean isShifted() { 450 if (mKeyboard != null) { 451 return mKeyboard.isShifted(); 452 } 453 return false; 454 } 455 456 /** 457 * Enables or disables the key feedback popup. This is a popup that shows a magnified 458 * version of the depressed key. By default the preview is enabled. 459 * @param previewEnabled whether or not to enable the key feedback popup 460 * @see #isPreviewEnabled() 461 */ 462 public void setPreviewEnabled(boolean previewEnabled) { 463 mShowPreview = previewEnabled; 464 } 465 466 /** 467 * Returns the enabled state of the key feedback popup. 468 * @return whether or not the key feedback popup is enabled 469 * @see #setPreviewEnabled(boolean) 470 */ 471 public boolean isPreviewEnabled() { 472 return mShowPreview; 473 } 474 475 public void setVerticalCorrection(int verticalOffset) { 476 477 } 478 public void setPopupParent(View v) { 479 mPopupParent = v; 480 } 481 482 public void setPopupOffset(int x, int y) { 483 mMiniKeyboardOffsetX = x; 484 mMiniKeyboardOffsetY = y; 485 if (mPreviewPopup.isShowing()) { 486 mPreviewPopup.dismiss(); 487 } 488 } 489 490 /** 491 * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key 492 * codes for adjacent keys. When disabled, only the primary key code will be 493 * reported. 494 * @param enabled whether or not the proximity correction is enabled 495 */ 496 public void setProximityCorrectionEnabled(boolean enabled) { 497 mProximityCorrectOn = enabled; 498 } 499 500 /** 501 * Returns true if proximity correction is enabled. 502 */ 503 public boolean isProximityCorrectionEnabled() { 504 return mProximityCorrectOn; 505 } 506 507 /** 508 * Popup keyboard close button clicked. 509 * @hide 510 */ 511 public void onClick(View v) { 512 dismissPopupKeyboard(); 513 } 514 515 private CharSequence adjustCase(CharSequence label) { 516 if (mKeyboard.isShifted() && label != null && label.length() < 3 517 && Character.isLowerCase(label.charAt(0))) { 518 label = label.toString().toUpperCase(); 519 } 520 return label; 521 } 522 523 @Override 524 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 525 // Round up a little 526 if (mKeyboard == null) { 527 setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom); 528 } else { 529 int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight; 530 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 531 width = MeasureSpec.getSize(widthMeasureSpec); 532 } 533 setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom); 534 } 535 } 536 537 /** 538 * Compute the average distance between adjacent keys (horizontally and vertically) 539 * and square it to get the proximity threshold. We use a square here and in computing 540 * the touch distance from a key's center to avoid taking a square root. 541 * @param keyboard 542 */ 543 private void computeProximityThreshold(Keyboard keyboard) { 544 if (keyboard == null) return; 545 final Key[] keys = mKeys; 546 if (keys == null) return; 547 int length = keys.length; 548 int dimensionSum = 0; 549 for (int i = 0; i < length; i++) { 550 Key key = keys[i]; 551 dimensionSum += Math.min(key.width, key.height) + key.gap; 552 } 553 if (dimensionSum < 0 || length == 0) return; 554 mProximityThreshold = (int) (dimensionSum * 1.4f / length); 555 mProximityThreshold *= mProximityThreshold; // Square it 556 } 557 558 @Override 559 public void onSizeChanged(int w, int h, int oldw, int oldh) { 560 super.onSizeChanged(w, h, oldw, oldh); 561 // Release the buffer, if any and it will be reallocated on the next draw 562 mBuffer = null; 563 } 564 565 @Override 566 public void onDraw(Canvas canvas) { 567 super.onDraw(canvas); 568 if (mDrawPending || mBuffer == null) { 569 onBufferDraw(); 570 } 571 canvas.drawBitmap(mBuffer, 0, 0, null); 572 } 573 574 private void onBufferDraw() { 575 if (mBuffer == null) { 576 mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 577 mCanvas = new Canvas(mBuffer); 578 invalidateAllKeys(); 579 } 580 final Canvas canvas = mCanvas; 581 canvas.clipRect(mDirtyRect, Op.REPLACE); 582 583 if (mKeyboard == null) return; 584 585 final Paint paint = mPaint; 586 final Drawable keyBackground = mKeyBackground; 587 final Rect clipRegion = mClipRegion; 588 final Rect padding = mPadding; 589 final int kbdPaddingLeft = mPaddingLeft; 590 final int kbdPaddingTop = mPaddingTop; 591 final Key[] keys = mKeys; 592 final Key invalidKey = mInvalidatedKey; 593 594 paint.setAlpha(255); 595 paint.setColor(mKeyTextColor); 596 boolean drawSingleKey = false; 597 if (invalidKey != null && canvas.getClipBounds(clipRegion)) { 598 // Is clipRegion completely contained within the invalidated key? 599 if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && 600 invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && 601 invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && 602 invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { 603 drawSingleKey = true; 604 } 605 } 606 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 607 final int keyCount = keys.length; 608 for (int i = 0; i < keyCount; i++) { 609 final Key key = keys[i]; 610 if (drawSingleKey && invalidKey != key) { 611 continue; 612 } 613 int[] drawableState = key.getCurrentDrawableState(); 614 keyBackground.setState(drawableState); 615 616 // Switch the character to uppercase if shift is pressed 617 String label = key.label == null? null : adjustCase(key.label).toString(); 618 619 final Rect bounds = keyBackground.getBounds(); 620 if (key.width != bounds.right || 621 key.height != bounds.bottom) { 622 keyBackground.setBounds(0, 0, key.width, key.height); 623 } 624 canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); 625 keyBackground.draw(canvas); 626 627 if (label != null) { 628 // For characters, use large font. For labels like "Done", use small font. 629 if (label.length() > 1 && key.codes.length < 2) { 630 paint.setTextSize(mLabelTextSize); 631 paint.setTypeface(Typeface.DEFAULT_BOLD); 632 } else { 633 paint.setTextSize(mKeyTextSize); 634 paint.setTypeface(Typeface.DEFAULT); 635 } 636 // Draw a drop shadow for the text 637 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 638 // Draw the text 639 canvas.drawText(label, 640 (key.width - padding.left - padding.right) / 2 641 + padding.left, 642 (key.height - padding.top - padding.bottom) / 2 643 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, 644 paint); 645 // Turn off drop shadow 646 paint.setShadowLayer(0, 0, 0, 0); 647 } else if (key.icon != null) { 648 final int drawableX = (key.width - padding.left - padding.right 649 - key.icon.getIntrinsicWidth()) / 2 + padding.left; 650 final int drawableY = (key.height - padding.top - padding.bottom 651 - key.icon.getIntrinsicHeight()) / 2 + padding.top; 652 canvas.translate(drawableX, drawableY); 653 key.icon.setBounds(0, 0, 654 key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); 655 key.icon.draw(canvas); 656 canvas.translate(-drawableX, -drawableY); 657 } 658 canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); 659 } 660 mInvalidatedKey = null; 661 // Overlay a dark rectangle to dim the keyboard 662 if (mMiniKeyboardOnScreen) { 663 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 664 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 665 } 666 667 if (DEBUG && mShowTouchPoints) { 668 paint.setAlpha(128); 669 paint.setColor(0xFFFF0000); 670 canvas.drawCircle(mStartX, mStartY, 3, paint); 671 canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); 672 paint.setColor(0xFF0000FF); 673 canvas.drawCircle(mLastX, mLastY, 3, paint); 674 paint.setColor(0xFF00FF00); 675 canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); 676 } 677 678 mDrawPending = false; 679 mDirtyRect.setEmpty(); 680 } 681 682 private int getKeyIndices(int x, int y, int[] allKeys) { 683 final Key[] keys = mKeys; 684 final boolean shifted = mKeyboard.isShifted(); 685 int primaryIndex = NOT_A_KEY; 686 int closestKey = NOT_A_KEY; 687 int closestKeyDist = mProximityThreshold + 1; 688 java.util.Arrays.fill(mDistances, Integer.MAX_VALUE); 689 int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); 690 final int keyCount = nearestKeyIndices.length; 691 for (int i = 0; i < keyCount; i++) { 692 final Key key = keys[nearestKeyIndices[i]]; 693 int dist = 0; 694 boolean isInside = key.isInside(x,y); 695 if (((mProximityCorrectOn 696 && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) 697 || isInside) 698 && key.codes[0] > 32) { 699 // Find insertion point 700 final int nCodes = key.codes.length; 701 if (dist < closestKeyDist) { 702 closestKeyDist = dist; 703 closestKey = nearestKeyIndices[i]; 704 } 705 706 if (allKeys == null) continue; 707 708 for (int j = 0; j < mDistances.length; j++) { 709 if (mDistances[j] > dist) { 710 // Make space for nCodes codes 711 System.arraycopy(mDistances, j, mDistances, j + nCodes, 712 mDistances.length - j - nCodes); 713 System.arraycopy(allKeys, j, allKeys, j + nCodes, 714 allKeys.length - j - nCodes); 715 for (int c = 0; c < nCodes; c++) { 716 allKeys[j + c] = key.codes[c]; 717 mDistances[j + c] = dist; 718 } 719 break; 720 } 721 } 722 } 723 724 if (isInside) { 725 primaryIndex = nearestKeyIndices[i]; 726 } 727 } 728 if (primaryIndex == NOT_A_KEY) { 729 primaryIndex = closestKey; 730 } 731 return primaryIndex; 732 } 733 734 private void detectAndSendKey(int x, int y, long eventTime) { 735 int index = mCurrentKey; 736 if (index != NOT_A_KEY && index < mKeys.length) { 737 final Key key = mKeys[index]; 738 if (key.text != null) { 739 mKeyboardActionListener.onText(key.text); 740 mKeyboardActionListener.onRelease(NOT_A_KEY); 741 } else { 742 int code = key.codes[0]; 743 //TextEntryState.keyPressedAt(key, x, y); 744 int[] codes = new int[MAX_NEARBY_KEYS]; 745 Arrays.fill(codes, NOT_A_KEY); 746 getKeyIndices(x, y, codes); 747 // Multi-tap 748 if (mInMultiTap) { 749 if (mTapCount != -1) { 750 mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); 751 } else { 752 mTapCount = 0; 753 } 754 code = key.codes[mTapCount]; 755 } 756 mKeyboardActionListener.onKey(code, codes); 757 mKeyboardActionListener.onRelease(code); 758 } 759 mLastSentIndex = index; 760 mLastTapTime = eventTime; 761 } 762 } 763 764 /** 765 * Handle multi-tap keys by producing the key label for the current multi-tap state. 766 */ 767 private CharSequence getPreviewText(Key key) { 768 if (mInMultiTap) { 769 // Multi-tap 770 mPreviewLabel.setLength(0); 771 mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); 772 return adjustCase(mPreviewLabel); 773 } else { 774 return adjustCase(key.label); 775 } 776 } 777 778 private void showPreview(int keyIndex) { 779 int oldKeyIndex = mCurrentKeyIndex; 780 final PopupWindow previewPopup = mPreviewPopup; 781 782 mCurrentKeyIndex = keyIndex; 783 // Release the old key and press the new key 784 final Key[] keys = mKeys; 785 if (oldKeyIndex != mCurrentKeyIndex) { 786 if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { 787 keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY); 788 invalidateKey(oldKeyIndex); 789 } 790 if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { 791 keys[mCurrentKeyIndex].onPressed(); 792 invalidateKey(mCurrentKeyIndex); 793 } 794 } 795 // If key changed and preview is on ... 796 if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { 797 mHandler.removeMessages(MSG_SHOW_PREVIEW); 798 if (previewPopup.isShowing()) { 799 if (keyIndex == NOT_A_KEY) { 800 mHandler.sendMessageDelayed(mHandler 801 .obtainMessage(MSG_REMOVE_PREVIEW), 802 DELAY_AFTER_PREVIEW); 803 } 804 } 805 if (keyIndex != NOT_A_KEY) { 806 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { 807 // Show right away, if it's already visible and finger is moving around 808 showKey(keyIndex); 809 } else { 810 mHandler.sendMessageDelayed( 811 mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), 812 DELAY_BEFORE_PREVIEW); 813 } 814 } 815 } 816 } 817 818 private void showKey(final int keyIndex) { 819 final PopupWindow previewPopup = mPreviewPopup; 820 final Key[] keys = mKeys; 821 Key key = keys[keyIndex]; 822 if (key.icon != null) { 823 mPreviewText.setCompoundDrawables(null, null, null, 824 key.iconPreview != null ? key.iconPreview : key.icon); 825 mPreviewText.setText(null); 826 } else { 827 mPreviewText.setCompoundDrawables(null, null, null, null); 828 mPreviewText.setText(getPreviewText(key)); 829 if (key.label.length() > 1 && key.codes.length < 2) { 830 mPreviewText.setTextSize(mKeyTextSize); 831 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 832 } else { 833 mPreviewText.setTextSize(mPreviewTextSizeLarge); 834 mPreviewText.setTypeface(Typeface.DEFAULT); 835 } 836 } 837 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 838 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 839 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width 840 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 841 final int popupHeight = mPreviewHeight; 842 LayoutParams lp = mPreviewText.getLayoutParams(); 843 if (lp != null) { 844 lp.width = popupWidth; 845 lp.height = popupHeight; 846 } 847 if (!mPreviewCentered) { 848 mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft; 849 mPopupPreviewY = key.y - popupHeight + mPreviewOffset; 850 } else { 851 // TODO: Fix this if centering is brought back 852 mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; 853 mPopupPreviewY = - mPreviewText.getMeasuredHeight(); 854 } 855 mHandler.removeMessages(MSG_REMOVE_PREVIEW); 856 if (mOffsetInWindow == null) { 857 mOffsetInWindow = new int[2]; 858 getLocationInWindow(mOffsetInWindow); 859 mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero 860 mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero 861 } 862 // Set the preview background state 863 mPreviewText.getBackground().setState( 864 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 865 if (previewPopup.isShowing()) { 866 previewPopup.update(mPopupPreviewX + mOffsetInWindow[0], 867 mPopupPreviewY + mOffsetInWindow[1], 868 popupWidth, popupHeight); 869 } else { 870 previewPopup.setWidth(popupWidth); 871 previewPopup.setHeight(popupHeight); 872 previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, 873 mPopupPreviewX + mOffsetInWindow[0], 874 mPopupPreviewY + mOffsetInWindow[1]); 875 } 876 mPreviewText.setVisibility(VISIBLE); 877 } 878 879 /** 880 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 881 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 882 * draws the cached buffer. 883 * @see #invalidateKey(int) 884 */ 885 public void invalidateAllKeys() { 886 mDirtyRect.union(0, 0, getWidth(), getHeight()); 887 mDrawPending = true; 888 invalidate(); 889 } 890 891 /** 892 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 893 * one key is changing it's content. Any changes that affect the position or size of the key 894 * may not be honored. 895 * @param keyIndex the index of the key in the attached {@link Keyboard}. 896 * @see #invalidateAllKeys 897 */ 898 public void invalidateKey(int keyIndex) { 899 if (mKeys == null) return; 900 if (keyIndex < 0 || keyIndex >= mKeys.length) { 901 return; 902 } 903 final Key key = mKeys[keyIndex]; 904 mInvalidatedKey = key; 905 mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop, 906 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 907 onBufferDraw(); 908 invalidate(key.x + mPaddingLeft, key.y + mPaddingTop, 909 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop); 910 } 911 912 private boolean openPopupIfRequired(MotionEvent me) { 913 // Check if we have a popup layout specified first. 914 if (mPopupLayout == 0) { 915 return false; 916 } 917 if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { 918 return false; 919 } 920 921 Key popupKey = mKeys[mCurrentKey]; 922 boolean result = onLongPress(popupKey); 923 if (result) { 924 mAbortKey = true; 925 showPreview(NOT_A_KEY); 926 } 927 return result; 928 } 929 930 /** 931 * Called when a key is long pressed. By default this will open any popup keyboard associated 932 * with this key through the attributes popupLayout and popupCharacters. 933 * @param popupKey the key that was long pressed 934 * @return true if the long press is handled, false otherwise. Subclasses should call the 935 * method on the base class if the subclass doesn't wish to handle the call. 936 */ 937 protected boolean onLongPress(Key popupKey) { 938 int popupKeyboardId = popupKey.popupResId; 939 940 if (popupKeyboardId != 0) { 941 mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); 942 if (mMiniKeyboardContainer == null) { 943 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( 944 Context.LAYOUT_INFLATER_SERVICE); 945 mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); 946 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 947 com.android.internal.R.id.keyboardView); 948 View closeButton = mMiniKeyboardContainer.findViewById( 949 com.android.internal.R.id.closeButton); 950 if (closeButton != null) closeButton.setOnClickListener(this); 951 mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { 952 public void onKey(int primaryCode, int[] keyCodes) { 953 mKeyboardActionListener.onKey(primaryCode, keyCodes); 954 dismissPopupKeyboard(); 955 } 956 957 public void onText(CharSequence text) { 958 mKeyboardActionListener.onText(text); 959 dismissPopupKeyboard(); 960 } 961 962 public void swipeLeft() { } 963 public void swipeRight() { } 964 public void swipeUp() { } 965 public void swipeDown() { } 966 public void onPress(int primaryCode) { 967 mKeyboardActionListener.onPress(primaryCode); 968 } 969 public void onRelease(int primaryCode) { 970 mKeyboardActionListener.onRelease(primaryCode); 971 } 972 }); 973 //mInputView.setSuggest(mSuggest); 974 Keyboard keyboard; 975 if (popupKey.popupCharacters != null) { 976 keyboard = new Keyboard(getContext(), popupKeyboardId, 977 popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); 978 } else { 979 keyboard = new Keyboard(getContext(), popupKeyboardId); 980 } 981 mMiniKeyboard.setKeyboard(keyboard); 982 mMiniKeyboard.setPopupParent(this); 983 mMiniKeyboardContainer.measure( 984 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 985 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 986 987 mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); 988 } else { 989 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( 990 com.android.internal.R.id.keyboardView); 991 } 992 if (mWindowOffset == null) { 993 mWindowOffset = new int[2]; 994 getLocationInWindow(mWindowOffset); 995 } 996 mPopupX = popupKey.x + mPaddingLeft; 997 mPopupY = popupKey.y + mPaddingTop; 998 mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); 999 mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); 1000 final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; 1001 final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; 1002 mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); 1003 mMiniKeyboard.setShifted(isShifted()); 1004 mPopupKeyboard.setContentView(mMiniKeyboardContainer); 1005 mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); 1006 mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); 1007 mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1008 mMiniKeyboardOnScreen = true; 1009 //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); 1010 invalidateAllKeys(); 1011 return true; 1012 } 1013 return false; 1014 } 1015 1016 @Override 1017 public boolean onTouchEvent(MotionEvent me) { 1018 int touchX = (int) me.getX() - mPaddingLeft; 1019 int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop; 1020 int action = me.getAction(); 1021 long eventTime = me.getEventTime(); 1022 int keyIndex = getKeyIndices(touchX, touchY, null); 1023 1024 if (mGestureDetector.onTouchEvent(me)) { 1025 showPreview(NOT_A_KEY); 1026 mHandler.removeMessages(MSG_REPEAT); 1027 mHandler.removeMessages(MSG_LONGPRESS); 1028 return true; 1029 } 1030 1031 // Needs to be called after the gesture detector gets a turn, as it may have 1032 // displayed the mini keyboard 1033 if (mMiniKeyboardOnScreen) { 1034 return true; 1035 } 1036 1037 switch (action) { 1038 case MotionEvent.ACTION_DOWN: 1039 mAbortKey = false; 1040 mStartX = touchX; 1041 mStartY = touchY; 1042 mLastCodeX = touchX; 1043 mLastCodeY = touchY; 1044 mLastKeyTime = 0; 1045 mCurrentKeyTime = 0; 1046 mLastKey = NOT_A_KEY; 1047 mCurrentKey = keyIndex; 1048 mDownTime = me.getEventTime(); 1049 mLastMoveTime = mDownTime; 1050 checkMultiTap(eventTime, keyIndex); 1051 mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? 1052 mKeys[keyIndex].codes[0] : 0); 1053 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { 1054 mRepeatKeyIndex = mCurrentKey; 1055 repeatKey(); 1056 Message msg = mHandler.obtainMessage(MSG_REPEAT); 1057 mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); 1058 } 1059 if (mCurrentKey != NOT_A_KEY) { 1060 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1061 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1062 } 1063 showPreview(keyIndex); 1064 break; 1065 1066 case MotionEvent.ACTION_MOVE: 1067 boolean continueLongPress = false; 1068 if (keyIndex != NOT_A_KEY) { 1069 if (mCurrentKey == NOT_A_KEY) { 1070 mCurrentKey = keyIndex; 1071 mCurrentKeyTime = eventTime - mDownTime; 1072 } else { 1073 if (keyIndex == mCurrentKey) { 1074 mCurrentKeyTime += eventTime - mLastMoveTime; 1075 continueLongPress = true; 1076 } else { 1077 resetMultiTap(); 1078 mLastKey = mCurrentKey; 1079 mLastCodeX = mLastX; 1080 mLastCodeY = mLastY; 1081 mLastKeyTime = 1082 mCurrentKeyTime + eventTime - mLastMoveTime; 1083 mCurrentKey = keyIndex; 1084 mCurrentKeyTime = 0; 1085 } 1086 } 1087 if (keyIndex != mRepeatKeyIndex) { 1088 mHandler.removeMessages(MSG_REPEAT); 1089 mRepeatKeyIndex = NOT_A_KEY; 1090 } 1091 } 1092 if (!continueLongPress) { 1093 // Cancel old longpress 1094 mHandler.removeMessages(MSG_LONGPRESS); 1095 // Start new longpress if key has changed 1096 if (keyIndex != NOT_A_KEY) { 1097 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); 1098 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); 1099 } 1100 } 1101 showPreview(keyIndex); 1102 break; 1103 1104 case MotionEvent.ACTION_UP: 1105 mHandler.removeMessages(MSG_SHOW_PREVIEW); 1106 mHandler.removeMessages(MSG_REPEAT); 1107 mHandler.removeMessages(MSG_LONGPRESS); 1108 if (keyIndex == mCurrentKey) { 1109 mCurrentKeyTime += eventTime - mLastMoveTime; 1110 } else { 1111 resetMultiTap(); 1112 mLastKey = mCurrentKey; 1113 mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; 1114 mCurrentKey = keyIndex; 1115 mCurrentKeyTime = 0; 1116 } 1117 if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) { 1118 mCurrentKey = mLastKey; 1119 touchX = mLastCodeX; 1120 touchY = mLastCodeY; 1121 } 1122 showPreview(NOT_A_KEY); 1123 Arrays.fill(mKeyIndices, NOT_A_KEY); 1124 // If we're not on a repeating key (which sends on a DOWN event) 1125 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { 1126 detectAndSendKey(touchX, touchY, eventTime); 1127 } 1128 invalidateKey(keyIndex); 1129 mRepeatKeyIndex = NOT_A_KEY; 1130 break; 1131 } 1132 mLastX = touchX; 1133 mLastY = touchY; 1134 return true; 1135 } 1136 1137 private boolean repeatKey() { 1138 Key key = mKeys[mRepeatKeyIndex]; 1139 detectAndSendKey(key.x, key.y, mLastTapTime); 1140 return true; 1141 } 1142 1143 protected void swipeRight() { 1144 mKeyboardActionListener.swipeRight(); 1145 } 1146 1147 protected void swipeLeft() { 1148 mKeyboardActionListener.swipeLeft(); 1149 } 1150 1151 protected void swipeUp() { 1152 mKeyboardActionListener.swipeUp(); 1153 } 1154 1155 protected void swipeDown() { 1156 mKeyboardActionListener.swipeDown(); 1157 } 1158 1159 public void closing() { 1160 if (mPreviewPopup.isShowing()) { 1161 mPreviewPopup.dismiss(); 1162 } 1163 mHandler.removeMessages(MSG_REPEAT); 1164 mHandler.removeMessages(MSG_LONGPRESS); 1165 mHandler.removeMessages(MSG_SHOW_PREVIEW); 1166 1167 dismissPopupKeyboard(); 1168 mBuffer = null; 1169 mCanvas = null; 1170 mMiniKeyboardCache.clear(); 1171 } 1172 1173 @Override 1174 public void onDetachedFromWindow() { 1175 super.onDetachedFromWindow(); 1176 closing(); 1177 } 1178 1179 private void dismissPopupKeyboard() { 1180 if (mPopupKeyboard.isShowing()) { 1181 mPopupKeyboard.dismiss(); 1182 mMiniKeyboardOnScreen = false; 1183 invalidateAllKeys(); 1184 } 1185 } 1186 1187 public boolean handleBack() { 1188 if (mPopupKeyboard.isShowing()) { 1189 dismissPopupKeyboard(); 1190 return true; 1191 } 1192 return false; 1193 } 1194 1195 private void resetMultiTap() { 1196 mLastSentIndex = NOT_A_KEY; 1197 mTapCount = 0; 1198 mLastTapTime = -1; 1199 mInMultiTap = false; 1200 } 1201 1202 private void checkMultiTap(long eventTime, int keyIndex) { 1203 if (keyIndex == NOT_A_KEY) return; 1204 Key key = mKeys[keyIndex]; 1205 if (key.codes.length > 1) { 1206 mInMultiTap = true; 1207 if (eventTime < mLastTapTime + MULTITAP_INTERVAL 1208 && keyIndex == mLastSentIndex) { 1209 mTapCount = (mTapCount + 1) % key.codes.length; 1210 return; 1211 } else { 1212 mTapCount = -1; 1213 return; 1214 } 1215 } 1216 if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { 1217 resetMultiTap(); 1218 } 1219 } 1220} 1221