KeyboardView.java revision b7758d6f912093747d4b18fbc8d1dcd77c7d1f9b
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 android.content.Context; 20import android.content.pm.PackageManager; 21import android.content.res.Resources; 22import android.content.res.TypedArray; 23import android.graphics.Bitmap; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.Paint; 27import android.graphics.Paint.Align; 28import android.graphics.PorterDuff; 29import android.graphics.Rect; 30import android.graphics.Region.Op; 31import android.graphics.Typeface; 32import android.graphics.drawable.Drawable; 33import android.os.Handler; 34import android.os.Message; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.util.TypedValue; 38import android.view.GestureDetector; 39import android.view.LayoutInflater; 40import android.view.MotionEvent; 41import android.view.View; 42import android.view.ViewConfiguration; 43import android.view.ViewGroup; 44import android.view.ViewGroup.MarginLayoutParams; 45import android.widget.FrameLayout; 46import android.widget.LinearLayout; 47import android.widget.PopupWindow; 48import android.widget.TextView; 49 50import com.android.inputmethod.latin.LatinImeLogger; 51import com.android.inputmethod.latin.R; 52 53import java.util.ArrayList; 54import java.util.HashMap; 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_backgroundDimAmount 62 * @attr ref R.styleable#KeyboardView_colorScheme 63 * @attr ref R.styleable#KeyboardView_keyBackground 64 * @attr ref R.styleable#KeyboardView_keyHysteresisDistance 65 * @attr ref R.styleable#KeyboardView_keyLetterRatio 66 * @attr ref R.styleable#KeyboardView_keyLetterStyle 67 * @attr ref R.styleable#KeyboardView_keyPreviewLayout 68 * @attr ref R.styleable#KeyboardView_keyPreviewOffset 69 * @attr ref R.styleable#KeyboardView_keyPreviewHeight 70 * @attr ref R.styleable#KeyboardView_keyTextColor 71 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled 72 * @attr ref R.styleable#KeyboardView_labelTextRatio 73 * @attr ref R.styleable#KeyboardView_verticalCorrection 74 * @attr ref R.styleable#KeyboardView_popupLayout 75 * @attr ref R.styleable#KeyboardView_shadowColor 76 * @attr ref R.styleable#KeyboardView_shadowRadius 77 */ 78public class KeyboardView extends View implements PointerTracker.UIProxy { 79 private static final String TAG = KeyboardView.class.getSimpleName(); 80 private static final boolean DEBUG_SHOW_ALIGN = false; 81 private static final boolean DEBUG_KEYBOARD_GRID = false; 82 83 private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true; 84 private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true; 85 86 public static final int COLOR_SCHEME_WHITE = 0; 87 public static final int COLOR_SCHEME_BLACK = 1; 88 89 // Timing constants 90 private final int mKeyRepeatInterval; 91 92 // Miscellaneous constants 93 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 94 private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1; 95 96 // XML attribute 97 private final float mKeyLetterRatio; 98 private final int mKeyTextColor; 99 private final int mKeyTextColorDisabled; 100 private final Typeface mKeyLetterStyle; 101 private final float mLabelTextRatio; 102 private final int mColorScheme; 103 private final int mShadowColor; 104 private final float mShadowRadius; 105 private final Drawable mKeyBackground; 106 private final float mBackgroundDimAmount; 107 private final float mKeyHysteresisDistance; 108 private final float mVerticalCorrection; 109 private final int mPreviewOffset; 110 private final int mPreviewHeight; 111 private final int mPopupLayout; 112 113 // Main keyboard 114 private Keyboard mKeyboard; 115 private int mKeyLetterSize; 116 private int mLabelTextSize; 117 118 // Key preview 119 private boolean mInForeground; 120 private TextView mPreviewText; 121 private float mPreviewTextRatio; 122 private int mPreviewTextSize; 123 private boolean mShowKeyPreviewPopup = true; 124 private int mKeyPreviewPopupDisplayedY = -1; 125 private final int mDelayBeforePreview; 126 private int mDelayAfterPreview; 127 private ViewGroup mPreviewPlacer; 128 private final int[] mCoordinates = new int[2]; 129 130 // Mini keyboard 131 private PopupWindow mPopupWindow; 132 private PopupPanel mPopupMiniKeyboardPanel; 133 private final WeakHashMap<Key, PopupPanel> mPopupPanelCache = 134 new WeakHashMap<Key, PopupPanel>(); 135 136 /** Listener for {@link KeyboardActionListener}. */ 137 private KeyboardActionListener mKeyboardActionListener; 138 139 private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); 140 141 // TODO: Let the PointerTracker class manage this pointer queue 142 private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue(); 143 144 private final boolean mHasDistinctMultitouch; 145 private int mOldPointerCount = 1; 146 private int mOldKeyIndex; 147 148 protected KeyDetector mKeyDetector = new KeyDetector(); 149 150 // Swipe gesture detector 151 protected GestureDetector mGestureDetector; 152 private final SwipeTracker mSwipeTracker = new SwipeTracker(); 153 private final int mSwipeThreshold; 154 private final boolean mDisambiguateSwipe; 155 156 // Drawing 157 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ 158 private boolean mDrawPending; 159 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ 160 private boolean mKeyboardChanged; 161 /** The dirty region in the keyboard bitmap */ 162 private final Rect mDirtyRect = new Rect(); 163 /** The key to invalidate. */ 164 private Key mInvalidatedKey; 165 /** The dirty region for single key drawing */ 166 private final Rect mInvalidatedKeyRect = new Rect(); 167 /** The keyboard bitmap for faster updates */ 168 private Bitmap mBuffer; 169 /** The canvas for the above mutable keyboard bitmap */ 170 private Canvas mCanvas; 171 private final Paint mPaint; 172 private final Rect mPadding; 173 // This map caches key label text height in pixel as value and key label text size as map key. 174 private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>(); 175 // Distance from horizontal center of the key, proportional to key label text height and width. 176 private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f; 177 private final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f; 178 private final String KEY_LABEL_REFERENCE_CHAR = "H"; 179 private final int KEY_LABEL_OPTION_ALIGN_LEFT = 0x01; 180 private final int KEY_LABEL_OPTION_ALIGN_RIGHT = 0x02; 181 private final int KEY_LABEL_OPTION_ALIGN_BOTTOM = 0x08; 182 private final int KEY_LABEL_OPTION_FONT_NORMAL = 0x10; 183 private final int KEY_LABEL_OPTION_POPUP_HINT = 0x20; 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_SHOW_KEY_PREVIEW = 1; 190 private static final int MSG_DISMISS_KEY_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 private static final int MSG_IGNORE_DOUBLE_TAP = 6; 195 196 private boolean mInKeyRepeat; 197 198 @Override 199 public void handleMessage(Message msg) { 200 final PointerTracker tracker = (PointerTracker) msg.obj; 201 switch (msg.what) { 202 case MSG_SHOW_KEY_PREVIEW: 203 showKey(msg.arg1, tracker); 204 break; 205 case MSG_DISMISS_KEY_PREVIEW: 206 mPreviewText.setVisibility(View.INVISIBLE); 207 break; 208 case MSG_REPEAT_KEY: 209 tracker.onRepeatKey(msg.arg1); 210 startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker); 211 break; 212 case MSG_LONGPRESS_KEY: 213 openMiniKeyboardIfRequired(msg.arg1, tracker); 214 break; 215 case MSG_LONGPRESS_SHIFT_KEY: 216 onLongPressShiftKey(tracker); 217 break; 218 } 219 } 220 221 public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) { 222 removeMessages(MSG_SHOW_KEY_PREVIEW); 223 if (mPreviewText.getVisibility() == VISIBLE || delay == 0) { 224 // Show right away, if it's already visible and finger is moving around 225 showKey(keyIndex, tracker); 226 } else { 227 sendMessageDelayed( 228 obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay); 229 } 230 } 231 232 public void cancelShowKeyPreview(PointerTracker tracker) { 233 removeMessages(MSG_SHOW_KEY_PREVIEW, tracker); 234 } 235 236 public void cancelAllShowKeyPreviews() { 237 removeMessages(MSG_SHOW_KEY_PREVIEW); 238 } 239 240 public void dismissKeyPreview(long delay, PointerTracker tracker) { 241 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 242 } 243 244 public void cancelDismissKeyPreview(PointerTracker tracker) { 245 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 246 } 247 248 public void cancelAllDismissKeyPreviews() { 249 removeMessages(MSG_DISMISS_KEY_PREVIEW); 250 } 251 252 public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { 253 mInKeyRepeat = true; 254 sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); 255 } 256 257 public void cancelKeyRepeatTimer() { 258 mInKeyRepeat = false; 259 removeMessages(MSG_REPEAT_KEY); 260 } 261 262 public boolean isInKeyRepeat() { 263 return mInKeyRepeat; 264 } 265 266 public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { 267 cancelLongPressTimers(); 268 sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); 269 } 270 271 public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) { 272 cancelLongPressTimers(); 273 if (ENABLE_CAPSLOCK_BY_LONGPRESS) { 274 sendMessageDelayed( 275 obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay); 276 } 277 } 278 279 public void cancelLongPressTimers() { 280 removeMessages(MSG_LONGPRESS_KEY); 281 removeMessages(MSG_LONGPRESS_SHIFT_KEY); 282 } 283 284 public void cancelKeyTimers() { 285 cancelKeyRepeatTimer(); 286 cancelLongPressTimers(); 287 removeMessages(MSG_IGNORE_DOUBLE_TAP); 288 } 289 290 public void startIgnoringDoubleTap() { 291 sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP), 292 ViewConfiguration.getDoubleTapTimeout()); 293 } 294 295 public boolean isIgnoringDoubleTap() { 296 return hasMessages(MSG_IGNORE_DOUBLE_TAP); 297 } 298 299 public void cancelAllMessages() { 300 cancelKeyTimers(); 301 cancelAllShowKeyPreviews(); 302 cancelAllDismissKeyPreviews(); 303 } 304 } 305 306 public KeyboardView(Context context, AttributeSet attrs) { 307 this(context, attrs, R.attr.keyboardViewStyle); 308 } 309 310 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 311 super(context, attrs, defStyle); 312 313 final TypedArray a = context.obtainStyledAttributes( 314 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 315 316 mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); 317 mKeyHysteresisDistance = a.getDimensionPixelOffset( 318 R.styleable.KeyboardView_keyHysteresisDistance, 0); 319 mVerticalCorrection = a.getDimensionPixelOffset( 320 R.styleable.KeyboardView_verticalCorrection, 0); 321 final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); 322 mPreviewOffset = a.getDimensionPixelOffset(R.styleable.KeyboardView_keyPreviewOffset, 0); 323 mPreviewHeight = a.getDimensionPixelSize(R.styleable.KeyboardView_keyPreviewHeight, 80); 324 mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio); 325 mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000); 326 mKeyTextColorDisabled = a.getColor( 327 R.styleable.KeyboardView_keyTextColorDisabled, 0xFF000000); 328 mLabelTextRatio = getRatio(a, R.styleable.KeyboardView_labelTextRatio); 329 mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0); 330 mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0); 331 mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f); 332 // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) 333 mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f); 334 mKeyLetterStyle = Typeface.defaultFromStyle( 335 a.getInt(R.styleable.KeyboardView_keyLetterStyle, Typeface.NORMAL)); 336 mColorScheme = a.getInt(R.styleable.KeyboardView_colorScheme, COLOR_SCHEME_WHITE); 337 338 final Resources res = getResources(); 339 340 if (previewLayout != 0) { 341 mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null); 342 mPreviewTextRatio = getRatio(res, R.fraction.key_preview_text_ratio); 343 } else { 344 mShowKeyPreviewPopup = false; 345 } 346 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 347 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 348 mKeyLabelHorizontalPadding = (int)res.getDimension( 349 R.dimen.key_label_horizontal_alignment_padding); 350 351 mPaint = new Paint(); 352 mPaint.setAntiAlias(true); 353 mPaint.setTextAlign(Align.CENTER); 354 mPaint.setAlpha(255); 355 356 mPadding = new Rect(0, 0, 0, 0); 357 mKeyBackground.getPadding(mPadding); 358 359 mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); 360 // TODO: Refer to frameworks/base/core/res/res/values/config.xml 361 mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); 362 363 GestureDetector.SimpleOnGestureListener listener = 364 new GestureDetector.SimpleOnGestureListener() { 365 private boolean mProcessingShiftDoubleTapEvent = false; 366 367 @Override 368 public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, 369 float velocityY) { 370 final float absX = Math.abs(velocityX); 371 final float absY = Math.abs(velocityY); 372 float deltaY = me2.getY() - me1.getY(); 373 int travelY = getHeight() / 2; // Half the keyboard height 374 mSwipeTracker.computeCurrentVelocity(1000); 375 final float endingVelocityY = mSwipeTracker.getYVelocity(); 376 if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { 377 if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { 378 onSwipeDown(); 379 return true; 380 } 381 } 382 return false; 383 } 384 385 @Override 386 public boolean onDoubleTap(MotionEvent firstDown) { 387 if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard 388 && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) { 389 final int pointerIndex = firstDown.getActionIndex(); 390 final int id = firstDown.getPointerId(pointerIndex); 391 final PointerTracker tracker = getPointerTracker(id); 392 // If the first down event is on shift key. 393 if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) { 394 mProcessingShiftDoubleTapEvent = true; 395 return true; 396 } 397 } 398 mProcessingShiftDoubleTapEvent = false; 399 return false; 400 } 401 402 @Override 403 public boolean onDoubleTapEvent(MotionEvent secondTap) { 404 if (mProcessingShiftDoubleTapEvent 405 && secondTap.getAction() == MotionEvent.ACTION_DOWN) { 406 final MotionEvent secondDown = secondTap; 407 final int pointerIndex = secondDown.getActionIndex(); 408 final int id = secondDown.getPointerId(pointerIndex); 409 final PointerTracker tracker = getPointerTracker(id); 410 // If the second down event is also on shift key. 411 if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) { 412 // Detected a double tap on shift key. If we are in the ignoring double tap 413 // mode, it means we have already turned off caps lock in 414 // {@link KeyboardSwitcher#onReleaseShift} . 415 final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap(); 416 if (!ignoringDoubleTap) 417 onDoubleTapShiftKey(tracker); 418 return true; 419 } 420 // Otherwise these events should not be handled as double tap. 421 mProcessingShiftDoubleTapEvent = false; 422 } 423 return mProcessingShiftDoubleTapEvent; 424 } 425 }; 426 427 final boolean ignoreMultitouch = true; 428 mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); 429 mGestureDetector.setIsLongpressEnabled(false); 430 431 mHasDistinctMultitouch = context.getPackageManager() 432 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); 433 mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); 434 } 435 436 // Read fraction value in TypedArray as float. 437 private static float getRatio(TypedArray a, int index) { 438 return a.getFraction(index, 1000, 1000, 1) / 1000.0f; 439 } 440 441 // Read fraction value in resource as float. 442 private static float getRatio(Resources res, int id) { 443 return res.getFraction(id, 1000, 1000) / 1000.0f; 444 } 445 446 public void startIgnoringDoubleTap() { 447 if (ENABLE_CAPSLOCK_BY_DOUBLETAP) 448 mHandler.startIgnoringDoubleTap(); 449 } 450 451 public void setOnKeyboardActionListener(KeyboardActionListener listener) { 452 mKeyboardActionListener = listener; 453 for (PointerTracker tracker : mPointerTrackers) { 454 tracker.setOnKeyboardActionListener(listener); 455 } 456 } 457 458 /** 459 * Returns the {@link KeyboardActionListener} object. 460 * @return the listener attached to this keyboard 461 */ 462 protected KeyboardActionListener getOnKeyboardActionListener() { 463 return mKeyboardActionListener; 464 } 465 466 @Override 467 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 468 // TODO: Should notify InputMethodService instead? 469 KeyboardSwitcher.getInstance().onSizeChanged(); 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 dismissAllKeyPreviews(); 482 } 483 // Remove any pending messages, except dismissing preview 484 mHandler.cancelKeyTimers(); 485 mHandler.cancelAllShowKeyPreviews(); 486 mKeyboard = keyboard; 487 LatinImeLogger.onSetKeyboard(keyboard); 488 mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), 489 -getPaddingTop() + mVerticalCorrection); 490 for (PointerTracker tracker : mPointerTrackers) { 491 tracker.setKeyboard(keyboard, mKeyHysteresisDistance); 492 } 493 requestLayout(); 494 mKeyboardChanged = true; 495 invalidateAllKeys(); 496 mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth()); 497 mPopupPanelCache.clear(); 498 final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap(); 499 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 500 mLabelTextSize = (int)(keyHeight * mLabelTextRatio); 501 mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); 502 } 503 504 /** 505 * Returns the current keyboard being displayed by this view. 506 * @return the currently attached keyboard 507 * @see #setKeyboard(Keyboard) 508 */ 509 public Keyboard getKeyboard() { 510 return mKeyboard; 511 } 512 513 /** 514 * Returns whether the device has distinct multi-touch panel. 515 * @return true if the device has distinct multi-touch panel. 516 */ 517 @Override 518 public boolean hasDistinctMultitouch() { 519 return mHasDistinctMultitouch; 520 } 521 522 /** 523 * Enables or disables the key feedback popup. This is a popup that shows a magnified 524 * version of the depressed key. By default the preview is enabled. 525 * @param previewEnabled whether or not to enable the key feedback preview 526 * @param delay the delay after which the preview is dismissed 527 * @see #isKeyPreviewPopupEnabled() 528 */ 529 public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { 530 mShowKeyPreviewPopup = previewEnabled; 531 mDelayAfterPreview = delay; 532 } 533 534 /** 535 * Returns the enabled state of the key feedback preview 536 * @return whether or not the key feedback preview is enabled 537 * @see #setKeyPreviewPopupEnabled(boolean, int) 538 */ 539 public boolean isKeyPreviewPopupEnabled() { 540 return mShowKeyPreviewPopup; 541 } 542 543 public int getColorScheme() { 544 return mColorScheme; 545 } 546 547 /** 548 * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key 549 * codes for adjacent keys. When disabled, only the primary key code will be 550 * reported. 551 * @param enabled whether or not the proximity correction is enabled 552 */ 553 public void setProximityCorrectionEnabled(boolean enabled) { 554 mKeyDetector.setProximityCorrectionEnabled(enabled); 555 } 556 557 /** 558 * Returns true if proximity correction is enabled. 559 */ 560 public boolean isProximityCorrectionEnabled() { 561 return mKeyDetector.isProximityCorrectionEnabled(); 562 } 563 564 protected CharSequence adjustCase(CharSequence label) { 565 if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3 566 && Character.isLowerCase(label.charAt(0))) { 567 return label.toString().toUpperCase(); 568 } 569 return label; 570 } 571 572 @Override 573 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 574 // Round up a little 575 if (mKeyboard == null) { 576 setMeasuredDimension( 577 getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); 578 } else { 579 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 580 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 581 width = MeasureSpec.getSize(widthMeasureSpec); 582 } 583 setMeasuredDimension( 584 width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 585 } 586 } 587 588 @Override 589 public void onDraw(Canvas canvas) { 590 super.onDraw(canvas); 591 if (mDrawPending || mBuffer == null || mKeyboardChanged) { 592 onBufferDraw(); 593 } 594 canvas.drawBitmap(mBuffer, 0, 0, null); 595 } 596 597 private void onBufferDraw() { 598 final int width = getWidth(); 599 final int height = getHeight(); 600 if (width == 0 || height == 0) 601 return; 602 if (mBuffer == null || mKeyboardChanged) { 603 mKeyboardChanged = false; 604 mDirtyRect.union(0, 0, width, height); 605 } 606 if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { 607 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 608 mCanvas = new Canvas(mBuffer); 609 } 610 final Canvas canvas = mCanvas; 611 canvas.clipRect(mDirtyRect, Op.REPLACE); 612 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 613 614 if (mKeyboard == null) return; 615 616 if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) { 617 // Draw a single key. 618 onBufferDrawKey(canvas, mInvalidatedKey); 619 } else { 620 // Draw all keys. 621 for (final Key key : mKeyboard.getKeys()) { 622 onBufferDrawKey(canvas, key); 623 } 624 } 625 626 // TODO: Move this function to ProximityInfo for getting rid of 627 // public declarations for 628 // GRID_WIDTH and GRID_HEIGHT 629 if (DEBUG_KEYBOARD_GRID) { 630 Paint p = new Paint(); 631 p.setStyle(Paint.Style.STROKE); 632 p.setStrokeWidth(1.0f); 633 p.setColor(0x800000c0); 634 int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) 635 / mKeyboard.GRID_WIDTH; 636 int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) 637 / mKeyboard.GRID_HEIGHT; 638 for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) 639 canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); 640 for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) 641 canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); 642 } 643 644 // Overlay a dark rectangle to dim the keyboard 645 if (mPopupMiniKeyboardPanel != null) { 646 mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 647 canvas.drawRect(0, 0, width, height, mPaint); 648 } 649 650 mInvalidatedKey = null; 651 mDrawPending = false; 652 mDirtyRect.setEmpty(); 653 } 654 655 private void onBufferDrawKey(final Canvas canvas, final Key key) { 656 final Paint paint = mPaint; 657 final Drawable keyBackground = mKeyBackground; 658 final Rect padding = mPadding; 659 final int kbdPaddingLeft = getPaddingLeft(); 660 final int kbdPaddingTop = getPaddingTop(); 661 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 662 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 663 final int rowHeight = padding.top + key.mHeight; 664 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 665 666 canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop); 667 668 // Draw key background. 669 final int[] drawableState = key.getCurrentDrawableState(); 670 keyBackground.setState(drawableState); 671 final Rect bounds = keyBackground.getBounds(); 672 if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) { 673 keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight); 674 } 675 keyBackground.draw(canvas); 676 677 // Draw key label. 678 if (key.mLabel != null) { 679 // Switch the character to uppercase if shift is pressed 680 final String label = key.mLabel == null ? null : adjustCase(key.mLabel).toString(); 681 // For characters, use large font. For labels like "Done", use small font. 682 final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint); 683 final int labelCharHeight = getLabelCharHeight(labelSize, paint); 684 685 // Vertical label text alignment. 686 final float baseline; 687 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) { 688 baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; 689 if (DEBUG_SHOW_ALIGN) 690 drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, 691 new Paint()); 692 } else { // Align center 693 final float centerY = (key.mHeight + padding.top - padding.bottom) / 2; 694 baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; 695 if (DEBUG_SHOW_ALIGN) 696 drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, 697 new Paint()); 698 } 699 // Horizontal label text alignment 700 final int positionX; 701 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 702 positionX = mKeyLabelHorizontalPadding + padding.left; 703 paint.setTextAlign(Align.LEFT); 704 if (DEBUG_SHOW_ALIGN) 705 drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint()); 706 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 707 positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right; 708 paint.setTextAlign(Align.RIGHT); 709 if (DEBUG_SHOW_ALIGN) 710 drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint()); 711 } else { 712 positionX = (keyDrawWidth + padding.left - padding.right) / 2; 713 paint.setTextAlign(Align.CENTER); 714 if (DEBUG_SHOW_ALIGN) { 715 if (label.length() > 1) 716 drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint()); 717 } 718 } 719 if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) { 720 paint.setColor(mKeyTextColorDisabled); 721 } else { 722 paint.setColor(mKeyTextColor); 723 } 724 if (key.mEnabled) { 725 // Set a drop shadow for the text 726 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); 727 } else { 728 // Make label invisible 729 paint.setColor(Color.TRANSPARENT); 730 } 731 canvas.drawText(label, positionX, baseline, paint); 732 // Turn off drop shadow 733 paint.setShadowLayer(0, 0, 0, 0); 734 } 735 736 // Draw key icon. 737 final Drawable icon = key.getIcon(); 738 if (key.mLabel == null && icon != null) { 739 final int drawableWidth = icon.getIntrinsicWidth(); 740 final int drawableHeight = icon.getIntrinsicHeight(); 741 final int drawableX; 742 final int drawableY = (key.mHeight + padding.top - padding.bottom - drawableHeight) / 2; 743 if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { 744 drawableX = padding.left + mKeyLabelHorizontalPadding; 745 if (DEBUG_SHOW_ALIGN) 746 drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint()); 747 } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { 748 drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding 749 - drawableWidth; 750 if (DEBUG_SHOW_ALIGN) 751 drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight, 752 0xc0808000, new Paint()); 753 } else { // Align center 754 drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2; 755 if (DEBUG_SHOW_ALIGN) 756 drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight, 757 0xc0008080, new Paint()); 758 } 759 drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight); 760 if (DEBUG_SHOW_ALIGN) 761 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 762 0x80c00000, new Paint()); 763 } 764 765 // Draw hint icon. 766 if (key.mHintIcon != null || (key.mLabelOption & KEY_LABEL_OPTION_POPUP_HINT) != 0) { 767 final int drawableWidth = keyDrawWidth; 768 final int drawableHeight = key.mHeight; 769 final int drawableX = 0; 770 final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; 771 Drawable hintIcon = (isManualTemporaryUpperCase 772 && key.mManualTemporaryUpperCaseHintIcon != null) 773 ? key.mManualTemporaryUpperCaseHintIcon 774 : (key.mHintIcon != null ? key.mHintIcon : mKeyboard.mPopupHintIcon); 775 drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight); 776 if (DEBUG_SHOW_ALIGN) 777 drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, 778 0x80c0c000, new Paint()); 779 } 780 781 canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop); 782 } 783 784 public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) { 785 // For characters, use large font. For labels like "Done", use small font. 786 final int labelSize; 787 final Typeface labelStyle; 788 if (label.length() > 1) { 789 labelSize = mLabelTextSize; 790 if ((keyLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) { 791 labelStyle = Typeface.DEFAULT; 792 } else { 793 labelStyle = Typeface.DEFAULT_BOLD; 794 } 795 } else { 796 labelSize = mKeyLetterSize; 797 labelStyle = mKeyLetterStyle; 798 } 799 paint.setTextSize(labelSize); 800 paint.setTypeface(labelStyle); 801 return labelSize; 802 } 803 804 private int getLabelCharHeight(int labelSize, Paint paint) { 805 Integer labelHeightValue = mTextHeightCache.get(labelSize); 806 final int labelCharHeight; 807 if (labelHeightValue != null) { 808 labelCharHeight = labelHeightValue; 809 } else { 810 Rect textBounds = new Rect(); 811 paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds); 812 labelCharHeight = textBounds.height(); 813 mTextHeightCache.put(labelSize, labelCharHeight); 814 } 815 return labelCharHeight; 816 } 817 818 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 819 int height) { 820 canvas.translate(x, y); 821 icon.setBounds(0, 0, width, height); 822 icon.draw(canvas); 823 canvas.translate(-x, -y); 824 } 825 826 private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { 827 paint.setStyle(Paint.Style.STROKE); 828 paint.setStrokeWidth(1.0f); 829 paint.setColor(color); 830 canvas.drawLine(0, y, w, y, paint); 831 } 832 833 private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { 834 paint.setStyle(Paint.Style.STROKE); 835 paint.setStrokeWidth(1.0f); 836 paint.setColor(color); 837 canvas.drawLine(x, 0, x, h, paint); 838 } 839 840 private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, 841 Paint paint) { 842 paint.setStyle(Paint.Style.STROKE); 843 paint.setStrokeWidth(1.0f); 844 paint.setColor(color); 845 canvas.translate(x, y); 846 canvas.drawRect(0, 0, w, h, paint); 847 canvas.translate(-x, -y); 848 } 849 850 public void setForeground(boolean foreground) { 851 mInForeground = foreground; 852 } 853 854 // TODO: clean up this method. 855 private void dismissAllKeyPreviews() { 856 for (PointerTracker tracker : mPointerTrackers) { 857 tracker.setReleasedKeyGraphics(); 858 dismissKeyPreview(tracker); 859 } 860 } 861 862 @Override 863 public void showKeyPreview(int keyIndex, PointerTracker tracker) { 864 if (mShowKeyPreviewPopup) { 865 mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker); 866 } else if (mKeyboard.needSpacebarPreview(keyIndex)) { 867 // Show key preview (in this case, slide language switcher) without any delay. 868 showKey(keyIndex, tracker); 869 } 870 } 871 872 @Override 873 public void dismissKeyPreview(PointerTracker tracker) { 874 if (mShowKeyPreviewPopup) { 875 mHandler.cancelShowKeyPreview(tracker); 876 mHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 877 } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) { 878 // Dismiss key preview (in this case, slide language switcher) without any delay. 879 mPreviewText.setVisibility(View.INVISIBLE); 880 } 881 // Clear key preview display position. 882 mKeyPreviewPopupDisplayedY = -1; 883 } 884 885 private void addKeyPreview(TextView keyPreview) { 886 ViewGroup placer = mPreviewPlacer; 887 if (placer == null) { 888 final FrameLayout screenContent = (FrameLayout) getRootView().findViewById( 889 android.R.id.content); 890 if (android.os.Build.VERSION.SDK_INT >= /* HONEYCOMB */11) { 891 placer = screenContent; 892 } else { 893 // Insert LinearLayout to be able to setMargin because pre-Honeycomb FrameLayout 894 // could not handle setMargin properly. 895 placer = new LinearLayout(getContext()); 896 screenContent.addView(placer); 897 } 898 mPreviewPlacer = placer; 899 } 900 if (placer instanceof FrameLayout) { 901 // Honeycomb or later. 902 placer.addView(keyPreview, new FrameLayout.LayoutParams(0, 0)); 903 } else { 904 // Gingerbread or ealier. 905 placer.addView(keyPreview, new LinearLayout.LayoutParams(0, 0)); 906 } 907 } 908 909 // TODO: Introduce minimum duration for displaying key previews 910 // TODO: Display up to two key previews when the user presses two keys at the same time 911 private void showKey(final int keyIndex, PointerTracker tracker) { 912 final TextView previewText = mPreviewText; 913 // If the key preview has no parent view yet, add it to the ViewGroup which can place 914 // key preview absolutely in SoftInputWindow. 915 if (previewText.getParent() == null) { 916 addKeyPreview(previewText); 917 } 918 919 final Key key = tracker.getKey(keyIndex); 920 // If keyIndex is invalid or IME is already closed, we must not show key preview. 921 // Trying to show key preview while root window is closed causes 922 // WindowManager.BadTokenException. 923 if (key == null || !mInForeground) 924 return; 925 926 mHandler.cancelAllDismissKeyPreviews(); 927 928 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 929 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 930 // What we show as preview should match what we show on key top in onBufferDraw(). 931 if (key.mLabel != null) { 932 // TODO Should take care of temporaryShiftLabel here. 933 previewText.setCompoundDrawables(null, null, null, null); 934 previewText.setText(adjustCase(tracker.getPreviewText(key))); 935 if (key.mLabel.length() > 1) { 936 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); 937 previewText.setTypeface(Typeface.DEFAULT_BOLD); 938 } else { 939 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSize); 940 previewText.setTypeface(mKeyLetterStyle); 941 } 942 } else { 943 final Drawable previewIcon = key.getPreviewIcon(); 944 previewText.setCompoundDrawables(null, null, null, 945 previewIcon != null ? previewIcon : key.getIcon()); 946 previewText.setText(null); 947 } 948 // Set the preview background state 949 previewText.getBackground().setState( 950 key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 951 952 previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 953 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 954 final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth 955 + previewText.getPaddingLeft() + previewText.getPaddingRight()); 956 final int previewHeight = mPreviewHeight; 957 getLocationInWindow(mCoordinates); 958 final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0]; 959 final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset; 960 // Record key preview position to display mini-keyboard later at the same position 961 mKeyPreviewPopupDisplayedY = previewY; 962 963 // Place the key preview. 964 // TODO: Adjust position of key previews which touch screen edges 965 final MarginLayoutParams lp = (MarginLayoutParams)previewText.getLayoutParams(); 966 lp.width = previewWidth; 967 lp.height = previewHeight; 968 lp.setMargins(previewX, previewY, 0, 0); 969 previewText.setVisibility(VISIBLE); 970 } 971 972 /** 973 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 974 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 975 * draws the cached buffer. 976 * @see #invalidateKey(Key) 977 */ 978 public void invalidateAllKeys() { 979 mDirtyRect.union(0, 0, getWidth(), getHeight()); 980 mDrawPending = true; 981 invalidate(); 982 } 983 984 /** 985 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 986 * one key is changing it's content. Any changes that affect the position or size of the key 987 * may not be honored. 988 * @param key key in the attached {@link Keyboard}. 989 * @see #invalidateAllKeys 990 */ 991 @Override 992 public void invalidateKey(Key key) { 993 if (key == null) 994 return; 995 mInvalidatedKey = key; 996 final int x = key.mX + getPaddingLeft(); 997 final int y = key.mY + getPaddingTop(); 998 mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight); 999 mDirtyRect.union(mInvalidatedKeyRect); 1000 onBufferDraw(); 1001 invalidate(mInvalidatedKeyRect); 1002 } 1003 1004 private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) { 1005 // Check if we have a popup layout specified first. 1006 if (mPopupLayout == 0) { 1007 return false; 1008 } 1009 1010 final Key parentKey = tracker.getKey(keyIndex); 1011 if (parentKey == null) 1012 return false; 1013 boolean result = onLongPress(parentKey, tracker); 1014 if (result) { 1015 dismissAllKeyPreviews(); 1016 tracker.onLongPressed(mPointerQueue); 1017 } 1018 return result; 1019 } 1020 1021 private void onLongPressShiftKey(PointerTracker tracker) { 1022 tracker.onLongPressed(mPointerQueue); 1023 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1024 } 1025 1026 private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { 1027 // When shift key is double tapped, the first tap is correctly processed as usual tap. And 1028 // the second tap is treated as this double tap event, so that we need not mark tracker 1029 // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. 1030 mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); 1031 } 1032 1033 // This default implementation returns a popup mini keyboard panel. 1034 // A derived class may return a language switcher popup panel, for instance. 1035 protected PopupPanel onCreatePopupPanel(Key parentKey) { 1036 if (parentKey.mPopupCharacters == null) 1037 return null; 1038 1039 final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); 1040 if (container == null) 1041 throw new NullPointerException(); 1042 1043 final PopupMiniKeyboardView miniKeyboardView = 1044 (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view); 1045 miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { 1046 @Override 1047 public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { 1048 mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); 1049 dismissMiniKeyboard(); 1050 } 1051 1052 @Override 1053 public void onTextInput(CharSequence text) { 1054 mKeyboardActionListener.onTextInput(text); 1055 dismissMiniKeyboard(); 1056 } 1057 1058 @Override 1059 public void onCancelInput() { 1060 mKeyboardActionListener.onCancelInput(); 1061 dismissMiniKeyboard(); 1062 } 1063 1064 @Override 1065 public void onSwipeDown() { 1066 // Nothing to do. 1067 } 1068 @Override 1069 public void onPress(int primaryCode, boolean withSliding) { 1070 mKeyboardActionListener.onPress(primaryCode, withSliding); 1071 } 1072 @Override 1073 public void onRelease(int primaryCode, boolean withSliding) { 1074 mKeyboardActionListener.onRelease(primaryCode, withSliding); 1075 } 1076 }); 1077 1078 final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(), 1079 parentKey, mKeyboard).build(); 1080 miniKeyboardView.setKeyboard(keyboard); 1081 1082 container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), 1083 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); 1084 1085 return miniKeyboardView; 1086 } 1087 1088 /** 1089 * Called when a key is long pressed. By default this will open mini keyboard associated 1090 * with this key. 1091 * @param parentKey the key that was long pressed 1092 * @param tracker the pointer tracker which pressed the parent key 1093 * @return true if the long press is handled, false otherwise. Subclasses should call the 1094 * method on the base class if the subclass doesn't wish to handle the call. 1095 */ 1096 protected boolean onLongPress(Key parentKey, PointerTracker tracker) { 1097 PopupPanel popupPanel = mPopupPanelCache.get(parentKey); 1098 if (popupPanel == null) { 1099 popupPanel = onCreatePopupPanel(parentKey); 1100 if (popupPanel == null) 1101 return false; 1102 mPopupPanelCache.put(parentKey, popupPanel); 1103 } 1104 if (mPopupWindow == null) { 1105 mPopupWindow = new PopupWindow(getContext()); 1106 mPopupWindow.setBackgroundDrawable(null); 1107 mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation); 1108 // Allow popup window to be drawn off the screen. 1109 mPopupWindow.setClippingEnabled(false); 1110 } 1111 mPopupMiniKeyboardPanel = popupPanel; 1112 popupPanel.showPanel(this, parentKey, tracker, mKeyPreviewPopupDisplayedY, mPopupWindow); 1113 1114 invalidateAllKeys(); 1115 return true; 1116 } 1117 1118 private PointerTracker getPointerTracker(final int id) { 1119 final ArrayList<PointerTracker> pointers = mPointerTrackers; 1120 final KeyboardActionListener listener = mKeyboardActionListener; 1121 1122 // Create pointer trackers until we can get 'id+1'-th tracker, if needed. 1123 for (int i = pointers.size(); i <= id; i++) { 1124 final PointerTracker tracker = 1125 new PointerTracker(i, this, mHandler, mKeyDetector, this); 1126 if (mKeyboard != null) 1127 tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance); 1128 if (listener != null) 1129 tracker.setOnKeyboardActionListener(listener); 1130 pointers.add(tracker); 1131 } 1132 1133 return pointers.get(id); 1134 } 1135 1136 public boolean isInSlidingKeyInput() { 1137 if (mPopupMiniKeyboardPanel != null) { 1138 return mPopupMiniKeyboardPanel.isInSlidingKeyInput(); 1139 } else { 1140 return mPointerQueue.isInSlidingKeyInput(); 1141 } 1142 } 1143 1144 public int getPointerCount() { 1145 return mOldPointerCount; 1146 } 1147 1148 @Override 1149 public boolean onTouchEvent(MotionEvent me) { 1150 final int action = me.getActionMasked(); 1151 final int pointerCount = me.getPointerCount(); 1152 final int oldPointerCount = mOldPointerCount; 1153 mOldPointerCount = pointerCount; 1154 1155 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1156 // If the device does not have distinct multi-touch support panel, ignore all multi-touch 1157 // events except a transition from/to single-touch. 1158 if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { 1159 return true; 1160 } 1161 1162 // Track the last few movements to look for spurious swipes. 1163 mSwipeTracker.addMovement(me); 1164 1165 // Gesture detector must be enabled only when mini-keyboard is not on the screen. 1166 if (mPopupMiniKeyboardPanel == null && mGestureDetector != null 1167 && mGestureDetector.onTouchEvent(me)) { 1168 dismissAllKeyPreviews(); 1169 mHandler.cancelKeyTimers(); 1170 return true; 1171 } 1172 1173 final long eventTime = me.getEventTime(); 1174 final int index = me.getActionIndex(); 1175 final int id = me.getPointerId(index); 1176 final int x = (int)me.getX(index); 1177 final int y = (int)me.getY(index); 1178 1179 // Needs to be called after the gesture detector gets a turn, as it may have displayed the 1180 // mini keyboard 1181 if (mPopupMiniKeyboardPanel != null) { 1182 return mPopupMiniKeyboardPanel.onTouchEvent(me); 1183 } 1184 1185 if (mHandler.isInKeyRepeat()) { 1186 final PointerTracker tracker = getPointerTracker(id); 1187 // Key repeating timer will be canceled if 2 or more keys are in action, and current 1188 // event (UP or DOWN) is non-modifier key. 1189 if (pointerCount > 1 && !tracker.isModifier()) { 1190 mHandler.cancelKeyRepeatTimer(); 1191 } 1192 // Up event will pass through. 1193 } 1194 1195 // TODO: cleanup this code into a multi-touch to single-touch event converter class? 1196 // Translate mutli-touch event to single-touch events on the device that has no distinct 1197 // multi-touch panel. 1198 if (!mHasDistinctMultitouch) { 1199 // Use only main (id=0) pointer tracker. 1200 PointerTracker tracker = getPointerTracker(0); 1201 if (pointerCount == 1 && oldPointerCount == 2) { 1202 // Multi-touch to single touch transition. 1203 // Send a down event for the latest pointer if the key is different from the 1204 // previous key. 1205 final int newKeyIndex = tracker.getKeyIndexOn(x, y); 1206 if (mOldKeyIndex != newKeyIndex) { 1207 tracker.onDownEvent(x, y, eventTime, null); 1208 if (action == MotionEvent.ACTION_UP) 1209 tracker.onUpEvent(x, y, eventTime, null); 1210 } 1211 } else if (pointerCount == 2 && oldPointerCount == 1) { 1212 // Single-touch to multi-touch transition. 1213 // Send an up event for the last pointer. 1214 final int lastX = tracker.getLastX(); 1215 final int lastY = tracker.getLastY(); 1216 mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY); 1217 tracker.onUpEvent(lastX, lastY, eventTime, null); 1218 } else if (pointerCount == 1 && oldPointerCount == 1) { 1219 tracker.onTouchEvent(action, x, y, eventTime, null); 1220 } else { 1221 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount 1222 + " (old " + oldPointerCount + ")"); 1223 } 1224 return true; 1225 } 1226 1227 final PointerTrackerQueue queue = mPointerQueue; 1228 if (action == MotionEvent.ACTION_MOVE) { 1229 for (int i = 0; i < pointerCount; i++) { 1230 final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); 1231 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); 1232 } 1233 } else { 1234 final PointerTracker tracker = getPointerTracker(id); 1235 switch (action) { 1236 case MotionEvent.ACTION_DOWN: 1237 case MotionEvent.ACTION_POINTER_DOWN: 1238 tracker.onDownEvent(x, y, eventTime, queue); 1239 break; 1240 case MotionEvent.ACTION_UP: 1241 case MotionEvent.ACTION_POINTER_UP: 1242 tracker.onUpEvent(x, y, eventTime, queue); 1243 break; 1244 case MotionEvent.ACTION_CANCEL: 1245 tracker.onCancelEvent(x, y, eventTime, queue); 1246 break; 1247 } 1248 } 1249 1250 return true; 1251 } 1252 1253 protected void onSwipeDown() { 1254 mKeyboardActionListener.onSwipeDown(); 1255 } 1256 1257 public void closing() { 1258 mPreviewText.setVisibility(View.GONE); 1259 mHandler.cancelAllMessages(); 1260 1261 dismissMiniKeyboard(); 1262 mDirtyRect.union(0, 0, getWidth(), getHeight()); 1263 mPopupPanelCache.clear(); 1264 requestLayout(); 1265 } 1266 1267 public void purgeKeyboardAndClosing() { 1268 mKeyboard = null; 1269 closing(); 1270 } 1271 1272 @Override 1273 public void onDetachedFromWindow() { 1274 super.onDetachedFromWindow(); 1275 closing(); 1276 } 1277 1278 private boolean dismissMiniKeyboard() { 1279 if (mPopupWindow != null && mPopupWindow.isShowing()) { 1280 mPopupWindow.dismiss(); 1281 mPopupMiniKeyboardPanel = null; 1282 invalidateAllKeys(); 1283 return true; 1284 } 1285 return false; 1286 } 1287 1288 public boolean handleBack() { 1289 return dismissMiniKeyboard(); 1290 } 1291} 1292