KeyboardView.java revision cb2469ae17e0ca8a94767008fef3945cb2a3b406
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 deltaX = me2.getX() - me1.getX(); 413 float deltaY = me2.getY() - me1.getY(); 414 int travelX = getWidth() / 2; // Half the keyboard width 415 int travelY = getHeight() / 2; // Half the keyboard height 416 mSwipeTracker.computeCurrentVelocity(1000); 417 final float endingVelocityX = mSwipeTracker.getXVelocity(); 418 final float endingVelocityY = mSwipeTracker.getYVelocity(); 419 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { 420 if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) { 421 swipeRight(); 422 return true; 423 } 424 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { 425 if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) { 426 swipeLeft(); 427 return true; 428 } 429 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { 430 if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) { 431 swipeUp(); 432 return true; 433 } 434 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 435 if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { 436 swipeDown(); 437 return true; 438 } 439 } 440 return false; 441 } 442 443 @Override 444 public boolean onDoubleTap(MotionEvent e) { 445 if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard 446 && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) { 447 final int pointerIndex = e.getActionIndex(); 448 final int id = e.getPointerId(pointerIndex); 449 final PointerTracker tracker = getPointerTracker(id); 450 if (tracker.isOnShiftKey((int)e.getX(), (int)e.getY())) { 451 onDoubleTapShiftKey(tracker); 452 mProcessingDoubleTapEvent = true; 453 return true; 454 } 455 } 456 mProcessingDoubleTapEvent = false; 457 return false; 458 } 459 460 @Override 461 public boolean onDoubleTapEvent(MotionEvent e) { 462 return mProcessingDoubleTapEvent; 463 } 464 }; 465 466 final boolean ignoreMultitouch = true; 467 mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); 468 mGestureDetector.setIsLongpressEnabled(false); 469 470 mHasDistinctMultitouch = context.getPackageManager() 471 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 472 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 473 } 474 475 public void setOnKeyboardActionListener(KeyboardActionListener listener) { 476 mKeyboardActionListener = listener; 477 for (PointerTracker tracker : mPointerTrackers) { 478 tracker.setOnKeyboardActionListener(listener); 479 } 480 } 481 482 /** 483 * Returns the {@link KeyboardActionListener} object. 484 * @return the listener attached to this keyboard 485 */ 486 protected KeyboardActionListener getOnKeyboardActionListener() { 487 return mKeyboardActionListener; 488 } 489 490 /** 491 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 492 * view will re-layout itself to accommodate the keyboard. 493 * @see Keyboard 494 * @see #getKeyboard() 495 * @param keyboard the keyboard to display in this view 496 */ 497 public void setKeyboard(Keyboard keyboard) { 498 if (mKeyboard != null) { 499 dismissKeyPreview(); 500 } 501 // Remove any pending messages, except dismissing preview 502 mHandler.cancelKeyTimers(); 503 mHandler.cancelPopupPreview(); 504 mKeyboard = keyboard; 505 LatinImeLogger.onSetKeyboard(keyboard); 506 mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), 507 -getPaddingTop() + mVerticalCorrection); 508 for (PointerTracker tracker : mPointerTrackers) { 509 tracker.setKeyboard(keyboard, mKeys, mKeyHysteresisDistance); 510 } 511 requestLayout(); 512 // Hint to reallocate the buffer if the size changed 513 mKeyboardChanged = true; 514 invalidateAllKeys(); 515 computeProximityThreshold(keyboard, mKeys); 516 mMiniKeyboardCache.clear(); 517 } 518 519 /** 520 * Returns the current keyboard being displayed by this view. 521 * @return the currently attached keyboard 522 * @see #setKeyboard(Keyboard) 523 */ 524 public Keyboard getKeyboard() { 525 return mKeyboard; 526 } 527 528 /** 529 * Return whether the device has distinct multi-touch panel. 530 * @return true if the device has distinct multi-touch panel. 531 */ 532 @Override 533 public boolean hasDistinctMultitouch() { 534 return mHasDistinctMultitouch; 535 } 536 537 /** 538 * Enables or disables the key feedback popup. This is a popup that shows a magnified 539 * version of the depressed key. By default the preview is enabled. 540 * @param previewEnabled whether or not to enable the key feedback popup 541 * @see #isPreviewEnabled() 542 */ 543 public void setPreviewEnabled(boolean previewEnabled) { 544 mShowPreview = previewEnabled; 545 } 546 547 /** 548 * Returns the enabled state of the key feedback popup. 549 * @return whether or not the key feedback popup is enabled 550 * @see #setPreviewEnabled(boolean) 551 */ 552 public boolean isPreviewEnabled() { 553 return mShowPreview; 554 } 555 556 public int getColorScheme() { 557 return mColorScheme; 558 } 559 560 public void setPopupParent(View v) { 561 mMiniKeyboardParent = v; 562 } 563 564 public void setPopupOffset(int x, int y) { 565 mPopupPreviewOffsetX = x; 566 mPopupPreviewOffsetY = y; 567 mPreviewPopup.dismiss(); 568 } 569 570 /** 571 * When enabled, calls to {@link KeyboardActionListener#onKey} will include key 572 * codes for adjacent keys. When disabled, only the primary key code will be 573 * reported. 574 * @param enabled whether or not the proximity correction is enabled 575 */ 576 public void setProximityCorrectionEnabled(boolean enabled) { 577 mKeyDetector.setProximityCorrectionEnabled(enabled); 578 } 579 580 /** 581 * Returns true if proximity correction is enabled. 582 */ 583 public boolean isProximityCorrectionEnabled() { 584 return mKeyDetector.isProximityCorrectionEnabled(); 585 } 586 587 protected CharSequence adjustCase(CharSequence label) { 588 if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3 589 && Character.isLowerCase(label.charAt(0))) { 590 return label.toString().toUpperCase(); 591 } 592 return label; 593 } 594 595 @Override 596 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 597 // Round up a little 598 if (mKeyboard == null) { 599 setMeasuredDimension( 600 getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); 601 } else { 602 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 603 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 604 width = MeasureSpec.getSize(widthMeasureSpec); 605 } 606 setMeasuredDimension( 607 width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 608 } 609 } 610 611 /** 612 * Compute the most common key width and use it as proximity key detection threshold. 613 * @param keyboard 614 * @param keys 615 */ 616 private void computeProximityThreshold(Keyboard keyboard, Key[] keys) { 617 if (keyboard == null || keys == null || keys.length == 0) return; 618 final HashMap<Integer, Integer> histogram = new HashMap<Integer, Integer>(); 619 int maxCount = 0; 620 int mostCommonWidth = 0; 621 for (Key key : keys) { 622 final Integer width = key.mWidth + key.mGap; 623 Integer count = histogram.get(width); 624 if (count == null) 625 count = 0; 626 histogram.put(width, ++count); 627 if (count > maxCount) { 628 maxCount = count; 629 mostCommonWidth = width; 630 } 631 } 632 mKeyDetector.setProximityThreshold(mostCommonWidth); 633 } 634 635 @Override 636 public void onSizeChanged(int w, int h, int oldw, int oldh) { 637 super.onSizeChanged(w, h, oldw, oldh); 638 // Release the buffer, if any and it will be reallocated on the next draw 639 mBuffer = null; 640 } 641 642 @Override 643 public void onDraw(Canvas canvas) { 644 super.onDraw(canvas); 645 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 646 onBufferDraw(); 647 } 648 canvas.drawBitmap(mBuffer, 0, 0, null); 649 } 650 651 @SuppressWarnings("unused") 652 private void onBufferDraw() { 653 if (mBuffer == null || mKeyboardChanged) { 654 if (mBuffer == null || mKeyboardChanged && 655 (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { 656 // Make sure our bitmap is at least 1x1 657 final int width = Math.max(1, getWidth()); 658 final int height = Math.max(1, getHeight()); 659 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 660 mCanvas = new Canvas(mBuffer); 661 } 662 invalidateAllKeys(); 663 mKeyboardChanged = false; 664 } 665 final Canvas canvas = mCanvas; 666 canvas.clipRect(mDirtyRect, Op.REPLACE); 667 668 if (mKeyboard == null) return; 669 670 final Paint paint = mPaint; 671 final Drawable keyBackground = mKeyBackground; 672 final Rect clipRegion = mClipRegion; 673 final Rect padding = mPadding; 674 final int kbdPaddingLeft = getPaddingLeft(); 675 final int kbdPaddingTop = getPaddingTop(); 676 final Key[] keys = mKeys; 677 final Key invalidKey = mInvalidatedKey; 678 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 679 680 boolean drawSingleKey = false; 681 if (invalidKey != null && canvas.getClipBounds(clipRegion)) { 682 // TODO we should use Rect.inset and Rect.contains here. 683 // Is clipRegion completely contained within the invalidated key? 684 if (invalidKey.mX + kbdPaddingLeft - 1 <= clipRegion.left && 685 invalidKey.mY + kbdPaddingTop - 1 <= clipRegion.top && 686 invalidKey.mX + invalidKey.mWidth + kbdPaddingLeft + 1 >= clipRegion.right && 687 invalidKey.mY + invalidKey.mHeight + kbdPaddingTop + 1 >= clipRegion.bottom) { 688 drawSingleKey = true; 689 } 690 } 691 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); 692 final int keyCount = keys.length; 693 for (int i = 0; i < keyCount; i++) { 694 final Key key = keys[i]; 695 if (drawSingleKey && invalidKey != key) { 696 continue; 697 } 698 int[] drawableState = key.getCurrentDrawableState(); 699 keyBackground.setState(drawableState); 700 701 // Switch the character to uppercase if shift is pressed 702 String label = key.mLabel == null? null : adjustCase(key.mLabel).toString(); 703 704 final Rect bounds = keyBackground.getBounds(); 705 if (key.mWidth != bounds.right || key.mHeight != bounds.bottom) { 706 keyBackground.setBounds(0, 0, key.mWidth, key.mHeight); 707 } 708 canvas.translate(key.mX + kbdPaddingLeft, key.mY + kbdPaddingTop); 709 keyBackground.draw(canvas); 710 711 final int rowHeight = padding.top + key.mHeight; 712 // Draw key label 713 if (label != null) { 714 // For characters, use large font. For labels like "Done", use small font. 715 final int labelSize = getLabelSizeAndSetPaint(label, key, paint); 716 final int labelCharHeight = getLabelCharHeight(labelSize, paint); 717 718 // Vertical label text alignment. 719 final float baseline; 720 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) { 721 baseline = key.mHeight - 722 + labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; 723 if (DEBUG_SHOW_ALIGN) 724 drawHorizontalLine(canvas, (int)baseline, key.mWidth, 0xc0008000, 725 new Paint()); 726 } else { // Align center 727 final float centerY = (key.mHeight + padding.top - padding.bottom) / 2; 728 baseline = centerY 729 + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; 730 if (DEBUG_SHOW_ALIGN) 731 drawHorizontalLine(canvas, (int)baseline, key.mWidth, 0xc0008000, 732 new Paint()); 733 } 734 // Horizontal label text alignment 735 final int positionX; 736 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 737 positionX = mKeyLabelHorizontalPadding + padding.left; 738 paint.setTextAlign(Align.LEFT); 739 if (DEBUG_SHOW_ALIGN) 740 drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint()); 741 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 742 positionX = key.mWidth - mKeyLabelHorizontalPadding - padding.right; 743 paint.setTextAlign(Align.RIGHT); 744 if (DEBUG_SHOW_ALIGN) 745 drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint()); 746 } else { 747 positionX = (key.mWidth + padding.left - padding.right) / 2; 748 paint.setTextAlign(Align.CENTER); 749 if (DEBUG_SHOW_ALIGN && label.length() > 1) 750 drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint()); 751 } 752 if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) { 753 paint.setColor(mKeyTextColorDisabled); 754 } else { 755 paint.setColor(mKeyTextColor); 756 } 757 // Set a drop shadow for the text 758 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 759 canvas.drawText(label, positionX, baseline, paint); 760 // Turn off drop shadow 761 paint.setShadowLayer(0, 0, 0, 0); 762 } 763 // Draw key icon 764 final Drawable icon = key.getIcon(); 765 if (key.mLabel == null && icon != null) { 766 final int drawableWidth = icon.getIntrinsicWidth(); 767 final int drawableHeight = icon.getIntrinsicHeight(); 768 final int drawableX; 769 final int drawableY = ( 770 key.mHeight + padding.top - padding.bottom - drawableHeight) / 2; 771 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 772 drawableX = padding.left + mKeyLabelHorizontalPadding; 773 if (DEBUG_SHOW_ALIGN) 774 drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint()); 775 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 776 drawableX = key.mWidth - padding.right - mKeyLabelHorizontalPadding 777 - drawableWidth; 778 if (DEBUG_SHOW_ALIGN) 779 drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight, 780 0xc0808000, new Paint()); 781 } else { // Align center 782 drawableX = (key.mWidth + padding.left - padding.right - drawableWidth) / 2; 783 if (DEBUG_SHOW_ALIGN) 784 drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight, 785 0xc0008080, new Paint()); 786 } 787 drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight); 788 if (DEBUG_SHOW_ALIGN) 789 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 790 0x80c00000, new Paint()); 791 } 792 if (key.mHintIcon != null) { 793 final int drawableWidth = key.mWidth; 794 final int drawableHeight = key.mHeight; 795 final int drawableX = 0; 796 final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; 797 Drawable hintIcon = (isManualTemporaryUpperCase 798 && key.mManualTemporaryUpperCaseHintIcon != null) 799 ? key.mManualTemporaryUpperCaseHintIcon : key.mHintIcon; 800 drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight); 801 if (DEBUG_SHOW_ALIGN) 802 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 803 0x80c0c000, new Paint()); 804 } 805 canvas.translate(-key.mX - kbdPaddingLeft, -key.mY - kbdPaddingTop); 806 } 807 808 if (DEBUG_KEYBOARD_GRID) { 809 Paint p = new Paint(); 810 p.setStyle(Paint.Style.STROKE); 811 p.setStrokeWidth(1.0f); 812 p.setColor(0x800000c0); 813 int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) / mKeyboard.GRID_WIDTH; 814 int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) / mKeyboard.GRID_HEIGHT; 815 for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) 816 canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); 817 for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) 818 canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); 819 } 820 821 mInvalidatedKey = null; 822 // Overlay a dark rectangle to dim the keyboard 823 if (mMiniKeyboard != null) { 824 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 825 canvas.drawRect(0, 0, getWidth(), getHeight(), paint); 826 } 827 828 if (DEBUG) { 829 if (mShowTouchPoints) { 830 for (PointerTracker tracker : mPointerTrackers) { 831 int startX = tracker.getStartX(); 832 int startY = tracker.getStartY(); 833 int lastX = tracker.getLastX(); 834 int lastY = tracker.getLastY(); 835 paint.setAlpha(128); 836 paint.setColor(0xFFFF0000); 837 canvas.drawCircle(startX, startY, 3, paint); 838 canvas.drawLine(startX, startY, lastX, lastY, paint); 839 paint.setColor(0xFF0000FF); 840 canvas.drawCircle(lastX, lastY, 3, paint); 841 paint.setColor(0xFF00FF00); 842 canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint); 843 } 844 } 845 } 846 847 mDrawPending = false; 848 mDirtyRect.setEmpty(); 849 } 850 851 private int getLabelSizeAndSetPaint(CharSequence label, Key key, Paint paint) { 852 // For characters, use large font. For labels like "Done", use small font. 853 final int labelSize; 854 final Typeface labelStyle; 855 if (label.length() > 1 && key.mCodes.length < 2) { 856 labelSize = mLabelTextSize; 857 if ((key.mLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) { 858 labelStyle = Typeface.DEFAULT; 859 } else { 860 labelStyle = Typeface.DEFAULT_BOLD; 861 } 862 } else { 863 labelSize = mKeyLetterSize; 864 labelStyle = mKeyLetterStyle; 865 } 866 paint.setTextSize(labelSize); 867 paint.setTypeface(labelStyle); 868 return labelSize; 869 } 870 871 private int getLabelCharHeight(int labelSize, Paint paint) { 872 Integer labelHeightValue = mTextHeightCache.get(labelSize); 873 final int labelCharHeight; 874 if (labelHeightValue != null) { 875 labelCharHeight = labelHeightValue; 876 } else { 877 Rect textBounds = new Rect(); 878 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds); 879 labelCharHeight = textBounds.height(); 880 mTextHeightCache.put(labelSize, labelCharHeight); 881 } 882 return labelCharHeight; 883 } 884 885 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 886 int height) { 887 canvas.translate(x, y); 888 icon.setBounds(0, 0, width, height); 889 icon.draw(canvas); 890 canvas.translate(-x, -y); 891 } 892 893 private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { 894 paint.setStyle(Paint.Style.STROKE); 895 paint.setStrokeWidth(1.0f); 896 paint.setColor(color); 897 canvas.drawLine(0, y, w, y, paint); 898 } 899 900 private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { 901 paint.setStyle(Paint.Style.STROKE); 902 paint.setStrokeWidth(1.0f); 903 paint.setColor(color); 904 canvas.drawLine(x, 0, x, h, paint); 905 } 906 907 private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, 908 Paint paint) { 909 paint.setStyle(Paint.Style.STROKE); 910 paint.setStrokeWidth(1.0f); 911 paint.setColor(color); 912 canvas.translate(x, y); 913 canvas.drawRect(0, 0, w, h, paint); 914 canvas.translate(-x, -y); 915 } 916 917 public void setForeground(boolean foreground) { 918 mInForeground = foreground; 919 } 920 921 // TODO: clean up this method. 922 private void dismissKeyPreview() { 923 for (PointerTracker tracker : mPointerTrackers) 924 tracker.releaseKey(); 925 showPreview(KeyDetector.NOT_A_KEY, null); 926 } 927 928 @Override 929 public void showPreview(int keyIndex, PointerTracker tracker) { 930 int oldKeyIndex = mOldPreviewKeyIndex; 931 mOldPreviewKeyIndex = keyIndex; 932 // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show 933 // the space key preview and 3) pointer moves off the space key to other letter key, we 934 // should hide the preview of the previous key. 935 @SuppressWarnings("unused") 936 final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) 937 || (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER 938 && SubtypeSwitcher.getInstance().needsToDisplayLanguage() 939 && (tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex))); 940 // If key changed and preview is on or the key is space (language switch is enabled) 941 if (oldKeyIndex != keyIndex && (mShowPreview || (hidePreviewOrShowSpaceKeyPreview))) { 942 if (keyIndex == KeyDetector.NOT_A_KEY) { 943 mHandler.cancelPopupPreview(); 944 mHandler.dismissPreview(mDelayAfterPreview); 945 } else if (tracker != null) { 946 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker); 947 } 948 } 949 } 950 951 // TODO Must fix popup preview on xlarge layout 952 private void showKey(final int keyIndex, PointerTracker tracker) { 953 Key key = tracker.getKey(keyIndex); 954 // If keyIndex is invalid or IME is already closed, we must not show key preview. 955 // Trying to show preview PopupWindow while root window is closed causes 956 // WindowManager.BadTokenException. 957 if (key == null || !mInForeground) 958 return; 959 // What we show as preview should match what we show on key top in onBufferDraw(). 960 if (key.mLabel != null) { 961 // TODO Should take care of temporaryShiftLabel here. 962 mPreviewText.setCompoundDrawables(null, null, null, null); 963 mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); 964 if (key.mLabel.length() > 1 && key.mCodes.length < 2) { 965 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); 966 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); 967 } else { 968 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); 969 mPreviewText.setTypeface(mKeyLetterStyle); 970 } 971 } else { 972 final Drawable previewIcon = key.getPreviewIcon(); 973 mPreviewText.setCompoundDrawables(null, null, null, 974 previewIcon != null ? previewIcon : key.getIcon()); 975 mPreviewText.setText(null); 976 } 977 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 978 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 979 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.mWidth 980 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); 981 final int popupHeight = mPreviewHeight; 982 LayoutParams lp = mPreviewText.getLayoutParams(); 983 if (lp != null) { 984 lp.width = popupWidth; 985 lp.height = popupHeight; 986 } 987 988 int popupPreviewX = key.mX - (popupWidth - key.mWidth) / 2; 989 int popupPreviewY = key.mY - popupHeight + mPreviewOffset; 990 991 mHandler.cancelDismissPreview(); 992 if (mOffsetInWindow == null) { 993 mOffsetInWindow = new int[2]; 994 getLocationInWindow(mOffsetInWindow); 995 mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero 996 mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero 997 int[] windowLocation = new int[2]; 998 getLocationOnScreen(windowLocation); 999 mWindowY = windowLocation[1]; 1000 } 1001 // Set the preview background state 1002 mPreviewText.getBackground().setState( 1003 key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 1004 popupPreviewX += mOffsetInWindow[0]; 1005 popupPreviewY += mOffsetInWindow[1]; 1006 1007 // If the popup cannot be shown above the key, put it on the side 1008 if (popupPreviewY + mWindowY < 0) { 1009 // If the key you're pressing is on the left side of the keyboard, show the popup on 1010 // the right, offset by enough to see at least one key to the left/right. 1011 if (key.mX + key.mWidth <= getWidth() / 2) { 1012 popupPreviewX += (int) (key.mWidth * 2.5); 1013 } else { 1014 popupPreviewX -= (int) (key.mWidth * 2.5); 1015 } 1016 popupPreviewY += popupHeight; 1017 } 1018 1019 try { 1020 if (mPreviewPopup.isShowing()) { 1021 mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); 1022 } else { 1023 mPreviewPopup.setWidth(popupWidth); 1024 mPreviewPopup.setHeight(popupHeight); 1025 mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, 1026 popupPreviewX, popupPreviewY); 1027 } 1028 } catch (WindowManager.BadTokenException e) { 1029 // Swallow the exception which will be happened when IME is already closed. 1030 Log.w(TAG, "LatinIME is already closed when tried showing key preview."); 1031 } 1032 // Record popup preview position to display mini-keyboard later at the same positon 1033 mPopupPreviewDisplayedY = popupPreviewY; 1034 mPreviewText.setVisibility(VISIBLE); 1035 } 1036 1037 /** 1038 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 1039 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 1040 * draws the cached buffer. 1041 * @see #invalidateKey(Key) 1042 */ 1043 public void invalidateAllKeys() { 1044 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1045 mDrawPending = true; 1046 invalidate(); 1047 } 1048 1049 /** 1050 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 1051 * one key is changing it's content. Any changes that affect the position or size of the key 1052 * may not be honored. 1053 * @param key key in the attached {@link Keyboard}. 1054 * @see #invalidateAllKeys 1055 */ 1056 @Override 1057 public void invalidateKey(Key key) { 1058 if (key == null) 1059 return; 1060 mInvalidatedKey = key; 1061 // TODO we should clean up this and record key's region to use in onBufferDraw. 1062 mDirtyRect.union(key.mX + getPaddingLeft(), key.mY + getPaddingTop(), 1063 key.mX + key.mWidth + getPaddingLeft(), key.mY + key.mHeight + getPaddingTop()); 1064 onBufferDraw(); 1065 invalidate(key.mX + getPaddingLeft(), key.mY + getPaddingTop(), 1066 key.mX + key.mWidth + getPaddingLeft(), key.mY + key.mHeight + getPaddingTop()); 1067 } 1068 1069 private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) { 1070 // Check if we have a popup layout specified first. 1071 if (mPopupLayout == 0) { 1072 return false; 1073 } 1074 1075 Key popupKey = tracker.getKey(keyIndex); 1076 if (popupKey == null) 1077 return false; 1078 boolean result = onLongPress(popupKey); 1079 if (result) { 1080 dismissKeyPreview(); 1081 mMiniKeyboardTrackerId = tracker.mPointerId; 1082 // Mark this tracker "already processed" and remove it from the pointer queue 1083 tracker.setAlreadyProcessed(); 1084 mPointerQueue.remove(tracker); 1085 } 1086 return result; 1087 } 1088 1089 private void onLongPressShiftKey(PointerTracker tracker) { 1090 tracker.setAlreadyProcessed(); 1091 mPointerQueue.remove(tracker); 1092 mKeyboardActionListener.onKey(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1093 } 1094 1095 private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { 1096 // When shift key is double tapped, the first tap is correctly processed as usual tap. And 1097 // the second tap is treated as this double tap event, so that we need not mark tracker 1098 // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. 1099 mKeyboardActionListener.onKey(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1100 } 1101 1102 private View inflateMiniKeyboardContainer(Key popupKey) { 1103 int popupKeyboardResId = mKeyboard.getPopupKeyboardResId(); 1104 LayoutInflater inflater = (LayoutInflater)getContext().getSystemService( 1105 Context.LAYOUT_INFLATER_SERVICE); 1106 View container = inflater.inflate(mPopupLayout, null); 1107 if (container == null) 1108 throw new NullPointerException(); 1109 1110 KeyboardView miniKeyboard = 1111 (KeyboardView)container.findViewById(R.id.KeyboardView); 1112 miniKeyboard.setOnKeyboardActionListener(new KeyboardActionListener() { 1113 @Override 1114 public void onKey(int primaryCode, int[] keyCodes, int x, int y) { 1115 mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y); 1116 dismissPopupKeyboard(); 1117 } 1118 1119 @Override 1120 public void onText(CharSequence text) { 1121 mKeyboardActionListener.onText(text); 1122 dismissPopupKeyboard(); 1123 } 1124 1125 @Override 1126 public void onCancel() { 1127 dismissPopupKeyboard(); 1128 } 1129 1130 @Override 1131 public void swipeLeft() { 1132 // Nothing to do. 1133 } 1134 @Override 1135 public void swipeRight() { 1136 // Nothing to do. 1137 } 1138 @Override 1139 public void swipeUp() { 1140 // Nothing to do. 1141 } 1142 @Override 1143 public void swipeDown() { 1144 // Nothing to do. 1145 } 1146 @Override 1147 public void onPress(int primaryCode) { 1148 mKeyboardActionListener.onPress(primaryCode); 1149 } 1150 @Override 1151 public void onRelease(int primaryCode) { 1152 mKeyboardActionListener.onRelease(primaryCode); 1153 } 1154 }); 1155 // Override default ProximityKeyDetector. 1156 miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance); 1157 // Remove gesture detector on mini-keyboard 1158 miniKeyboard.mGestureDetector = null; 1159 1160 Keyboard keyboard = new MiniKeyboardBuilder(getContext(), popupKeyboardResId, popupKey) 1161 .build(); 1162 miniKeyboard.setKeyboard(keyboard); 1163 miniKeyboard.setPopupParent(this); 1164 1165 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1166 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1167 1168 return container; 1169 } 1170 1171 private static boolean isOneRowKeys(List<Key> keys) { 1172 if (keys.size() == 0) return false; 1173 final int edgeFlags = keys.get(0).mEdgeFlags; 1174 // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows, 1175 // does not have both top and bottom edge flags on at the same time. On the other hand, 1176 // the first key of mini keyboard that was created with popupCharacters must have both top 1177 // and bottom edge flags on. 1178 // When you want to use one row mini-keyboard from xml file, make sure that the row has 1179 // both top and bottom edge flags set. 1180 return (edgeFlags & Keyboard.EDGE_TOP) != 0 1181 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; 1182 } 1183 1184 /** 1185 * Called when a key is long pressed. By default this will open any popup keyboard associated 1186 * with this key through the attributes popupLayout and popupCharacters. 1187 * @param popupKey the key that was long pressed 1188 * @return true if the long press is handled, false otherwise. Subclasses should call the 1189 * method on the base class if the subclass doesn't wish to handle the call. 1190 */ 1191 protected boolean onLongPress(Key popupKey) { 1192 if (popupKey.mPopupCharacters == null) 1193 return false; 1194 1195 View container = mMiniKeyboardCache.get(popupKey); 1196 if (container == null) { 1197 container = inflateMiniKeyboardContainer(popupKey); 1198 mMiniKeyboardCache.put(popupKey, container); 1199 } 1200 mMiniKeyboard = (KeyboardView)container.findViewById(R.id.KeyboardView); 1201 if (mWindowOffset == null) { 1202 mWindowOffset = new int[2]; 1203 getLocationInWindow(mWindowOffset); 1204 } 1205 1206 // Get width of a key in the mini popup keyboard = "miniKeyWidth". 1207 // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard. 1208 // We adjust the position of mini popup keyboard with the edge key in it: 1209 // a) When we have the leftmost key in popup keyboard directly above the pressed key 1210 // Right edges of both keys should be aligned for consistent default selection 1211 // b) When we have the rightmost key in popup keyboard directly above the pressed key 1212 // Left edges of both keys should be aligned for consistent default selection 1213 final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys(); 1214 final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).mWidth : 0; 1215 1216 // HACK: Have the leftmost number in the popup characters right above the key 1217 boolean isNumberAtLeftmost = 1218 hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey); 1219 int popupX = popupKey.mX + mWindowOffset[0]; 1220 popupX += getPaddingLeft(); 1221 if (isNumberAtLeftmost) { 1222 popupX += popupKey.mWidth - miniKeyWidth; // adjustment for a) described above 1223 popupX -= container.getPaddingLeft(); 1224 } else { 1225 popupX += miniKeyWidth; // adjustment for b) described above 1226 popupX -= container.getMeasuredWidth(); 1227 popupX += container.getPaddingRight(); 1228 } 1229 int popupY = popupKey.mY + mWindowOffset[1]; 1230 popupY += getPaddingTop(); 1231 popupY -= container.getMeasuredHeight(); 1232 popupY += container.getPaddingBottom(); 1233 final int x = popupX; 1234 final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY; 1235 1236 int adjustedX = x; 1237 if (x < 0) { 1238 adjustedX = 0; 1239 } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) { 1240 adjustedX = getMeasuredWidth() - container.getMeasuredWidth(); 1241 } 1242 mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0]; 1243 mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; 1244 mMiniKeyboard.setPopupOffset(adjustedX, y); 1245 Keyboard baseMiniKeyboard = mMiniKeyboard.getKeyboard(); 1246 if (baseMiniKeyboard != null && baseMiniKeyboard.setShifted(mKeyboard == null 1247 ? false : mKeyboard.isShiftedOrShiftLocked())) { 1248 mMiniKeyboard.invalidateAllKeys(); 1249 } 1250 // Mini keyboard needs no pop-up key preview displayed. 1251 mMiniKeyboard.setPreviewEnabled(false); 1252 mMiniKeyboardPopup.setContentView(container); 1253 mMiniKeyboardPopup.setWidth(container.getMeasuredWidth()); 1254 mMiniKeyboardPopup.setHeight(container.getMeasuredHeight()); 1255 mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); 1256 1257 // Inject down event on the key to mini keyboard. 1258 long eventTime = SystemClock.uptimeMillis(); 1259 mMiniKeyboardPopupTime = eventTime; 1260 MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.mX 1261 + popupKey.mWidth / 2, popupKey.mY + popupKey.mHeight / 2, eventTime); 1262 mMiniKeyboard.onTouchEvent(downEvent); 1263 downEvent.recycle(); 1264 1265 invalidateAllKeys(); 1266 return true; 1267 } 1268 1269 private static boolean hasMultiplePopupChars(Key key) { 1270 if (key.mPopupCharacters != null && key.mPopupCharacters.length > 1) { 1271 return true; 1272 } 1273 return false; 1274 } 1275 1276 private static boolean isNumberAtLeftmostPopupChar(Key key) { 1277 if (key.mPopupCharacters != null && isAsciiDigit(key.mPopupCharacters[0].charAt(0))) { 1278 return true; 1279 } 1280 return false; 1281 } 1282 1283 private static boolean isAsciiDigit(char c) { 1284 return (c < 0x80) && Character.isDigit(c); 1285 } 1286 1287 private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) { 1288 return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action, 1289 x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0); 1290 } 1291 1292 private PointerTracker getPointerTracker(final int id) { 1293 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1294 final Key[] keys = mKeys; 1295 final KeyboardActionListener listener = mKeyboardActionListener; 1296 1297 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1298 for (int i = pointers.size(); i <= id; i++) { 1299 final PointerTracker tracker = 1300 new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); 1301 if (keys != null) 1302 tracker.setKeyboard(mKeyboard, keys, mKeyHysteresisDistance); 1303 if (listener != null) 1304 tracker.setOnKeyboardActionListener(listener); 1305 pointers.add(tracker); 1306 } 1307 1308 return pointers.get(id); 1309 } 1310 1311 public boolean isInSlidingKeyInput() { 1312 if (mMiniKeyboard != null) { 1313 return mMiniKeyboard.isInSlidingKeyInput(); 1314 } else { 1315 return mPointerQueue.isInSlidingKeyInput(); 1316 } 1317 } 1318 1319 public int getPointerCount() { 1320 return mOldPointerCount; 1321 } 1322 1323 @Override 1324 public boolean onTouchEvent(MotionEvent me) { 1325 final int action = me.getActionMasked(); 1326 final int pointerCount = me.getPointerCount(); 1327 final int oldPointerCount = mOldPointerCount; 1328 mOldPointerCount = pointerCount; 1329 1330 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1331 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1332 // events except a transition from/to single-touch. 1333 if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { 1334 return true; 1335 } 1336 1337 // Track the last few movements to look for spurious swipes. 1338 mSwipeTracker.addMovement(me); 1339 1340 // Gesture detector must be enabled only when mini-keyboard is not on the screen. 1341 if (mMiniKeyboard == null 1342 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { 1343 dismissKeyPreview(); 1344 mHandler.cancelKeyTimers(); 1345 return true; 1346 } 1347 1348 final long eventTime = me.getEventTime(); 1349 final int index = me.getActionIndex(); 1350 final int id = me.getPointerId(index); 1351 final int x = (int)me.getX(index); 1352 final int y = (int)me.getY(index); 1353 1354 // Needs to be called after the gesture detector gets a turn, as it may have 1355 // displayed the mini keyboard 1356 if (mMiniKeyboard != null) { 1357 final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId); 1358 if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) { 1359 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex); 1360 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex); 1361 MotionEvent translated = generateMiniKeyboardMotionEvent(action, 1362 miniKeyboardX, miniKeyboardY, eventTime); 1363 mMiniKeyboard.onTouchEvent(translated); 1364 translated.recycle(); 1365 } 1366 return true; 1367 } 1368 1369 if (mHandler.isInKeyRepeat()) { 1370 // It will keep being in the key repeating mode while the key is being pressed. 1371 if (action == MotionEvent.ACTION_MOVE) { 1372 return true; 1373 } 1374 final PointerTracker tracker = getPointerTracker(id); 1375 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1376 // event (UP or DOWN) is non-modifier key. 1377 if (pointerCount > 1 && !tracker.isModifier()) { 1378 mHandler.cancelKeyRepeatTimer(); 1379 } 1380 // Up event will pass through. 1381 } 1382 1383 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1384 // Translate mutli-touch event to single-touch events on the device that has no distinct 1385 // multi-touch panel. 1386 if (!mHasDistinctMultitouch) { 1387 // Use only main (id=0) pointer tracker. 1388 PointerTracker tracker = getPointerTracker(0); 1389 if (pointerCount == 1 && oldPointerCount == 2) { 1390 // Multi-touch to single touch transition. 1391 // Send a down event for the latest pointer. 1392 tracker.onDownEvent(x, y, eventTime); 1393 } else if (pointerCount == 2 && oldPointerCount == 1) { 1394 // Single-touch to multi-touch transition. 1395 // Send an up event for the last pointer. 1396 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime); 1397 } else if (pointerCount == 1 && oldPointerCount == 1) { 1398 tracker.onTouchEvent(action, x, y, eventTime); 1399 } else { 1400 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1401 + " (old " + oldPointerCount + ")"); 1402 } 1403 return true; 1404 } 1405 1406 if (action == MotionEvent.ACTION_MOVE) { 1407 for (int i = 0; i < pointerCount; i++) { 1408 PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1409 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime); 1410 } 1411 } else { 1412 PointerTracker tracker = getPointerTracker(id); 1413 switch (action) { 1414 case MotionEvent.ACTION_DOWN: 1415 case MotionEvent.ACTION_POINTER_DOWN: 1416 onDownEvent(tracker, x, y, eventTime); 1417 break; 1418 case MotionEvent.ACTION_UP: 1419 case MotionEvent.ACTION_POINTER_UP: 1420 onUpEvent(tracker, x, y, eventTime); 1421 break; 1422 case MotionEvent.ACTION_CANCEL: 1423 onCancelEvent(tracker, x, y, eventTime); 1424 break; 1425 } 1426 } 1427 1428 return true; 1429 } 1430 1431 private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) { 1432 if (tracker.isOnModifierKey(x, y)) { 1433 // Before processing a down event of modifier key, all pointers already being tracked 1434 // should be released. 1435 mPointerQueue.releaseAllPointersExcept(null, eventTime); 1436 } 1437 tracker.onDownEvent(x, y, eventTime); 1438 mPointerQueue.add(tracker); 1439 } 1440 1441 private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) { 1442 if (tracker.isModifier()) { 1443 // Before processing an up event of modifier key, all pointers already being tracked 1444 // should be released. 1445 mPointerQueue.releaseAllPointersExcept(tracker, eventTime); 1446 } else { 1447 int index = mPointerQueue.lastIndexOf(tracker); 1448 if (index >= 0) { 1449 mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime); 1450 } else { 1451 Log.w(TAG, "onUpEvent: corresponding down event not found for pointer " 1452 + tracker.mPointerId); 1453 } 1454 } 1455 tracker.onUpEvent(x, y, eventTime); 1456 mPointerQueue.remove(tracker); 1457 } 1458 1459 private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) { 1460 tracker.onCancelEvent(x, y, eventTime); 1461 mPointerQueue.remove(tracker); 1462 } 1463 1464 protected void swipeRight() { 1465 mKeyboardActionListener.swipeRight(); 1466 } 1467 1468 protected void swipeLeft() { 1469 mKeyboardActionListener.swipeLeft(); 1470 } 1471 1472 protected void swipeUp() { 1473 mKeyboardActionListener.swipeUp(); 1474 } 1475 1476 protected void swipeDown() { 1477 mKeyboardActionListener.swipeDown(); 1478 } 1479 1480 public void closing() { 1481 mPreviewPopup.dismiss(); 1482 mHandler.cancelAllMessages(); 1483 1484 dismissPopupKeyboard(); 1485 mBuffer = null; 1486 mCanvas = null; 1487 mMiniKeyboardCache.clear(); 1488 } 1489 1490 @Override 1491 public void onDetachedFromWindow() { 1492 super.onDetachedFromWindow(); 1493 closing(); 1494 } 1495 1496 private void dismissPopupKeyboard() { 1497 if (mMiniKeyboardPopup.isShowing()) { 1498 mMiniKeyboardPopup.dismiss(); 1499 mMiniKeyboard = null; 1500 mMiniKeyboardOriginX = 0; 1501 mMiniKeyboardOriginY = 0; 1502 invalidateAllKeys(); 1503 } 1504 } 1505 1506 public boolean handleBack() { 1507 if (mMiniKeyboardPopup.isShowing()) { 1508 dismissPopupKeyboard(); 1509 return true; 1510 } 1511 return false; 1512 } 1513} 1514