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