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