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