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