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