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