blob: 4b484092d02e84dd4c9d4ba4dfd2ccabd6049eb7 [file] [log] [blame]
satokc9e1a332010-06-18 03:02:00 +09001/*
2 * Copyright (C) 2008-2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.inputmethodservice;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.Paint.Align;
28import android.graphics.Region.Op;
29import android.graphics.drawable.Drawable;
30import android.inputmethodservice.Keyboard.Key;
31import android.os.Handler;
32import android.os.Message;
33import android.util.AttributeSet;
34import android.util.TypedValue;
35import android.view.GestureDetector;
36import android.view.Gravity;
37import android.view.LayoutInflater;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewConfiguration;
41import android.view.ViewGroup.LayoutParams;
42import android.widget.PopupWindow;
43import android.widget.TextView;
44
45import com.android.internal.R;
46
47import java.util.Arrays;
48import java.util.HashMap;
49import java.util.List;
50import java.util.Map;
51
52/**
53 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and
54 * detecting key presses and touch movements.
55 *
56 * @attr ref android.R.styleable#KeyboardView_keyBackground
57 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
58 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
59 * @attr ref android.R.styleable#KeyboardView_labelTextSize
60 * @attr ref android.R.styleable#KeyboardView_keyTextSize
61 * @attr ref android.R.styleable#KeyboardView_keyTextColor
62 * @attr ref android.R.styleable#KeyboardView_verticalCorrection
63 * @attr ref android.R.styleable#KeyboardView_popupLayout
64 */
65public class KeyboardView extends View implements View.OnClickListener {
66
67 /**
68 * Listener for virtual keyboard events.
69 */
70 public interface OnKeyboardActionListener {
71
72 /**
73 * Called when the user presses a key. This is sent before the {@link #onKey} is called.
74 * For keys that repeat, this is only called once.
75 * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
76 * key, the value will be zero.
77 */
78 void onPress(int primaryCode);
79
80 /**
81 * Called when the user releases a key. This is sent after the {@link #onKey} is called.
82 * For keys that repeat, this is only called once.
83 * @param primaryCode the code of the key that was released
84 */
85 void onRelease(int primaryCode);
86
87 /**
88 * Send a key press to the listener.
89 * @param primaryCode this is the key that was pressed
90 * @param keyCodes the codes for all the possible alternative keys
91 * with the primary code being the first. If the primary key code is
92 * a single character such as an alphabet or number or symbol, the alternatives
93 * will include other characters that may be on the same key or adjacent keys.
94 * These codes are useful to correct for accidental presses of a key adjacent to
95 * the intended key.
96 */
97 void onKey(int primaryCode, int[] keyCodes);
98
99 /**
100 * Sends a sequence of characters to the listener.
101 * @param text the sequence of characters to be displayed.
102 */
103 void onText(CharSequence text);
104
105 /**
106 * Called when the user quickly moves the finger from right to left.
107 */
108 void swipeLeft();
109
110 /**
111 * Called when the user quickly moves the finger from left to right.
112 */
113 void swipeRight();
114
115 /**
116 * Called when the user quickly moves the finger from up to down.
117 */
118 void swipeDown();
119
120 /**
121 * Called when the user quickly moves the finger from down to up.
122 */
123 void swipeUp();
124 }
125
126 private static final boolean DEBUG = false;
127 private static final int NOT_A_KEY = -1;
128 private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
129 private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
130
131 private Keyboard mKeyboard;
132 private int mCurrentKeyIndex = NOT_A_KEY;
133 private int mLabelTextSize;
134 private int mKeyTextSize;
135 private int mKeyTextColor;
136 private float mShadowRadius;
137 private int mShadowColor;
138 private float mBackgroundDimAmount;
139
140 private TextView mPreviewText;
141 private PopupWindow mPreviewPopup;
142 private int mPreviewTextSizeLarge;
143 private int mPreviewOffset;
144 private int mPreviewHeight;
145 private int[] mOffsetInWindow;
146
147 private PopupWindow mPopupKeyboard;
148 private View mMiniKeyboardContainer;
149 private KeyboardView mMiniKeyboard;
150 private boolean mMiniKeyboardOnScreen;
151 private View mPopupParent;
152 private int mMiniKeyboardOffsetX;
153 private int mMiniKeyboardOffsetY;
154 private Map<Key,View> mMiniKeyboardCache;
155 private int[] mWindowOffset;
156 private Key[] mKeys;
157
158 /** Listener for {@link OnKeyboardActionListener}. */
159 private OnKeyboardActionListener mKeyboardActionListener;
160
161 private static final int MSG_SHOW_PREVIEW = 1;
162 private static final int MSG_REMOVE_PREVIEW = 2;
163 private static final int MSG_REPEAT = 3;
164 private static final int MSG_LONGPRESS = 4;
165
166 private static final int DELAY_BEFORE_PREVIEW = 0;
167 private static final int DELAY_AFTER_PREVIEW = 70;
168 private static final int DEBOUNCE_TIME = 70;
169
170 private int mVerticalCorrection;
171 private int mProximityThreshold;
172
173 private boolean mPreviewCentered = false;
174 private boolean mShowPreview = true;
175 private boolean mShowTouchPoints = true;
176 private int mPopupPreviewX;
177 private int mPopupPreviewY;
178 private int mWindowY;
179
180 private int mLastX;
181 private int mLastY;
182 private int mStartX;
183 private int mStartY;
184
185 private boolean mProximityCorrectOn;
186
187 private Paint mPaint;
188 private Rect mPadding;
189
190 private long mDownTime;
191 private long mLastMoveTime;
192 private int mLastKey;
193 private int mLastCodeX;
194 private int mLastCodeY;
195 private int mCurrentKey = NOT_A_KEY;
196 private int mDownKey = NOT_A_KEY;
197 private long mLastKeyTime;
198 private long mCurrentKeyTime;
199 private int[] mKeyIndices = new int[12];
200 private GestureDetector mGestureDetector;
201 private int mPopupX;
202 private int mPopupY;
203 private int mRepeatKeyIndex = NOT_A_KEY;
204 private int mPopupLayout;
205 private boolean mAbortKey;
206 private Key mInvalidatedKey;
207 private Rect mClipRegion = new Rect(0, 0, 0, 0);
208 private boolean mPossiblePoly;
209 private SwipeTracker mSwipeTracker = new SwipeTracker();
210 private int mSwipeThreshold;
211 private boolean mDisambiguateSwipe;
212
213 // Variables for dealing with multiple pointers
214 private int mOldPointerCount = 1;
215 private float mOldPointerX;
216 private float mOldPointerY;
217
218 private Drawable mKeyBackground;
219
220 private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
221 private static final int REPEAT_START_DELAY = 400;
222 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
223
224 private static int MAX_NEARBY_KEYS = 12;
225 private int[] mDistances = new int[MAX_NEARBY_KEYS];
226
227 // For multi-tap
228 private int mLastSentIndex;
229 private int mTapCount;
230 private long mLastTapTime;
231 private boolean mInMultiTap;
232 private static final int MULTITAP_INTERVAL = 800; // milliseconds
233 private StringBuilder mPreviewLabel = new StringBuilder(1);
234
235 /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
236 private boolean mDrawPending;
237 /** The dirty region in the keyboard bitmap */
238 private Rect mDirtyRect = new Rect();
239 /** The keyboard bitmap for faster updates */
240 private Bitmap mBuffer;
241 /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
242 private boolean mKeyboardChanged;
243 /** The canvas for the above mutable keyboard bitmap */
244 private Canvas mCanvas;
245
246 Handler mHandler = new Handler() {
247 @Override
248 public void handleMessage(Message msg) {
249 switch (msg.what) {
250 case MSG_SHOW_PREVIEW:
251 showKey(msg.arg1);
252 break;
253 case MSG_REMOVE_PREVIEW:
254 mPreviewText.setVisibility(INVISIBLE);
255 break;
256 case MSG_REPEAT:
257 if (repeatKey()) {
258 Message repeat = Message.obtain(this, MSG_REPEAT);
259 sendMessageDelayed(repeat, REPEAT_INTERVAL);
260 }
261 break;
262 case MSG_LONGPRESS:
263 openPopupIfRequired((MotionEvent) msg.obj);
264 break;
265 }
266 }
267 };
268
269 public KeyboardView(Context context, AttributeSet attrs) {
270 this(context, attrs, com.android.internal.R.attr.keyboardViewStyle);
271 }
272
273 public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
274 super(context, attrs, defStyle);
275
276 TypedArray a =
277 context.obtainStyledAttributes(
278 attrs, android.R.styleable.KeyboardView, defStyle, 0);
279
280 LayoutInflater inflate =
281 (LayoutInflater) context
282 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
283
284 int previewLayout = 0;
285 int keyTextSize = 0;
286
287 int n = a.getIndexCount();
288
289 for (int i = 0; i < n; i++) {
290 int attr = a.getIndex(i);
291
292 switch (attr) {
293 case com.android.internal.R.styleable.KeyboardView_keyBackground:
294 mKeyBackground = a.getDrawable(attr);
295 break;
296 case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
297 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
298 break;
299 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
300 previewLayout = a.getResourceId(attr, 0);
301 break;
302 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
303 mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
304 break;
305 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
306 mPreviewHeight = a.getDimensionPixelSize(attr, 80);
307 break;
308 case com.android.internal.R.styleable.KeyboardView_keyTextSize:
309 mKeyTextSize = a.getDimensionPixelSize(attr, 18);
310 break;
311 case com.android.internal.R.styleable.KeyboardView_keyTextColor:
312 mKeyTextColor = a.getColor(attr, 0xFF000000);
313 break;
314 case com.android.internal.R.styleable.KeyboardView_labelTextSize:
315 mLabelTextSize = a.getDimensionPixelSize(attr, 14);
316 break;
317 case com.android.internal.R.styleable.KeyboardView_popupLayout:
318 mPopupLayout = a.getResourceId(attr, 0);
319 break;
320 case com.android.internal.R.styleable.KeyboardView_shadowColor:
321 mShadowColor = a.getColor(attr, 0);
322 break;
323 case com.android.internal.R.styleable.KeyboardView_shadowRadius:
324 mShadowRadius = a.getFloat(attr, 0f);
325 break;
326 }
327 }
328
329 a = mContext.obtainStyledAttributes(
330 com.android.internal.R.styleable.Theme);
331 mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
332
333 mPreviewPopup = new PopupWindow(context);
334 if (previewLayout != 0) {
335 mPreviewText = (TextView) inflate.inflate(previewLayout, null);
336 mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
337 mPreviewPopup.setContentView(mPreviewText);
338 mPreviewPopup.setBackgroundDrawable(null);
339 } else {
340 mShowPreview = false;
341 }
342
343 mPreviewPopup.setTouchable(false);
344
345 mPopupKeyboard = new PopupWindow(context);
346 mPopupKeyboard.setBackgroundDrawable(null);
347 //mPopupKeyboard.setClippingEnabled(false);
348
349 mPopupParent = this;
350 //mPredicting = true;
351
352 mPaint = new Paint();
353 mPaint.setAntiAlias(true);
354 mPaint.setTextSize(keyTextSize);
355 mPaint.setTextAlign(Align.CENTER);
356 mPaint.setAlpha(255);
357
358 mPadding = new Rect(0, 0, 0, 0);
359 mMiniKeyboardCache = new HashMap<Key,View>();
360 mKeyBackground.getPadding(mPadding);
361
362 mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
363 mDisambiguateSwipe = getResources().getBoolean(
364 com.android.internal.R.bool.config_swipeDisambiguation);
365 resetMultiTap();
366 initGestureDetector();
367 }
368
369 private void initGestureDetector() {
370 mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
371 @Override
372 public boolean onFling(MotionEvent me1, MotionEvent me2,
373 float velocityX, float velocityY) {
374 if (mPossiblePoly) return false;
375 final float absX = Math.abs(velocityX);
376 final float absY = Math.abs(velocityY);
377 float deltaX = me2.getX() - me1.getX();
378 float deltaY = me2.getY() - me1.getY();
379 int travelX = getWidth() / 2; // Half the keyboard width
380 int travelY = getHeight() / 2; // Half the keyboard height
381 mSwipeTracker.computeCurrentVelocity(1000);
382 final float endingVelocityX = mSwipeTracker.getXVelocity();
383 final float endingVelocityY = mSwipeTracker.getYVelocity();
384 boolean sendDownKey = false;
385 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
386 if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) {
387 sendDownKey = true;
388 } else {
389 swipeRight();
390 return true;
391 }
392 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
393 if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) {
394 sendDownKey = true;
395 } else {
396 swipeLeft();
397 return true;
398 }
399 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
400 if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) {
401 sendDownKey = true;
402 } else {
403 swipeUp();
404 return true;
405 }
406 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
407 if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) {
408 sendDownKey = true;
409 } else {
410 swipeDown();
411 return true;
412 }
413 }
414
415 if (sendDownKey) {
416 detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime());
417 }
418 return false;
419 }
420 });
421
422 mGestureDetector.setIsLongpressEnabled(false);
423 }
424
425 public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
426 mKeyboardActionListener = listener;
427 }
428
429 /**
430 * Returns the {@link OnKeyboardActionListener} object.
431 * @return the listener attached to this keyboard
432 */
433 protected OnKeyboardActionListener getOnKeyboardActionListener() {
434 return mKeyboardActionListener;
435 }
436
437 /**
438 * Attaches a keyboard to this view. The keyboard can be switched at any time and the
439 * view will re-layout itself to accommodate the keyboard.
440 * @see Keyboard
441 * @see #getKeyboard()
442 * @param keyboard the keyboard to display in this view
443 */
444 public void setKeyboard(Keyboard keyboard) {
445 if (mKeyboard != null) {
446 showPreview(NOT_A_KEY);
447 }
448 // Remove any pending messages
449 removeMessages();
450 mKeyboard = keyboard;
451 List<Key> keys = mKeyboard.getKeys();
452 mKeys = keys.toArray(new Key[keys.size()]);
453 requestLayout();
454 // Hint to reallocate the buffer if the size changed
455 mKeyboardChanged = true;
456 invalidateAllKeys();
457 computeProximityThreshold(keyboard);
458 mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
459 // Switching to a different keyboard should abort any pending keys so that the key up
460 // doesn't get delivered to the old or new keyboard
461 mAbortKey = true; // Until the next ACTION_DOWN
462 }
463
464 /**
465 * Returns the current keyboard being displayed by this view.
466 * @return the currently attached keyboard
467 * @see #setKeyboard(Keyboard)
468 */
469 public Keyboard getKeyboard() {
470 return mKeyboard;
471 }
472
473 /**
474 * Sets the state of the shift key of the keyboard, if any.
475 * @param shifted whether or not to enable the state of the shift key
476 * @return true if the shift key state changed, false if there was no change
477 * @see KeyboardView#isShifted()
478 */
479 public boolean setShifted(boolean shifted) {
480 if (mKeyboard != null) {
481 if (mKeyboard.setShifted(shifted)) {
482 // The whole keyboard probably needs to be redrawn
483 invalidateAllKeys();
484 return true;
485 }
486 }
487 return false;
488 }
489
490 /**
491 * Returns the state of the shift key of the keyboard, if any.
492 * @return true if the shift is in a pressed state, false otherwise. If there is
493 * no shift key on the keyboard or there is no keyboard attached, it returns false.
494 * @see KeyboardView#setShifted(boolean)
495 */
496 public boolean isShifted() {
497 if (mKeyboard != null) {
498 return mKeyboard.isShifted();
499 }
500 return false;
501 }
502
503 /**
504 * Enables or disables the key feedback popup. This is a popup that shows a magnified
505 * version of the depressed key. By default the preview is enabled.
506 * @param previewEnabled whether or not to enable the key feedback popup
507 * @see #isPreviewEnabled()
508 */
509 public void setPreviewEnabled(boolean previewEnabled) {
510 mShowPreview = previewEnabled;
511 }
512
513 /**
514 * Returns the enabled state of the key feedback popup.
515 * @return whether or not the key feedback popup is enabled
516 * @see #setPreviewEnabled(boolean)
517 */
518 public boolean isPreviewEnabled() {
519 return mShowPreview;
520 }
521
522 public void setVerticalCorrection(int verticalOffset) {
523
524 }
525 public void setPopupParent(View v) {
526 mPopupParent = v;
527 }
528
529 public void setPopupOffset(int x, int y) {
530 mMiniKeyboardOffsetX = x;
531 mMiniKeyboardOffsetY = y;
532 if (mPreviewPopup.isShowing()) {
533 mPreviewPopup.dismiss();
534 }
535 }
536
537 /**
538 * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
539 * codes for adjacent keys. When disabled, only the primary key code will be
540 * reported.
541 * @param enabled whether or not the proximity correction is enabled
542 */
543 public void setProximityCorrectionEnabled(boolean enabled) {
544 mProximityCorrectOn = enabled;
545 }
546
547 /**
548 * Returns true if proximity correction is enabled.
549 */
550 public boolean isProximityCorrectionEnabled() {
551 return mProximityCorrectOn;
552 }
553
554 /**
555 * Popup keyboard close button clicked.
556 * @hide
557 */
558 public void onClick(View v) {
559 dismissPopupKeyboard();
560 }
561
562 private CharSequence adjustCase(CharSequence label) {
563 if (mKeyboard.isShifted() && label != null && label.length() < 3
564 && Character.isLowerCase(label.charAt(0))) {
565 label = label.toString().toUpperCase();
566 }
567 return label;
568 }
569
570 @Override
571 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
572 // Round up a little
573 if (mKeyboard == null) {
574 setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom);
575 } else {
576 int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight;
577 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
578 width = MeasureSpec.getSize(widthMeasureSpec);
579 }
580 setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom);
581 }
582 }
583
584 /**
585 * Compute the average distance between adjacent keys (horizontally and vertically)
586 * and square it to get the proximity threshold. We use a square here and in computing
587 * the touch distance from a key's center to avoid taking a square root.
588 * @param keyboard
589 */
590 private void computeProximityThreshold(Keyboard keyboard) {
591 if (keyboard == null) return;
592 final Key[] keys = mKeys;
593 if (keys == null) return;
594 int length = keys.length;
595 int dimensionSum = 0;
596 for (int i = 0; i < length; i++) {
597 Key key = keys[i];
598 dimensionSum += Math.min(key.width, key.height) + key.gap;
599 }
600 if (dimensionSum < 0 || length == 0) return;
601 mProximityThreshold = (int) (dimensionSum * 1.4f / length);
602 mProximityThreshold *= mProximityThreshold; // Square it
603 }
604
605 @Override
606 public void onSizeChanged(int w, int h, int oldw, int oldh) {
607 super.onSizeChanged(w, h, oldw, oldh);
608 // Release the buffer, if any and it will be reallocated on the next draw
609 mBuffer = null;
610 }
611
612 @Override
613 public void onDraw(Canvas canvas) {
614 super.onDraw(canvas);
615 if (mDrawPending || mBuffer == null || mKeyboardChanged) {
616 onBufferDraw();
617 }
618 canvas.drawBitmap(mBuffer, 0, 0, null);
619 }
620
621 private void onBufferDraw() {
622 if (mBuffer == null || mKeyboardChanged) {
623 if (mBuffer == null || mKeyboardChanged &&
624 (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
625 // Make sure our bitmap is at least 1x1
626 final int width = Math.max(1, getWidth());
627 final int height = Math.max(1, getHeight());
628 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
629 mCanvas = new Canvas(mBuffer);
630 }
631 invalidateAllKeys();
632 mKeyboardChanged = false;
633 }
634 final Canvas canvas = mCanvas;
635 canvas.clipRect(mDirtyRect, Op.REPLACE);
636
637 if (mKeyboard == null) return;
638
639 final Paint paint = mPaint;
640 final Drawable keyBackground = mKeyBackground;
641 final Rect clipRegion = mClipRegion;
642 final Rect padding = mPadding;
643 final int kbdPaddingLeft = mPaddingLeft;
644 final int kbdPaddingTop = mPaddingTop;
645 final Key[] keys = mKeys;
646 final Key invalidKey = mInvalidatedKey;
647
648 paint.setColor(mKeyTextColor);
649 boolean drawSingleKey = false;
650 if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
651 // Is clipRegion completely contained within the invalidated key?
652 if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
653 invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
654 invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
655 invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
656 drawSingleKey = true;
657 }
658 }
659 canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
660 final int keyCount = keys.length;
661 for (int i = 0; i < keyCount; i++) {
662 final Key key = keys[i];
663 if (drawSingleKey && invalidKey != key) {
664 continue;
665 }
666 int[] drawableState = key.getCurrentDrawableState();
667 keyBackground.setState(drawableState);
668
669 // Switch the character to uppercase if shift is pressed
670 String label = key.label == null? null : adjustCase(key.label).toString();
671
672 final Rect bounds = keyBackground.getBounds();
673 if (key.width != bounds.right ||
674 key.height != bounds.bottom) {
675 keyBackground.setBounds(0, 0, key.width, key.height);
676 }
677 canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
678 keyBackground.draw(canvas);
679
680 if (label != null) {
681 // For characters, use large font. For labels like "Done", use small font.
682 if (label.length() > 1 && key.codes.length < 2) {
683 paint.setTextSize(mLabelTextSize);
684 paint.setTypeface(Typeface.DEFAULT_BOLD);
685 } else {
686 paint.setTextSize(mKeyTextSize);
687 paint.setTypeface(Typeface.DEFAULT);
688 }
689 // Draw a drop shadow for the text
690 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
691 // Draw the text
692 canvas.drawText(label,
693 (key.width - padding.left - padding.right) / 2
694 + padding.left,
695 (key.height - padding.top - padding.bottom) / 2
696 + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
697 paint);
698 // Turn off drop shadow
699 paint.setShadowLayer(0, 0, 0, 0);
700 } else if (key.icon != null) {
701 final int drawableX = (key.width - padding.left - padding.right
702 - key.icon.getIntrinsicWidth()) / 2 + padding.left;
703 final int drawableY = (key.height - padding.top - padding.bottom
704 - key.icon.getIntrinsicHeight()) / 2 + padding.top;
705 canvas.translate(drawableX, drawableY);
706 key.icon.setBounds(0, 0,
707 key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
708 key.icon.draw(canvas);
709 canvas.translate(-drawableX, -drawableY);
710 }
711 canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
712 }
713 mInvalidatedKey = null;
714 // Overlay a dark rectangle to dim the keyboard
715 if (mMiniKeyboardOnScreen) {
716 paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
717 canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
718 }
719
720 if (DEBUG && mShowTouchPoints) {
721 paint.setAlpha(128);
722 paint.setColor(0xFFFF0000);
723 canvas.drawCircle(mStartX, mStartY, 3, paint);
724 canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
725 paint.setColor(0xFF0000FF);
726 canvas.drawCircle(mLastX, mLastY, 3, paint);
727 paint.setColor(0xFF00FF00);
728 canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
729 }
730
731 mDrawPending = false;
732 mDirtyRect.setEmpty();
733 }
734
735 private int getKeyIndices(int x, int y, int[] allKeys) {
736 final Key[] keys = mKeys;
737 int primaryIndex = NOT_A_KEY;
738 int closestKey = NOT_A_KEY;
739 int closestKeyDist = mProximityThreshold + 1;
740 java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
741 int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
742 final int keyCount = nearestKeyIndices.length;
743 for (int i = 0; i < keyCount; i++) {
744 final Key key = keys[nearestKeyIndices[i]];
745 int dist = 0;
746 boolean isInside = key.isInside(x,y);
747 if (isInside) {
748 primaryIndex = nearestKeyIndices[i];
749 }
750
751 if (((mProximityCorrectOn
752 && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
753 || isInside)
754 && key.codes[0] > 32) {
755 // Find insertion point
756 final int nCodes = key.codes.length;
757 if (dist < closestKeyDist) {
758 closestKeyDist = dist;
759 closestKey = nearestKeyIndices[i];
760 }
761
762 if (allKeys == null) continue;
763
764 for (int j = 0; j < mDistances.length; j++) {
765 if (mDistances[j] > dist) {
766 // Make space for nCodes codes
767 System.arraycopy(mDistances, j, mDistances, j + nCodes,
768 mDistances.length - j - nCodes);
769 System.arraycopy(allKeys, j, allKeys, j + nCodes,
770 allKeys.length - j - nCodes);
771 for (int c = 0; c < nCodes; c++) {
772 allKeys[j + c] = key.codes[c];
773 mDistances[j + c] = dist;
774 }
775 break;
776 }
777 }
778 }
779 }
780 if (primaryIndex == NOT_A_KEY) {
781 primaryIndex = closestKey;
782 }
783 return primaryIndex;
784 }
785
786 private void detectAndSendKey(int index, int x, int y, long eventTime) {
787 if (index != NOT_A_KEY && index < mKeys.length) {
788 final Key key = mKeys[index];
789 if (key.text != null) {
790 mKeyboardActionListener.onText(key.text);
791 mKeyboardActionListener.onRelease(NOT_A_KEY);
792 } else {
793 int code = key.codes[0];
794 //TextEntryState.keyPressedAt(key, x, y);
795 int[] codes = new int[MAX_NEARBY_KEYS];
796 Arrays.fill(codes, NOT_A_KEY);
797 getKeyIndices(x, y, codes);
798 // Multi-tap
799 if (mInMultiTap) {
800 if (mTapCount != -1) {
801 mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
802 } else {
803 mTapCount = 0;
804 }
805 code = key.codes[mTapCount];
806 }
807 mKeyboardActionListener.onKey(code, codes);
808 mKeyboardActionListener.onRelease(code);
809 }
810 mLastSentIndex = index;
811 mLastTapTime = eventTime;
812 }
813 }
814
815 /**
816 * Handle multi-tap keys by producing the key label for the current multi-tap state.
817 */
818 private CharSequence getPreviewText(Key key) {
819 if (mInMultiTap) {
820 // Multi-tap
821 mPreviewLabel.setLength(0);
822 mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
823 return adjustCase(mPreviewLabel);
824 } else {
825 return adjustCase(key.label);
826 }
827 }
828
829 private void showPreview(int keyIndex) {
830 int oldKeyIndex = mCurrentKeyIndex;
831 final PopupWindow previewPopup = mPreviewPopup;
832
833 mCurrentKeyIndex = keyIndex;
834 // Release the old key and press the new key
835 final Key[] keys = mKeys;
836 if (oldKeyIndex != mCurrentKeyIndex) {
837 if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
838 keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY);
839 invalidateKey(oldKeyIndex);
840 }
841 if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
842 keys[mCurrentKeyIndex].onPressed();
843 invalidateKey(mCurrentKeyIndex);
844 }
845 }
846 // If key changed and preview is on ...
847 if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
848 mHandler.removeMessages(MSG_SHOW_PREVIEW);
849 if (previewPopup.isShowing()) {
850 if (keyIndex == NOT_A_KEY) {
851 mHandler.sendMessageDelayed(mHandler
852 .obtainMessage(MSG_REMOVE_PREVIEW),
853 DELAY_AFTER_PREVIEW);
854 }
855 }
856 if (keyIndex != NOT_A_KEY) {
857 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
858 // Show right away, if it's already visible and finger is moving around
859 showKey(keyIndex);
860 } else {
861 mHandler.sendMessageDelayed(
862 mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
863 DELAY_BEFORE_PREVIEW);
864 }
865 }
866 }
867 }
868
869 private void showKey(final int keyIndex) {
870 final PopupWindow previewPopup = mPreviewPopup;
871 final Key[] keys = mKeys;
872 if (keyIndex < 0 || keyIndex >= mKeys.length) return;
873 Key key = keys[keyIndex];
874 if (key.icon != null) {
875 mPreviewText.setCompoundDrawables(null, null, null,
876 key.iconPreview != null ? key.iconPreview : key.icon);
877 mPreviewText.setText(null);
878 } else {
879 mPreviewText.setCompoundDrawables(null, null, null, null);
880 mPreviewText.setText(getPreviewText(key));
881 if (key.label.length() > 1 && key.codes.length < 2) {
882 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
883 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
884 } else {
885 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
886 mPreviewText.setTypeface(Typeface.DEFAULT);
887 }
888 }
889 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
890 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
891 int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
892 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
893 final int popupHeight = mPreviewHeight;
894 LayoutParams lp = mPreviewText.getLayoutParams();
895 if (lp != null) {
896 lp.width = popupWidth;
897 lp.height = popupHeight;
898 }
899 if (!mPreviewCentered) {
900 mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
901 mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
902 } else {
903 // TODO: Fix this if centering is brought back
904 mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
905 mPopupPreviewY = - mPreviewText.getMeasuredHeight();
906 }
907 mHandler.removeMessages(MSG_REMOVE_PREVIEW);
908 if (mOffsetInWindow == null) {
909 mOffsetInWindow = new int[2];
910 getLocationInWindow(mOffsetInWindow);
911 mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero
912 mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero
913 int[] mWindowLocation = new int[2];
914 getLocationOnScreen(mWindowLocation);
915 mWindowY = mWindowLocation[1];
916 }
917 // Set the preview background state
918 mPreviewText.getBackground().setState(
919 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
920 mPopupPreviewX += mOffsetInWindow[0];
921 mPopupPreviewY += mOffsetInWindow[1];
922
923 // If the popup cannot be shown above the key, put it on the side
924 if (mPopupPreviewY + mWindowY < 0) {
925 // If the key you're pressing is on the left side of the keyboard, show the popup on
926 // the right, offset by enough to see at least one key to the left/right.
927 if (key.x + key.width <= getWidth() / 2) {
928 mPopupPreviewX += (int) (key.width * 2.5);
929 } else {
930 mPopupPreviewX -= (int) (key.width * 2.5);
931 }
932 mPopupPreviewY += popupHeight;
933 }
934
935 if (previewPopup.isShowing()) {
936 previewPopup.update(mPopupPreviewX, mPopupPreviewY,
937 popupWidth, popupHeight);
938 } else {
939 previewPopup.setWidth(popupWidth);
940 previewPopup.setHeight(popupHeight);
941 previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
942 mPopupPreviewX, mPopupPreviewY);
943 }
944 mPreviewText.setVisibility(VISIBLE);
945 }
946
947 /**
948 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
949 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
950 * draws the cached buffer.
951 * @see #invalidateKey(int)
952 */
953 public void invalidateAllKeys() {
954 mDirtyRect.union(0, 0, getWidth(), getHeight());
955 mDrawPending = true;
956 invalidate();
957 }
958
959 /**
960 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
961 * one key is changing it's content. Any changes that affect the position or size of the key
962 * may not be honored.
963 * @param keyIndex the index of the key in the attached {@link Keyboard}.
964 * @see #invalidateAllKeys
965 */
966 public void invalidateKey(int keyIndex) {
967 if (mKeys == null) return;
968 if (keyIndex < 0 || keyIndex >= mKeys.length) {
969 return;
970 }
971 final Key key = mKeys[keyIndex];
972 mInvalidatedKey = key;
973 mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop,
974 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
975 onBufferDraw();
976 invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
977 key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
978 }
979
980 private boolean openPopupIfRequired(MotionEvent me) {
981 // Check if we have a popup layout specified first.
982 if (mPopupLayout == 0) {
983 return false;
984 }
985 if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
986 return false;
987 }
988
989 Key popupKey = mKeys[mCurrentKey];
990 boolean result = onLongPress(popupKey);
991 if (result) {
992 mAbortKey = true;
993 showPreview(NOT_A_KEY);
994 }
995 return result;
996 }
997
998 /**
999 * Called when a key is long pressed. By default this will open any popup keyboard associated
1000 * with this key through the attributes popupLayout and popupCharacters.
1001 * @param popupKey the key that was long pressed
1002 * @return true if the long press is handled, false otherwise. Subclasses should call the
1003 * method on the base class if the subclass doesn't wish to handle the call.
1004 */
1005 protected boolean onLongPress(Key popupKey) {
1006 int popupKeyboardId = popupKey.popupResId;
1007
1008 if (popupKeyboardId != 0) {
1009 mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
1010 if (mMiniKeyboardContainer == null) {
1011 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
1012 Context.LAYOUT_INFLATER_SERVICE);
1013 mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
1014 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1015 com.android.internal.R.id.keyboardView);
1016 View closeButton = mMiniKeyboardContainer.findViewById(
1017 com.android.internal.R.id.closeButton);
1018 if (closeButton != null) closeButton.setOnClickListener(this);
1019 mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
1020 public void onKey(int primaryCode, int[] keyCodes) {
1021 mKeyboardActionListener.onKey(primaryCode, keyCodes);
1022 dismissPopupKeyboard();
1023 }
1024
1025 public void onText(CharSequence text) {
1026 mKeyboardActionListener.onText(text);
1027 dismissPopupKeyboard();
1028 }
1029
1030 public void swipeLeft() { }
1031 public void swipeRight() { }
1032 public void swipeUp() { }
1033 public void swipeDown() { }
1034 public void onPress(int primaryCode) {
1035 mKeyboardActionListener.onPress(primaryCode);
1036 }
1037 public void onRelease(int primaryCode) {
1038 mKeyboardActionListener.onRelease(primaryCode);
1039 }
1040 });
1041 //mInputView.setSuggest(mSuggest);
1042 Keyboard keyboard;
1043 if (popupKey.popupCharacters != null) {
1044 keyboard = new Keyboard(getContext(), popupKeyboardId,
1045 popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
1046 } else {
1047 keyboard = new Keyboard(getContext(), popupKeyboardId);
1048 }
1049 mMiniKeyboard.setKeyboard(keyboard);
1050 mMiniKeyboard.setPopupParent(this);
1051 mMiniKeyboardContainer.measure(
1052 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1053 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1054
1055 mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
1056 } else {
1057 mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1058 com.android.internal.R.id.keyboardView);
1059 }
1060 if (mWindowOffset == null) {
1061 mWindowOffset = new int[2];
1062 getLocationInWindow(mWindowOffset);
1063 }
1064 mPopupX = popupKey.x + mPaddingLeft;
1065 mPopupY = popupKey.y + mPaddingTop;
1066 mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
1067 mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
1068 final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0];
1069 final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1];
1070 mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
1071 mMiniKeyboard.setShifted(isShifted());
1072 mPopupKeyboard.setContentView(mMiniKeyboardContainer);
1073 mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
1074 mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
1075 mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1076 mMiniKeyboardOnScreen = true;
1077 //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
1078 invalidateAllKeys();
1079 return true;
1080 }
1081 return false;
1082 }
1083
1084 private long mOldEventTime;
1085 private boolean mUsedVelocity;
1086
1087 @Override
1088 public boolean onTouchEvent(MotionEvent me) {
1089 // Convert multi-pointer up/down events to single up/down events to
1090 // deal with the typical multi-pointer behavior of two-thumb typing
1091 final int pointerCount = me.getPointerCount();
1092 final int action = me.getAction();
1093 boolean result = false;
1094 final long now = me.getEventTime();
1095
1096 if (pointerCount != mOldPointerCount) {
1097 if (pointerCount == 1) {
1098 // Send a down event for the latest pointer
1099 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1100 me.getX(), me.getY(), me.getMetaState());
1101 result = onModifiedTouchEvent(down, false);
1102 down.recycle();
1103 // If it's an up action, then deliver the up as well.
1104 if (action == MotionEvent.ACTION_UP) {
1105 result = onModifiedTouchEvent(me, true);
1106 }
1107 } else {
1108 // Send an up event for the last pointer
1109 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1110 mOldPointerX, mOldPointerY, me.getMetaState());
1111 result = onModifiedTouchEvent(up, true);
1112 up.recycle();
1113 }
1114 } else {
1115 if (pointerCount == 1) {
1116 result = onModifiedTouchEvent(me, false);
1117 mOldPointerX = me.getX();
1118 mOldPointerY = me.getY();
1119 } else {
1120 // Don't do anything when 2 pointers are down and moving.
1121 result = true;
1122 }
1123 }
1124 mOldPointerCount = pointerCount;
1125
1126 return result;
1127 }
1128
1129 private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
1130 int touchX = (int) me.getX() - mPaddingLeft;
1131 int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop;
1132 final int action = me.getAction();
1133 final long eventTime = me.getEventTime();
1134 mOldEventTime = eventTime;
1135 int keyIndex = getKeyIndices(touchX, touchY, null);
1136 mPossiblePoly = possiblePoly;
1137
1138 // Track the last few movements to look for spurious swipes.
1139 if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
1140 mSwipeTracker.addMovement(me);
1141
1142 // Ignore all motion events until a DOWN.
1143 if (mAbortKey
1144 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
1145 return true;
1146 }
1147
1148 if (mGestureDetector.onTouchEvent(me)) {
1149 showPreview(NOT_A_KEY);
1150 mHandler.removeMessages(MSG_REPEAT);
1151 mHandler.removeMessages(MSG_LONGPRESS);
1152 return true;
1153 }
1154
1155 // Needs to be called after the gesture detector gets a turn, as it may have
1156 // displayed the mini keyboard
1157 if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) {
1158 return true;
1159 }
1160
1161 switch (action) {
1162 case MotionEvent.ACTION_DOWN:
1163 mAbortKey = false;
1164 mStartX = touchX;
1165 mStartY = touchY;
1166 mLastCodeX = touchX;
1167 mLastCodeY = touchY;
1168 mLastKeyTime = 0;
1169 mCurrentKeyTime = 0;
1170 mLastKey = NOT_A_KEY;
1171 mCurrentKey = keyIndex;
1172 mDownKey = keyIndex;
1173 mDownTime = me.getEventTime();
1174 mLastMoveTime = mDownTime;
1175 checkMultiTap(eventTime, keyIndex);
1176 mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
1177 mKeys[keyIndex].codes[0] : 0);
1178 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1179 mRepeatKeyIndex = mCurrentKey;
1180 Message msg = mHandler.obtainMessage(MSG_REPEAT);
1181 mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1182 repeatKey();
1183 // Delivering the key could have caused an abort
1184 if (mAbortKey) {
1185 mRepeatKeyIndex = NOT_A_KEY;
1186 break;
1187 }
1188 }
1189 if (mCurrentKey != NOT_A_KEY) {
1190 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1191 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1192 }
1193 showPreview(keyIndex);
1194 break;
1195
1196 case MotionEvent.ACTION_MOVE:
1197 boolean continueLongPress = false;
1198 if (keyIndex != NOT_A_KEY) {
1199 if (mCurrentKey == NOT_A_KEY) {
1200 mCurrentKey = keyIndex;
1201 mCurrentKeyTime = eventTime - mDownTime;
1202 } else {
1203 if (keyIndex == mCurrentKey) {
1204 mCurrentKeyTime += eventTime - mLastMoveTime;
1205 continueLongPress = true;
1206 } else if (mRepeatKeyIndex == NOT_A_KEY) {
1207 resetMultiTap();
1208 mLastKey = mCurrentKey;
1209 mLastCodeX = mLastX;
1210 mLastCodeY = mLastY;
1211 mLastKeyTime =
1212 mCurrentKeyTime + eventTime - mLastMoveTime;
1213 mCurrentKey = keyIndex;
1214 mCurrentKeyTime = 0;
1215 }
1216 }
1217 }
1218 if (!continueLongPress) {
1219 // Cancel old longpress
1220 mHandler.removeMessages(MSG_LONGPRESS);
1221 // Start new longpress if key has changed
1222 if (keyIndex != NOT_A_KEY) {
1223 Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1224 mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1225 }
1226 }
1227 showPreview(mCurrentKey);
1228 mLastMoveTime = eventTime;
1229 break;
1230
1231 case MotionEvent.ACTION_UP:
1232 removeMessages();
1233 if (keyIndex == mCurrentKey) {
1234 mCurrentKeyTime += eventTime - mLastMoveTime;
1235 } else {
1236 resetMultiTap();
1237 mLastKey = mCurrentKey;
1238 mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1239 mCurrentKey = keyIndex;
1240 mCurrentKeyTime = 0;
1241 }
1242 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
1243 && mLastKey != NOT_A_KEY) {
1244 mCurrentKey = mLastKey;
1245 touchX = mLastCodeX;
1246 touchY = mLastCodeY;
1247 }
1248 showPreview(NOT_A_KEY);
1249 Arrays.fill(mKeyIndices, NOT_A_KEY);
1250 // If we're not on a repeating key (which sends on a DOWN event)
1251 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
1252 detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
1253 }
1254 invalidateKey(keyIndex);
1255 mRepeatKeyIndex = NOT_A_KEY;
1256 break;
1257 case MotionEvent.ACTION_CANCEL:
1258 removeMessages();
1259 dismissPopupKeyboard();
1260 mAbortKey = true;
1261 showPreview(NOT_A_KEY);
1262 invalidateKey(mCurrentKey);
1263 break;
1264 }
1265 mLastX = touchX;
1266 mLastY = touchY;
1267 return true;
1268 }
1269
1270 private boolean repeatKey() {
1271 Key key = mKeys[mRepeatKeyIndex];
1272 detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
1273 return true;
1274 }
1275
1276 protected void swipeRight() {
1277 mKeyboardActionListener.swipeRight();
1278 }
1279
1280 protected void swipeLeft() {
1281 mKeyboardActionListener.swipeLeft();
1282 }
1283
1284 protected void swipeUp() {
1285 mKeyboardActionListener.swipeUp();
1286 }
1287
1288 protected void swipeDown() {
1289 mKeyboardActionListener.swipeDown();
1290 }
1291
1292 public void closing() {
1293 if (mPreviewPopup.isShowing()) {
1294 mPreviewPopup.dismiss();
1295 }
1296 removeMessages();
1297
1298 dismissPopupKeyboard();
1299 mBuffer = null;
1300 mCanvas = null;
1301 mMiniKeyboardCache.clear();
1302 }
1303
1304 private void removeMessages() {
1305 mHandler.removeMessages(MSG_REPEAT);
1306 mHandler.removeMessages(MSG_LONGPRESS);
1307 mHandler.removeMessages(MSG_SHOW_PREVIEW);
1308 }
1309
1310 @Override
1311 public void onDetachedFromWindow() {
1312 super.onDetachedFromWindow();
1313 closing();
1314 }
1315
1316 private void dismissPopupKeyboard() {
1317 if (mPopupKeyboard.isShowing()) {
1318 mPopupKeyboard.dismiss();
1319 mMiniKeyboardOnScreen = false;
1320 invalidateAllKeys();
1321 }
1322 }
1323
1324 public boolean handleBack() {
1325 if (mPopupKeyboard.isShowing()) {
1326 dismissPopupKeyboard();
1327 return true;
1328 }
1329 return false;
1330 }
1331
1332 private void resetMultiTap() {
1333 mLastSentIndex = NOT_A_KEY;
1334 mTapCount = 0;
1335 mLastTapTime = -1;
1336 mInMultiTap = false;
1337 }
1338
1339 private void checkMultiTap(long eventTime, int keyIndex) {
1340 if (keyIndex == NOT_A_KEY) return;
1341 Key key = mKeys[keyIndex];
1342 if (key.codes.length > 1) {
1343 mInMultiTap = true;
1344 if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1345 && keyIndex == mLastSentIndex) {
1346 mTapCount = (mTapCount + 1) % key.codes.length;
1347 return;
1348 } else {
1349 mTapCount = -1;
1350 return;
1351 }
1352 }
1353 if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1354 resetMultiTap();
1355 }
1356 }
1357
1358 private static class SwipeTracker {
1359
1360 static final int NUM_PAST = 4;
1361 static final int LONGEST_PAST_TIME = 200;
1362
1363 final float mPastX[] = new float[NUM_PAST];
1364 final float mPastY[] = new float[NUM_PAST];
1365 final long mPastTime[] = new long[NUM_PAST];
1366
1367 float mYVelocity;
1368 float mXVelocity;
1369
1370 public void clear() {
1371 mPastTime[0] = 0;
1372 }
1373
1374 public void addMovement(MotionEvent ev) {
1375 long time = ev.getEventTime();
1376 final int N = ev.getHistorySize();
1377 for (int i=0; i<N; i++) {
1378 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
1379 ev.getHistoricalEventTime(i));
1380 }
1381 addPoint(ev.getX(), ev.getY(), time);
1382 }
1383
1384 private void addPoint(float x, float y, long time) {
1385 int drop = -1;
1386 int i;
1387 final long[] pastTime = mPastTime;
1388 for (i=0; i<NUM_PAST; i++) {
1389 if (pastTime[i] == 0) {
1390 break;
1391 } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
1392 drop = i;
1393 }
1394 }
1395 if (i == NUM_PAST && drop < 0) {
1396 drop = 0;
1397 }
1398 if (drop == i) drop--;
1399 final float[] pastX = mPastX;
1400 final float[] pastY = mPastY;
1401 if (drop >= 0) {
1402 final int start = drop+1;
1403 final int count = NUM_PAST-drop-1;
1404 System.arraycopy(pastX, start, pastX, 0, count);
1405 System.arraycopy(pastY, start, pastY, 0, count);
1406 System.arraycopy(pastTime, start, pastTime, 0, count);
1407 i -= (drop+1);
1408 }
1409 pastX[i] = x;
1410 pastY[i] = y;
1411 pastTime[i] = time;
1412 i++;
1413 if (i < NUM_PAST) {
1414 pastTime[i] = 0;
1415 }
1416 }
1417
1418 public void computeCurrentVelocity(int units) {
1419 computeCurrentVelocity(units, Float.MAX_VALUE);
1420 }
1421
1422 public void computeCurrentVelocity(int units, float maxVelocity) {
1423 final float[] pastX = mPastX;
1424 final float[] pastY = mPastY;
1425 final long[] pastTime = mPastTime;
1426
1427 final float oldestX = pastX[0];
1428 final float oldestY = pastY[0];
1429 final long oldestTime = pastTime[0];
1430 float accumX = 0;
1431 float accumY = 0;
1432 int N=0;
1433 while (N < NUM_PAST) {
1434 if (pastTime[N] == 0) {
1435 break;
1436 }
1437 N++;
1438 }
1439
1440 for (int i=1; i < N; i++) {
1441 final int dur = (int)(pastTime[i] - oldestTime);
1442 if (dur == 0) continue;
1443 float dist = pastX[i] - oldestX;
1444 float vel = (dist/dur) * units; // pixels/frame.
1445 if (accumX == 0) accumX = vel;
1446 else accumX = (accumX + vel) * .5f;
1447
1448 dist = pastY[i] - oldestY;
1449 vel = (dist/dur) * units; // pixels/frame.
1450 if (accumY == 0) accumY = vel;
1451 else accumY = (accumY + vel) * .5f;
1452 }
1453 mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
1454 : Math.min(accumX, maxVelocity);
1455 mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
1456 : Math.min(accumY, maxVelocity);
1457 }
1458
1459 public float getXVelocity() {
1460 return mXVelocity;
1461 }
1462
1463 public float getYVelocity() {
1464 return mYVelocity;
1465 }
1466 }
1467}