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