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