1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Align; 27 import android.graphics.PorterDuff; 28 import android.graphics.Rect; 29 import android.graphics.Region.Op; 30 import android.graphics.Typeface; 31 import android.graphics.drawable.Drawable; 32 import android.inputmethodservice.Keyboard; 33 import android.inputmethodservice.Keyboard.Key; 34 import android.os.Handler; 35 import android.os.Message; 36 import android.os.SystemClock; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.util.TypedValue; 40 import android.view.GestureDetector; 41 import android.view.Gravity; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.ViewGroup.LayoutParams; 46 import android.widget.PopupWindow; 47 import android.widget.TextView; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.LinkedList; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.WeakHashMap; 55 56 /** 57 * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and 58 * detecting key presses and touch movements. 59 * 60 * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class. 61 * 62 * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground 63 * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout 64 * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset 65 * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize 66 * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize 67 * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor 68 * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection 69 * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout 70 */ 71 public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy { 72 private static final String TAG = "LatinKeyboardBaseView"; 73 private static final boolean DEBUG = false; 74 75 public static final int NOT_A_TOUCH_COORDINATE = -1; 76 77 public interface OnKeyboardActionListener { 78 79 /** 80 * Called when the user presses a key. This is sent before the 81 * {@link #onKey} is called. For keys that repeat, this is only 82 * called once. 83 * 84 * @param primaryCode 85 * the unicode of the key being pressed. If the touch is 86 * not on a valid key, the value will be zero. 87 */ 88 void onPress(int primaryCode); 89 90 /** 91 * Called when the user releases a key. This is sent after the 92 * {@link #onKey} is called. For keys that repeat, this is only 93 * called once. 94 * 95 * @param primaryCode 96 * the code of the key that was released 97 */ 98 void onRelease(int primaryCode); 99 100 /** 101 * Send a key press to the listener. 102 * 103 * @param primaryCode 104 * this is the key that was pressed 105 * @param keyCodes 106 * the codes for all the possible alternative keys with 107 * the primary code being the first. If the primary key 108 * code is a single character such as an alphabet or 109 * number or symbol, the alternatives will include other 110 * characters that may be on the same key or adjacent 111 * keys. These codes are useful to correct for 112 * accidental presses of a key adjacent to the intended 113 * key. 114 * @param x 115 * x-coordinate pixel of touched event. If onKey is not called by onTouchEvent, 116 * the value should be NOT_A_TOUCH_COORDINATE. 117 * @param y 118 * y-coordinate pixel of touched event. If onKey is not called by onTouchEvent, 119 * the value should be NOT_A_TOUCH_COORDINATE. 120 */ 121 void onKey(int primaryCode, int[] keyCodes, int x, int y); 122 123 /** 124 * Sends a sequence of characters to the listener. 125 * 126 * @param text 127 * the sequence of characters to be displayed. 128 */ 129 void onText(CharSequence text); 130 131 /** 132 * Called when user released a finger outside any key. 133 */ 134 void onCancel(); 135 136 /** 137 * Called when the user quickly moves the finger from right to 138 * left. 139 */ 140 void swipeLeft(); 141 142 /** 143 * Called when the user quickly moves the finger from left to 144 * right. 145 */ 146 void swipeRight(); 147 148 /** 149 * Called when the user quickly moves the finger from up to down. 150 */ 151 void swipeDown(); 152 153 /** 154 * Called when the user quickly moves the finger from down to up. 155 */ 156 void swipeUp(); 157 } 158 159 // Timing constants 160 private final int mKeyRepeatInterval; 161 162 // Miscellaneous constants 163 /* package */ static final int NOT_A_KEY = -1; 164 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 165 private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1; 166 167 // XML attribute 168 private int mKeyTextSize; 169 private int mKeyTextColor; 170 private Typeface mKeyTextStyle = Typeface.DEFAULT; 171 private int mLabelTextSize; 172 private int mSymbolColorScheme = 0; 173 private int mShadowColor; 174 private float mShadowRadius; 175 private Drawable mKeyBackground; 176 private float mBackgroundDimAmount; 177 private float mKeyHysteresisDistance; 178 private float mVerticalCorrection; 179 private int mPreviewOffset; 180 private int mPreviewHeight; 181 private int mPopupLayout; 182 183 // Main keyboard 184 private Keyboard mKeyboard; 185 private Key[] mKeys; 186 // TODO this attribute should be gotten from Keyboard. 187 private int mKeyboardVerticalGap; 188 189 // Key preview popup 190 private TextView mPreviewText; 191 private PopupWindow mPreviewPopup; 192 private int mPreviewTextSizeLarge; 193 private int[] mOffsetInWindow; 194 private int mOldPreviewKeyIndex = NOT_A_KEY; 195 private boolean mShowPreview = true; 196 private boolean mShowTouchPoints = true; 197 private int mPopupPreviewOffsetX; 198 private int mPopupPreviewOffsetY; 199 private int mWindowY; 200 private int mPopupPreviewDisplayedY; 201 private final int mDelayBeforePreview; 202 private final int mDelayAfterPreview; 203 204 // Popup mini keyboard 205 private PopupWindow mMiniKeyboardPopup; 206 private LatinKeyboardBaseView mMiniKeyboard; 207 private View mMiniKeyboardParent; 208 private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>(); 209 private int mMiniKeyboardOriginX; 210 private int mMiniKeyboardOriginY; 211 private long mMiniKeyboardPopupTime; 212 private int[] mWindowOffset; 213 private final float mMiniKeyboardSlideAllowance; 214 private int mMiniKeyboardTrackerId; 215 216 /** Listener for {@link OnKeyboardActionListener}. */ 217 private OnKeyboardActionListener mKeyboardActionListener; 218 219 private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); 220 221 // TODO: Let the PointerTracker class manage this pointer queue 222 private final PointerQueue mPointerQueue = new PointerQueue(); 223 224 private final boolean mHasDistinctMultitouch; 225 private int mOldPointerCount = 1; 226 227 protected KeyDetector mKeyDetector = new ProximityKeyDetector(); 228 229 // Swipe gesture detector 230 private GestureDetector mGestureDetector; 231 private final SwipeTracker mSwipeTracker = new SwipeTracker(); 232 private final int mSwipeThreshold; 233 private final boolean mDisambiguateSwipe; 234 235 // Drawing 236 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 237 private boolean mDrawPending; 238 /** The dirty region in the keyboard bitmap */ 239 private final Rect mDirtyRect = new Rect(); 240 /** The keyboard bitmap for faster updates */ 241 private Bitmap mBuffer; 242 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ 243 private boolean mKeyboardChanged; 244 private Key mInvalidatedKey; 245 /** The canvas for the above mutable keyboard bitmap */ 246 private Canvas mCanvas; 247 private final Paint mPaint; 248 private final Rect mPadding; 249 private final Rect mClipRegion = new Rect(0, 0, 0, 0); 250 // This map caches key label text height in pixel as value and key label text size as map key. 251 private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>(); 252 // Distance from horizontal center of the key, proportional to key label text height. 253 private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f; 254 private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H"; 255 256 private final UIHandler mHandler = new UIHandler(); 257 258 class UIHandler extends Handler { 259 private static final int MSG_POPUP_PREVIEW = 1; 260 private static final int MSG_DISMISS_PREVIEW = 2; 261 private static final int MSG_REPEAT_KEY = 3; 262 private static final int MSG_LONGPRESS_KEY = 4; 263 264 private boolean mInKeyRepeat; 265 266 @Override 267 public void handleMessage(Message msg) { 268 switch (msg.what) { 269 case MSG_POPUP_PREVIEW: 270 showKey(msg.arg1, (PointerTracker)msg.obj); 271 break; 272 case MSG_DISMISS_PREVIEW: 273 mPreviewPopup.dismiss(); 274 break; 275 case MSG_REPEAT_KEY: { 276 final PointerTracker tracker = (PointerTracker)msg.obj; 277 tracker.repeatKey(msg.arg1); 278 startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker); 279 break; 280 } 281 case MSG_LONGPRESS_KEY: { 282 final PointerTracker tracker = (PointerTracker)msg.obj; 283 openPopupIfRequired(msg.arg1, tracker); 284 break; 285 } 286 } 287 } 288 289 public void popupPreview(long delay, int keyIndex, PointerTracker tracker) { 290 removeMessages(MSG_POPUP_PREVIEW); 291 if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { 292 // Show right away, if it's already visible and finger is moving around 293 showKey(keyIndex, tracker); 294 } else { 295 sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker), 296 delay); 297 } 298 } 299 300 public void cancelPopupPreview() { 301 removeMessages(MSG_POPUP_PREVIEW); 302 } 303 304 public void dismissPreview(long delay) { 305 if (mPreviewPopup.isShowing()) { 306 sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay); 307 } 308 } 309 310 public void cancelDismissPreview() { 311 removeMessages(MSG_DISMISS_PREVIEW); 312 } 313 314 public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { 315 mInKeyRepeat = true; 316 sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); 317 } 318 319 public void cancelKeyRepeatTimer() { 320 mInKeyRepeat = false; 321 removeMessages(MSG_REPEAT_KEY); 322 } 323 324 public boolean isInKeyRepeat() { 325 return mInKeyRepeat; 326 } 327 328 public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { 329 removeMessages(MSG_LONGPRESS_KEY); 330 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); 331 } 332 333 public void cancelLongPressTimer() { 334 removeMessages(MSG_LONGPRESS_KEY); 335 } 336 337 public void cancelKeyTimers() { 338 cancelKeyRepeatTimer(); 339 cancelLongPressTimer(); 340 } 341 342 public void cancelAllMessages() { 343 cancelKeyTimers(); 344 cancelPopupPreview(); 345 cancelDismissPreview(); 346 } 347 } 348 349 static class PointerQueue { 350 private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>(); 351 352 public void add(PointerTracker tracker) { 353 mQueue.add(tracker); 354 } 355 356 public int lastIndexOf(PointerTracker tracker) { 357 LinkedList<PointerTracker> queue = mQueue; 358 for (int index = queue.size() - 1; index >= 0; index--) { 359 PointerTracker t = queue.get(index); 360 if (t == tracker) 361 return index; 362 } 363 return -1; 364 } 365 366 public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) { 367 LinkedList<PointerTracker> queue = mQueue; 368 int oldestPos = 0; 369 for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) { 370 if (t.isModifier()) { 371 oldestPos++; 372 } else { 373 t.onUpEvent(t.getLastX(), t.getLastY(), eventTime); 374 t.setAlreadyProcessed(); 375 queue.remove(oldestPos); 376 } 377 } 378 } 379 380 public void releaseAllPointersExcept(PointerTracker tracker, long eventTime) { 381 for (PointerTracker t : mQueue) { 382 if (t == tracker) 383 continue; 384 t.onUpEvent(t.getLastX(), t.getLastY(), eventTime); 385 t.setAlreadyProcessed(); 386 } 387 mQueue.clear(); 388 if (tracker != null) 389 mQueue.add(tracker); 390 } 391 392 public void remove(PointerTracker tracker) { 393 mQueue.remove(tracker); 394 } 395 396 public boolean isInSlidingKeyInput() { 397 for (final PointerTracker tracker : mQueue) { 398 if (tracker.isInSlidingKeyInput()) 399 return true; 400 } 401 return false; 402 } 403 } 404 405 public LatinKeyboardBaseView(Context context, AttributeSet attrs) { 406 this(context, attrs, R.attr.keyboardViewStyle); 407 } 408 409 public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) { 410 super(context, attrs, defStyle); 411 412 TypedArray a = context.obtainStyledAttributes( 413 attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView); 414 LayoutInflater inflate = 415 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 416 int previewLayout = 0; 417 int keyTextSize = 0; 418 419 int n = a.getIndexCount(); 420 421 for (int i = 0; i < n; i++) { 422 int attr = a.getIndex(i); 423 424 switch (attr) { 425 case R.styleable.LatinKeyboardBaseView_keyBackground: 426 mKeyBackground = a.getDrawable(attr); 427 break; 428 case R.styleable.LatinKeyboardBaseView_keyHysteresisDistance: 429 mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0); 430 break; 431 case R.styleable.LatinKeyboardBaseView_verticalCorrection: 432 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); 433 break; 434 case R.styleable.LatinKeyboardBaseView_keyPreviewLayout: 435 previewLayout = a.getResourceId(attr, 0); 436 break; 437 case R.styleable.LatinKeyboardBaseView_keyPreviewOffset: 438 mPreviewOffset = a.getDimensionPixelOffset(attr, 0); 439 break; 440 case R.styleable.LatinKeyboardBaseView_keyPreviewHeight: 441 mPreviewHeight = a.getDimensionPixelSize(attr, 80); 442 break; 443 case R.styleable.LatinKeyboardBaseView_keyTextSize: 444 mKeyTextSize = a.getDimensionPixelSize(attr, 18); 445 break; 446 case R.styleable.LatinKeyboardBaseView_keyTextColor: 447 mKeyTextColor = a.getColor(attr, 0xFF000000); 448 break; 449 case R.styleable.LatinKeyboardBaseView_labelTextSize: 450 mLabelTextSize = a.getDimensionPixelSize(attr, 14); 451 break; 452 case R.styleable.LatinKeyboardBaseView_popupLayout: 453 mPopupLayout = a.getResourceId(attr, 0); 454 break; 455 case R.styleable.LatinKeyboardBaseView_shadowColor: 456 mShadowColor = a.getColor(attr, 0); 457 break; 458 case R.styleable.LatinKeyboardBaseView_shadowRadius: 459 mShadowRadius = a.getFloat(attr, 0f); 460 break; 461 // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) 462 case R.styleable.LatinKeyboardBaseView_backgroundDimAmount: 463 mBackgroundDimAmount = a.getFloat(attr, 0.5f); 464 break; 465 //case android.R.styleable. 466 case R.styleable.LatinKeyboardBaseView_keyTextStyle: 467 int textStyle = a.getInt(attr, 0); 468 switch (textStyle) { 469 case 0: 470 mKeyTextStyle = Typeface.DEFAULT; 471 break; 472 case 1: 473 mKeyTextStyle = Typeface.DEFAULT_BOLD; 474 break; 475 default: 476 mKeyTextStyle = Typeface.defaultFromStyle(textStyle); 477 break; 478 } 479 break; 480 case R.styleable.LatinKeyboardBaseView_symbolColorScheme: 481 mSymbolColorScheme = a.getInt(attr, 0); 482 break; 483 } 484 } 485 486 final Resources res = getResources(); 487 488 mPreviewPopup = new PopupWindow(context); 489 if (previewLayout != 0) { 490 mPreviewText = (TextView) inflate.inflate(previewLayout, null); 491 mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large); 492 mPreviewPopup.setContentView(mPreviewText); 493 mPreviewPopup.setBackgroundDrawable(null); 494 } else { 495 mShowPreview = false; 496 } 497 mPreviewPopup.setTouchable(false); 498 mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation); 499 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 500 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 501 502 mMiniKeyboardParent = this; 503 mMiniKeyboardPopup = new PopupWindow(context); 504 mMiniKeyboardPopup.setBackgroundDrawable(null); 505 mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation); 506 507 mPaint = new Paint(); 508 mPaint.setAntiAlias(true); 509 mPaint.setTextSize(keyTextSize); 510 mPaint.setTextAlign(Align.CENTER); 511 mPaint.setAlpha(255); 512 513 mPadding = new Rect(0, 0, 0, 0); 514 mKeyBackground.getPadding(mPadding); 515 516 mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); 517 // TODO: Refer frameworks/base/core/res/res/values/config.xml 518 mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); 519 mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance); 520 521 GestureDetector.SimpleOnGestureListener listener = 522 new GestureDetector.SimpleOnGestureListener() { 523 @Override 524 public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, 525 float velocityY) { 526 final float absX = Math.abs(velocityX); 527 final float absY = Math.abs(velocityY); 528 float deltaX = me2.getX() - me1.getX(); 529 float deltaY = me2.getY() - me1.getY(); 530 int travelX = getWidth() / 2; // Half the keyboard width 531 int travelY = getHeight() / 2; // Half the keyboard height 532 mSwipeTracker.computeCurrentVelocity(1000); 533 final float endingVelocityX = mSwipeTracker.getXVelocity(); 534 final float endingVelocityY = mSwipeTracker.getYVelocity(); 535 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { 536 if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) { 537 swipeRight(); 538 return true; 539 } 540 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { 541 if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) { 542 swipeLeft(); 543 return true; 544 } 545 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { 546 if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) { 547 swipeUp(); 548 return true; 549 } 550 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 551 if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { 552 swipeDown(); 553 return true; 554 } 555 } 556 return false; 557 } 558 }; 559 560 final boolean ignoreMultitouch = true; 561 mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); 562 mGestureDetector.setIsLongpressEnabled(false); 563 564 mHasDistinctMultitouch = context.getPackageManager() 565 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 566 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 567 } 568 569 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { 570 mKeyboardActionListener = listener; 571 for (PointerTracker tracker : mPointerTrackers) { 572 tracker.setOnKeyboardActionListener(listener); 573 } 574 } 575 576 /** 577 * Returns the {@link OnKeyboardActionListener} object. 578 * @return the listener attached to this keyboard 579 */ 580 protected OnKeyboardActionListener getOnKeyboardActionListener() { 581 return mKeyboardActionListener; 582 } 583 584 /** 585 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 586 * view will re-layout itself to accommodate the keyboard. 587 * @see Keyboard 588 * @see #getKeyboard() 589 * @param keyboard the keyboard to display in this view 590 */ 591 public void setKeyboard(Keyboard keyboard) { 592 if (mKeyboard != null) { 593 dismissKeyPreview(); 594 } 595 // Remove any pending messages, except dismissing preview 596 mHandler.cancelKeyTimers(); 597 mHandler.cancelPopupPreview(); 598 mKeyboard = keyboard; 599 LatinImeLogger.onSetKeyboard(keyboard); 600 mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), 601 -getPaddingTop() + mVerticalCorrection); 602 mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap); 603 for (PointerTracker tracker : mPointerTrackers) { 604 tracker.setKeyboard(mKeys, mKeyHysteresisDistance); 605 } 606 requestLayout(); 607 // Hint to reallocate the buffer if the size changed 608 mKeyboardChanged = true; 609 invalidateAllKeys(); 610 computeProximityThreshold(keyboard); 611 mMiniKeyboardCache.clear(); 612 } 613 614 /** 615 * Returns the current keyboard being displayed by this view. 616 * @return the currently attached keyboard 617 * @see #setKeyboard(Keyboard) 618 */ 619 public Keyboard getKeyboard() { 620 return mKeyboard; 621 } 622 623 /** 624 * Return whether the device has distinct multi-touch panel. 625 * @return true if the device has distinct multi-touch panel. 626 */ 627 public boolean hasDistinctMultitouch() { 628 return mHasDistinctMultitouch; 629 } 630 631 /** 632 * Sets the state of the shift key of the keyboard, if any. 633 * @param shifted whether or not to enable the state of the shift key 634 * @return true if the shift key state changed, false if there was no change 635 */ 636 public boolean setShifted(boolean shifted) { 637 if (mKeyboard != null) { 638 if (mKeyboard.setShifted(shifted)) { 639 // The whole keyboard probably needs to be redrawn 640 invalidateAllKeys(); 641 return true; 642 } 643 } 644 return false; 645 } 646 647 /** 648 * Returns the state of the shift key of the keyboard, if any. 649 * @return true if the shift is in a pressed state, false otherwise. If there is 650 * no shift key on the keyboard or there is no keyboard attached, it returns false. 651 */ 652 public boolean isShifted() { 653 if (mKeyboard != null) { 654 return mKeyboard.isShifted(); 655 } 656 return false; 657 } 658 659 /** 660 * Enables or disables the key feedback popup. This is a popup that shows a magnified 661 * version of the depressed key. By default the preview is enabled. 662 * @param previewEnabled whether or not to enable the key feedback popup 663 * @see #isPreviewEnabled() 664 */ 665 public void setPreviewEnabled(boolean previewEnabled) { 666 mShowPreview = previewEnabled; 667 } 668 669 /** 670 * Returns the enabled state of the key feedback popup. 671 * @return whether or not the key feedback popup is enabled 672 * @see #setPreviewEnabled(boolean) 673 */ 674 public boolean isPreviewEnabled() { 675 return mShowPreview; 676 } 677 678 public int getSymbolColorScheme() { 679 return mSymbolColorScheme; 680 } 681 682 public void setPopupParent(View v) { 683 mMiniKeyboardParent = v; 684 } 685 686 public void setPopupOffset(int x, int y) { 687 mPopupPreviewOffsetX = x; 688 mPopupPreviewOffsetY = y; 689 mPreviewPopup.dismiss(); 690 } 691 692 /** 693 * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key 694 * codes for adjacent keys. When disabled, only the primary key code will be 695 * reported. 696 * @param enabled whether or not the proximity correction is enabled 697 */ 698 public void setProximityCorrectionEnabled(boolean enabled) { 699 mKeyDetector.setProximityCorrectionEnabled(enabled); 700 } 701 702 /** 703 * Returns true if proximity correction is enabled. 704 */ 705 public boolean isProximityCorrectionEnabled() { 706 return mKeyDetector.isProximityCorrectionEnabled(); 707 } 708 709 protected Locale getKeyboardLocale() { 710 if (mKeyboard instanceof LatinKeyboard) { 711 return ((LatinKeyboard)mKeyboard).getInputLocale(); 712 } else { 713 return getContext().getResources().getConfiguration().locale; 714 } 715 } 716 717 protected CharSequence adjustCase(CharSequence label) { 718 if (mKeyboard.isShifted() && label != null && label.length() < 3 719 && Character.isLowerCase(label.charAt(0))) { 720 return label.toString().toUpperCase(getKeyboardLocale()); 721 } 722 return label; 723 } 724 725 @Override 726 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 727 // Round up a little 728 if (mKeyboard == null) { 729 setMeasuredDimension( 730 getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); 731 } else { 732 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 733 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 734 width = MeasureSpec.getSize(widthMeasureSpec); 735 } 736 setMeasuredDimension( 737 width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 738 } 739 } 740 741 /** 742 * Compute the average distance between adjacent keys (horizontally and vertically) 743 * and square it to get the proximity threshold. We use a square here and in computing 744 * the touch distance from a key's center to avoid taking a square root. 745 * @param keyboard 746 */ 747 private void computeProximityThreshold(Keyboard keyboard) { 748 if (keyboard == null) return; 749 final Key[] keys = mKeys; 750 if (keys == null) return; 751 int length = keys.length; 752 int dimensionSum = 0; 753 for (int i = 0; i < length; i++) { 754 Key key = keys[i]; 755 dimensionSum += Math.min(key.width, key.height + mKeyboardVerticalGap) + key.gap; 756 } 757 if (dimensionSum < 0 || length == 0) return; 758 mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length)); 759 } 760 761 @Override 762 public void onSizeChanged(int w, int h, int oldw, int oldh) { 763 super.onSizeChanged(w, h, oldw, oldh); 764 // Release the buffer, if any and it will be reallocated on the next draw 765 mBuffer = null; 766 } 767 768 @Override 769 public void onDraw(Canvas canvas) { 770 super.onDraw(canvas); 771 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 772 onBufferDraw(); 773 } 774 canvas.drawBitmap(mBuffer, 0, 0, null); 775 } 776 777 private void onBufferDraw() { 778 if (mBuffer == null || mKeyboardChanged) { 779 if (mBuffer == null || mKeyboardChanged && 780 (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { 781 // Make sure our bitmap is at least 1x1 782 final int width = Math.max(1, getWidth()); 783 final int height = Math.max(1, getHeight()); 784 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 785 mCanvas = new Canvas(mBuffer); 786 } 787 invalidateAllKeys(); 788 mKeyboardChanged = false; 789 } 790 final Canvas canvas = mCanvas; 791 canvas.clipRect(mDirtyRect, Op.REPLACE); 792 793 if (mKeyboard == null) return; 794 795 final Paint paint = mPaint; 796 final Drawable keyBackground = mKeyBackground; 797 final Rect clipRegion = mClipRegion; 798 final Rect padding = mPadding; 799 final int kbdPaddingLeft = getPaddingLeft(); 800 final int kbdPaddingTop = getPaddingTop(); 801 final Key[] keys = mKeys; 802 final Key invalidKey = mInvalidatedKey; 803 804 paint.setColor(mKeyTextColor); 805 boolean drawSingleKey = false; 806 if (invalidKey != null && canvas.getClipBounds(clipRegion)) { 807 // TODO we should use Rect.inset and Rect.contains here. 808 // Is clipRegion completely contained within the invalidated key? 809 if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && 810 invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && 811 invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && 812 invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { 813 drawSingleKey = true; 814 } 815 } 816 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 817 final int keyCount = keys.length; 818 for (int i = 0; i < keyCount; i++) { 819 final Key key = keys[i]; 820 if (drawSingleKey && invalidKey != key) { 821 continue; 822 } 823 int[] drawableState = key.getCurrentDrawableState(); 824 keyBackground.setState(drawableState); 825 826 // Switch the character to uppercase if shift is pressed 827 String label = key.label == null? null : adjustCase(key.label).toString(); 828 829 final Rect bounds = keyBackground.getBounds(); 830 if (key.width != bounds.right || key.height != bounds.bottom) { 831 keyBackground.setBounds(0, 0, key.width, key.height); 832 } 833 canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); 834 keyBackground.draw(canvas); 835 836 boolean shouldDrawIcon = true; 837 if (label != null) { 838 // For characters, use large font. For labels like "Done", use small font. 839 final int labelSize; 840 if (label.length() > 1 && key.codes.length < 2) { 841 labelSize = mLabelTextSize; 842 paint.setTypeface(Typeface.DEFAULT_BOLD); 843 } else { 844 labelSize = mKeyTextSize; 845 paint.setTypeface(mKeyTextStyle); 846 } 847 paint.setTextSize(labelSize); 848 849 Integer labelHeightValue = mTextHeightCache.get(labelSize); 850 final int labelHeight; 851 if (labelHeightValue != null) { 852 labelHeight = labelHeightValue; 853 } else { 854 Rect textBounds = new Rect(); 855 paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds); 856 labelHeight = textBounds.height(); 857 mTextHeightCache.put(labelSize, labelHeight); 858 } 859 860 // Draw a drop shadow for the text 861 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 862 final int centerX = (key.width + padding.left - padding.right) / 2; 863 final int centerY = (key.height + padding.top - padding.bottom) / 2; 864 final float baseline = centerY 865 + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR; 866 canvas.drawText(label, centerX, baseline, paint); 867 // Turn off drop shadow 868 paint.setShadowLayer(0, 0, 0, 0); 869 870 // Usually don't draw icon if label is not null, but we draw icon for the number 871 // hint and popup hint. 872 shouldDrawIcon = shouldDrawLabelAndIcon(key); 873 } 874 if (key.icon != null && shouldDrawIcon) { 875 // Special handing for the upper-right number hint icons 876 final int drawableWidth; 877 final int drawableHeight; 878 final int drawableX; 879 final int drawableY; 880 if (shouldDrawIconFully(key)) { 881 drawableWidth = key.width; 882 drawableHeight = key.height; 883 drawableX = 0; 884 drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL; 885 } else { 886 drawableWidth = key.icon.getIntrinsicWidth(); 887 drawableHeight = key.icon.getIntrinsicHeight(); 888 drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2; 889 drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2; 890 } 891 canvas.translate(drawableX, drawableY); 892 key.icon.setBounds(0, 0, drawableWidth, drawableHeight); 893 key.icon.draw(canvas); 894 canvas.translate(-drawableX, -drawableY); 895 } 896 canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); 897 } 898 mInvalidatedKey = null; 899 // Overlay a dark rectangle to dim the keyboard 900 if (mMiniKeyboard != null) { 901 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 902 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 903 } 904 905 if (DEBUG) { 906 if (mShowTouchPoints) { 907 for (PointerTracker tracker : mPointerTrackers) { 908 int startX = tracker.getStartX(); 909 int startY = tracker.getStartY(); 910 int lastX = tracker.getLastX(); 911 int lastY = tracker.getLastY(); 912 paint.setAlpha(128); 913 paint.setColor(0xFFFF0000); 914 canvas.drawCircle(startX, startY, 3, paint); 915 canvas.drawLine(startX, startY, lastX, lastY, paint); 916 paint.setColor(0xFF0000FF); 917 canvas.drawCircle(lastX, lastY, 3, paint); 918 paint.setColor(0xFF00FF00); 919 canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint); 920 } 921 } 922 } 923 924 mDrawPending = false; 925 mDirtyRect.setEmpty(); 926 } 927 928 // TODO: clean up this method. 929 private void dismissKeyPreview() { 930 for (PointerTracker tracker : mPointerTrackers) 931 tracker.updateKey(NOT_A_KEY); 932 showPreview(NOT_A_KEY, null); 933 } 934 935 public void showPreview(int keyIndex, PointerTracker tracker) { 936 int oldKeyIndex = mOldPreviewKeyIndex; 937 mOldPreviewKeyIndex = keyIndex; 938 final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard) 939 && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled(); 940 // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show 941 // the space key preview and 3) pointer moves off the space key to other letter key, we 942 // should hide the preview of the previous key. 943 final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) 944 || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex); 945 // If key changed and preview is on or the key is space (language switch is enabled) 946 if (oldKeyIndex != keyIndex 947 && (mShowPreview 948 || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) { 949 if (keyIndex == NOT_A_KEY) { 950 mHandler.cancelPopupPreview(); 951 mHandler.dismissPreview(mDelayAfterPreview); 952 } else if (tracker != null) { 953 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker); 954 } 955 } 956 } 957 958 private void showKey(final int keyIndex, PointerTracker tracker) { 959 Key key = tracker.getKey(keyIndex); 960 if (key == null) 961 return; 962 // Should not draw hint icon in key preview 963 if (key.icon != null && !shouldDrawLabelAndIcon(key)) { 964 mPreviewText.setCompoundDrawables(null, null, null, 965 key.iconPreview != null ? key.iconPreview : key.icon); 966 mPreviewText.setText(null); 967 } else { 968 mPreviewText.setCompoundDrawables(null, null, null, null); 969 mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); 970 if (key.label.length() > 1 && key.codes.length < 2) { 971 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); 972 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 973 } else { 974 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 975 mPreviewText.setTypeface(mKeyTextStyle); 976 } 977 } 978 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 979 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 980 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width 981 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 982 final int popupHeight = mPreviewHeight; 983 LayoutParams lp = mPreviewText.getLayoutParams(); 984 if (lp != null) { 985 lp.width = popupWidth; 986 lp.height = popupHeight; 987 } 988 989 int popupPreviewX = key.x - (popupWidth - key.width) / 2; 990 int popupPreviewY = key.y - popupHeight + mPreviewOffset; 991 992 mHandler.cancelDismissPreview(); 993 if (mOffsetInWindow == null) { 994 mOffsetInWindow = new int[2]; 995 getLocationInWindow(mOffsetInWindow); 996 mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero 997 mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero 998 int[] windowLocation = new int[2]; 999 getLocationOnScreen(windowLocation); 1000 mWindowY = windowLocation[1]; 1001 } 1002 // Set the preview background state 1003 mPreviewText.getBackground().setState( 1004 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 1005 popupPreviewX += mOffsetInWindow[0]; 1006 popupPreviewY += mOffsetInWindow[1]; 1007 1008 // If the popup cannot be shown above the key, put it on the side 1009 if (popupPreviewY + mWindowY < 0) { 1010 // If the key you're pressing is on the left side of the keyboard, show the popup on 1011 // the right, offset by enough to see at least one key to the left/right. 1012 if (key.x + key.width <= getWidth() / 2) { 1013 popupPreviewX += (int) (key.width * 2.5); 1014 } else { 1015 popupPreviewX -= (int) (key.width * 2.5); 1016 } 1017 popupPreviewY += popupHeight; 1018 } 1019 1020 if (mPreviewPopup.isShowing()) { 1021 mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); 1022 } else { 1023 mPreviewPopup.setWidth(popupWidth); 1024 mPreviewPopup.setHeight(popupHeight); 1025 mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, 1026 popupPreviewX, popupPreviewY); 1027 } 1028 // Record popup preview position to display mini-keyboard later at the same positon 1029 mPopupPreviewDisplayedY = popupPreviewY; 1030 mPreviewText.setVisibility(VISIBLE); 1031 } 1032 1033 /** 1034 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 1035 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 1036 * draws the cached buffer. 1037 * @see #invalidateKey(Key) 1038 */ 1039 public void invalidateAllKeys() { 1040 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1041 mDrawPending = true; 1042 invalidate(); 1043 } 1044 1045 /** 1046 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 1047 * one key is changing it's content. Any changes that affect the position or size of the key 1048 * may not be honored. 1049 * @param key key in the attached {@link Keyboard}. 1050 * @see #invalidateAllKeys 1051 */ 1052 public void invalidateKey(Key key) { 1053 if (key == null) 1054 return; 1055 mInvalidatedKey = key; 1056 // TODO we should clean up this and record key's region to use in onBufferDraw. 1057 mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(), 1058 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); 1059 onBufferDraw(); 1060 invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(), 1061 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); 1062 } 1063 1064 private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) { 1065 // Check if we have a popup layout specified first. 1066 if (mPopupLayout == 0) { 1067 return false; 1068 } 1069 1070 Key popupKey = tracker.getKey(keyIndex); 1071 if (popupKey == null) 1072 return false; 1073 boolean result = onLongPress(popupKey); 1074 if (result) { 1075 dismissKeyPreview(); 1076 mMiniKeyboardTrackerId = tracker.mPointerId; 1077 // Mark this tracker "already processed" and remove it from the pointer queue 1078 tracker.setAlreadyProcessed(); 1079 mPointerQueue.remove(tracker); 1080 } 1081 return result; 1082 } 1083 1084 private View inflateMiniKeyboardContainer(Key popupKey) { 1085 int popupKeyboardId = popupKey.popupResId; 1086 LayoutInflater inflater = (LayoutInflater)getContext().getSystemService( 1087 Context.LAYOUT_INFLATER_SERVICE); 1088 View container = inflater.inflate(mPopupLayout, null); 1089 if (container == null) 1090 throw new NullPointerException(); 1091 1092 LatinKeyboardBaseView miniKeyboard = 1093 (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); 1094 miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { 1095 public void onKey(int primaryCode, int[] keyCodes, int x, int y) { 1096 mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y); 1097 dismissPopupKeyboard(); 1098 } 1099 1100 public void onText(CharSequence text) { 1101 mKeyboardActionListener.onText(text); 1102 dismissPopupKeyboard(); 1103 } 1104 1105 public void onCancel() { 1106 mKeyboardActionListener.onCancel(); 1107 dismissPopupKeyboard(); 1108 } 1109 1110 public void swipeLeft() { 1111 } 1112 public void swipeRight() { 1113 } 1114 public void swipeUp() { 1115 } 1116 public void swipeDown() { 1117 } 1118 public void onPress(int primaryCode) { 1119 mKeyboardActionListener.onPress(primaryCode); 1120 } 1121 public void onRelease(int primaryCode) { 1122 mKeyboardActionListener.onRelease(primaryCode); 1123 } 1124 }); 1125 // Override default ProximityKeyDetector. 1126 miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance); 1127 // Remove gesture detector on mini-keyboard 1128 miniKeyboard.mGestureDetector = null; 1129 1130 Keyboard keyboard; 1131 if (popupKey.popupCharacters != null) { 1132 keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters, 1133 -1, getPaddingLeft() + getPaddingRight()); 1134 } else { 1135 keyboard = new Keyboard(getContext(), popupKeyboardId); 1136 } 1137 miniKeyboard.setKeyboard(keyboard); 1138 miniKeyboard.setPopupParent(this); 1139 1140 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1141 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1142 1143 return container; 1144 } 1145 1146 private static boolean isOneRowKeys(List<Key> keys) { 1147 if (keys.size() == 0) return false; 1148 final int edgeFlags = keys.get(0).edgeFlags; 1149 // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows, 1150 // does not have both top and bottom edge flags on at the same time. On the other hand, 1151 // the first key of mini keyboard that was created with popupCharacters must have both top 1152 // and bottom edge flags on. 1153 // When you want to use one row mini-keyboard from xml file, make sure that the row has 1154 // both top and bottom edge flags set. 1155 return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; 1156 } 1157 1158 /** 1159 * Called when a key is long pressed. By default this will open any popup keyboard associated 1160 * with this key through the attributes popupLayout and popupCharacters. 1161 * @param popupKey the key that was long pressed 1162 * @return true if the long press is handled, false otherwise. Subclasses should call the 1163 * method on the base class if the subclass doesn't wish to handle the call. 1164 */ 1165 protected boolean onLongPress(Key popupKey) { 1166 // TODO if popupKey.popupCharacters has only one letter, send it as key without opening 1167 // mini keyboard. 1168 1169 if (popupKey.popupResId == 0) 1170 return false; 1171 1172 View container = mMiniKeyboardCache.get(popupKey); 1173 if (container == null) { 1174 container = inflateMiniKeyboardContainer(popupKey); 1175 mMiniKeyboardCache.put(popupKey, container); 1176 } 1177 mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); 1178 if (mWindowOffset == null) { 1179 mWindowOffset = new int[2]; 1180 getLocationInWindow(mWindowOffset); 1181 } 1182 1183 // Get width of a key in the mini popup keyboard = "miniKeyWidth". 1184 // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard. 1185 // We adjust the position of mini popup keyboard with the edge key in it: 1186 // a) When we have the leftmost key in popup keyboard directly above the pressed key 1187 // Right edges of both keys should be aligned for consistent default selection 1188 // b) When we have the rightmost key in popup keyboard directly above the pressed key 1189 // Left edges of both keys should be aligned for consistent default selection 1190 final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys(); 1191 final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).width : 0; 1192 1193 // HACK: Have the leftmost number in the popup characters right above the key 1194 boolean isNumberAtLeftmost = 1195 hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey); 1196 int popupX = popupKey.x + mWindowOffset[0]; 1197 popupX += getPaddingLeft(); 1198 if (isNumberAtLeftmost) { 1199 popupX += popupKey.width - miniKeyWidth; // adjustment for a) described above 1200 popupX -= container.getPaddingLeft(); 1201 } else { 1202 popupX += miniKeyWidth; // adjustment for b) described above 1203 popupX -= container.getMeasuredWidth(); 1204 popupX += container.getPaddingRight(); 1205 } 1206 int popupY = popupKey.y + mWindowOffset[1]; 1207 popupY += getPaddingTop(); 1208 popupY -= container.getMeasuredHeight(); 1209 popupY += container.getPaddingBottom(); 1210 final int x = popupX; 1211 final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY; 1212 1213 int adjustedX = x; 1214 if (x < 0) { 1215 adjustedX = 0; 1216 } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) { 1217 adjustedX = getMeasuredWidth() - container.getMeasuredWidth(); 1218 } 1219 mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0]; 1220 mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; 1221 mMiniKeyboard.setPopupOffset(adjustedX, y); 1222 mMiniKeyboard.setShifted(isShifted()); 1223 // Mini keyboard needs no pop-up key preview displayed. 1224 mMiniKeyboard.setPreviewEnabled(false); 1225 mMiniKeyboardPopup.setContentView(container); 1226 mMiniKeyboardPopup.setWidth(container.getMeasuredWidth()); 1227 mMiniKeyboardPopup.setHeight(container.getMeasuredHeight()); 1228 mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1229 1230 // Inject down event on the key to mini keyboard. 1231 long eventTime = SystemClock.uptimeMillis(); 1232 mMiniKeyboardPopupTime = eventTime; 1233 MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.x 1234 + popupKey.width / 2, popupKey.y + popupKey.height / 2, eventTime); 1235 mMiniKeyboard.onTouchEvent(downEvent); 1236 downEvent.recycle(); 1237 1238 invalidateAllKeys(); 1239 return true; 1240 } 1241 1242 private static boolean hasMultiplePopupChars(Key key) { 1243 if (key.popupCharacters != null && key.popupCharacters.length() > 1) { 1244 return true; 1245 } 1246 return false; 1247 } 1248 1249 private boolean shouldDrawIconFully(Key key) { 1250 return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key) 1251 || LatinKeyboard.hasPuncOrSmileysPopup(key); 1252 } 1253 1254 private boolean shouldDrawLabelAndIcon(Key key) { 1255 return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key) 1256 || LatinKeyboard.hasPuncOrSmileysPopup(key); 1257 } 1258 1259 private boolean isLatinF1Key(Key key) { 1260 return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key); 1261 } 1262 1263 private boolean isNonMicLatinF1Key(Key key) { 1264 return isLatinF1Key(key) && key.label != null; 1265 } 1266 1267 private static boolean isNumberAtEdgeOfPopupChars(Key key) { 1268 return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key); 1269 } 1270 1271 /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) { 1272 if (key.popupCharacters != null && key.popupCharacters.length() > 0 1273 && isAsciiDigit(key.popupCharacters.charAt(0))) { 1274 return true; 1275 } 1276 return false; 1277 } 1278 1279 /* package */ static boolean isNumberAtRightmostPopupChar(Key key) { 1280 if (key.popupCharacters != null && key.popupCharacters.length() > 0 1281 && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) { 1282 return true; 1283 } 1284 return false; 1285 } 1286 1287 private static boolean isAsciiDigit(char c) { 1288 return (c < 0x80) && Character.isDigit(c); 1289 } 1290 1291 private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) { 1292 return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action, 1293 x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0); 1294 } 1295 1296 private PointerTracker getPointerTracker(final int id) { 1297 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1298 final Key[] keys = mKeys; 1299 final OnKeyboardActionListener listener = mKeyboardActionListener; 1300 1301 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1302 for (int i = pointers.size(); i <= id; i++) { 1303 final PointerTracker tracker = 1304 new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); 1305 if (keys != null) 1306 tracker.setKeyboard(keys, mKeyHysteresisDistance); 1307 if (listener != null) 1308 tracker.setOnKeyboardActionListener(listener); 1309 pointers.add(tracker); 1310 } 1311 1312 return pointers.get(id); 1313 } 1314 1315 public boolean isInSlidingKeyInput() { 1316 if (mMiniKeyboard != null) { 1317 return mMiniKeyboard.isInSlidingKeyInput(); 1318 } else { 1319 return mPointerQueue.isInSlidingKeyInput(); 1320 } 1321 } 1322 1323 public int getPointerCount() { 1324 return mOldPointerCount; 1325 } 1326 1327 @Override 1328 public boolean onTouchEvent(MotionEvent me) { 1329 final int action = me.getActionMasked(); 1330 final int pointerCount = me.getPointerCount(); 1331 final int oldPointerCount = mOldPointerCount; 1332 mOldPointerCount = pointerCount; 1333 1334 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1335 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1336 // events except a transition from/to single-touch. 1337 if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { 1338 return true; 1339 } 1340 1341 // Track the last few movements to look for spurious swipes. 1342 mSwipeTracker.addMovement(me); 1343 1344 // Gesture detector must be enabled only when mini-keyboard is not on the screen. 1345 if (mMiniKeyboard == null 1346 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { 1347 dismissKeyPreview(); 1348 mHandler.cancelKeyTimers(); 1349 return true; 1350 } 1351 1352 final long eventTime = me.getEventTime(); 1353 final int index = me.getActionIndex(); 1354 final int id = me.getPointerId(index); 1355 final int x = (int)me.getX(index); 1356 final int y = (int)me.getY(index); 1357 1358 // Needs to be called after the gesture detector gets a turn, as it may have 1359 // displayed the mini keyboard 1360 if (mMiniKeyboard != null) { 1361 final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId); 1362 if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) { 1363 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex); 1364 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex); 1365 MotionEvent translated = generateMiniKeyboardMotionEvent(action, 1366 miniKeyboardX, miniKeyboardY, eventTime); 1367 mMiniKeyboard.onTouchEvent(translated); 1368 translated.recycle(); 1369 } 1370 return true; 1371 } 1372 1373 if (mHandler.isInKeyRepeat()) { 1374 // It will keep being in the key repeating mode while the key is being pressed. 1375 if (action == MotionEvent.ACTION_MOVE) { 1376 return true; 1377 } 1378 final PointerTracker tracker = getPointerTracker(id); 1379 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1380 // event (UP or DOWN) is non-modifier key. 1381 if (pointerCount > 1 && !tracker.isModifier()) { 1382 mHandler.cancelKeyRepeatTimer(); 1383 } 1384 // Up event will pass through. 1385 } 1386 1387 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1388 // Translate mutli-touch event to single-touch events on the device that has no distinct 1389 // multi-touch panel. 1390 if (!mHasDistinctMultitouch) { 1391 // Use only main (id=0) pointer tracker. 1392 PointerTracker tracker = getPointerTracker(0); 1393 if (pointerCount == 1 && oldPointerCount == 2) { 1394 // Multi-touch to single touch transition. 1395 // Send a down event for the latest pointer. 1396 tracker.onDownEvent(x, y, eventTime); 1397 } else if (pointerCount == 2 && oldPointerCount == 1) { 1398 // Single-touch to multi-touch transition. 1399 // Send an up event for the last pointer. 1400 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime); 1401 } else if (pointerCount == 1 && oldPointerCount == 1) { 1402 tracker.onTouchEvent(action, x, y, eventTime); 1403 } else { 1404 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1405 + " (old " + oldPointerCount + ")"); 1406 } 1407 return true; 1408 } 1409 1410 if (action == MotionEvent.ACTION_MOVE) { 1411 for (int i = 0; i < pointerCount; i++) { 1412 PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1413 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime); 1414 } 1415 } else { 1416 PointerTracker tracker = getPointerTracker(id); 1417 switch (action) { 1418 case MotionEvent.ACTION_DOWN: 1419 case MotionEvent.ACTION_POINTER_DOWN: 1420 onDownEvent(tracker, x, y, eventTime); 1421 break; 1422 case MotionEvent.ACTION_UP: 1423 case MotionEvent.ACTION_POINTER_UP: 1424 onUpEvent(tracker, x, y, eventTime); 1425 break; 1426 case MotionEvent.ACTION_CANCEL: 1427 onCancelEvent(tracker, x, y, eventTime); 1428 break; 1429 } 1430 } 1431 1432 return true; 1433 } 1434 1435 private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) { 1436 if (tracker.isOnModifierKey(x, y)) { 1437 // Before processing a down event of modifier key, all pointers already being tracked 1438 // should be released. 1439 mPointerQueue.releaseAllPointersExcept(null, eventTime); 1440 } 1441 tracker.onDownEvent(x, y, eventTime); 1442 mPointerQueue.add(tracker); 1443 } 1444 1445 private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) { 1446 if (tracker.isModifier()) { 1447 // Before processing an up event of modifier key, all pointers already being tracked 1448 // should be released. 1449 mPointerQueue.releaseAllPointersExcept(tracker, eventTime); 1450 } else { 1451 int index = mPointerQueue.lastIndexOf(tracker); 1452 if (index >= 0) { 1453 mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime); 1454 } else { 1455 Log.w(TAG, "onUpEvent: corresponding down event not found for pointer " 1456 + tracker.mPointerId); 1457 } 1458 } 1459 tracker.onUpEvent(x, y, eventTime); 1460 mPointerQueue.remove(tracker); 1461 } 1462 1463 private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) { 1464 tracker.onCancelEvent(x, y, eventTime); 1465 mPointerQueue.remove(tracker); 1466 } 1467 1468 protected void swipeRight() { 1469 mKeyboardActionListener.swipeRight(); 1470 } 1471 1472 protected void swipeLeft() { 1473 mKeyboardActionListener.swipeLeft(); 1474 } 1475 1476 protected void swipeUp() { 1477 mKeyboardActionListener.swipeUp(); 1478 } 1479 1480 protected void swipeDown() { 1481 mKeyboardActionListener.swipeDown(); 1482 } 1483 1484 public void closing() { 1485 mPreviewPopup.dismiss(); 1486 mHandler.cancelAllMessages(); 1487 1488 dismissPopupKeyboard(); 1489 mBuffer = null; 1490 mCanvas = null; 1491 mMiniKeyboardCache.clear(); 1492 } 1493 1494 @Override 1495 public void onDetachedFromWindow() { 1496 super.onDetachedFromWindow(); 1497 closing(); 1498 } 1499 1500 private void dismissPopupKeyboard() { 1501 if (mMiniKeyboardPopup.isShowing()) { 1502 mMiniKeyboardPopup.dismiss(); 1503 mMiniKeyboard = null; 1504 mMiniKeyboardOriginX = 0; 1505 mMiniKeyboardOriginY = 0; 1506 invalidateAllKeys(); 1507 } 1508 } 1509 1510 public boolean handleBack() { 1511 if (mMiniKeyboardPopup.isShowing()) { 1512 dismissPopupKeyboard(); 1513 return true; 1514 } 1515 return false; 1516 } 1517 } 1518