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