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