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