SuddenJumpingTouchEventHandler.java revision 0fef498a07515bdd5ac1ccfa564776d72fd85a51
1/* 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 com.android.inputmethod.latin; 18 19import java.util.List; 20 21import android.content.Context; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.inputmethodservice.Keyboard; 25import android.inputmethodservice.KeyboardView; 26import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; 27import android.inputmethodservice.Keyboard.Key; 28import android.os.Handler; 29import android.os.Message; 30import android.os.SystemClock; 31import android.util.AttributeSet; 32import android.view.LayoutInflater; 33import android.view.MotionEvent; 34import android.widget.PopupWindow; 35 36public class LatinKeyboardView extends KeyboardView { 37 38 static final int KEYCODE_OPTIONS = -100; 39 static final int KEYCODE_SHIFT_LONGPRESS = -101; 40 static final int KEYCODE_VOICE = -102; 41 static final int KEYCODE_F1 = -103; 42 static final int KEYCODE_NEXT_LANGUAGE = -104; 43 static final int KEYCODE_PREV_LANGUAGE = -105; 44 45 private Keyboard mPhoneKeyboard; 46 47 /** Whether the extension of this keyboard is visible */ 48 private boolean mExtensionVisible; 49 /** The view that is shown as an extension of this keyboard view */ 50 private LatinKeyboardView mExtension; 51 /** The popup window that contains the extension of this keyboard */ 52 private PopupWindow mExtensionPopup; 53 /** Whether this view is an extension of another keyboard */ 54 private boolean mIsExtensionType; 55 private boolean mFirstEvent; 56 /** Whether we've started dropping move events because we found a big jump */ 57 private boolean mDroppingEvents; 58 /** 59 * Whether multi-touch disambiguation needs to be disabled for any reason. There are 2 reasons 60 * for this to happen - (1) if a real multi-touch event has occured and (2) we've opened an 61 * extension keyboard. 62 */ 63 private boolean mDisableDisambiguation; 64 /** The distance threshold at which we start treating the touch session as a multi-touch */ 65 private int mJumpThresholdSquare = Integer.MAX_VALUE; 66 67 public LatinKeyboardView(Context context, AttributeSet attrs) { 68 super(context, attrs); 69 } 70 71 public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 } 74 75 public void setPhoneKeyboard(Keyboard phoneKeyboard) { 76 mPhoneKeyboard = phoneKeyboard; 77 } 78 79 @Override 80 public void setKeyboard(Keyboard k) { 81 super.setKeyboard(k); 82 // One-seventh of the keyboard width seems like a reasonable threshold 83 mJumpThresholdSquare = k.getMinWidth() / 7; 84 mJumpThresholdSquare *= mJumpThresholdSquare; 85 setKeyboardLocal(k); 86 } 87 88 @Override 89 protected boolean onLongPress(Key key) { 90 if (key.codes[0] == Keyboard.KEYCODE_MODE_CHANGE) { 91 getOnKeyboardActionListener().onKey(KEYCODE_OPTIONS, null); 92 return true; 93 } else if (key.codes[0] == Keyboard.KEYCODE_SHIFT) { 94 getOnKeyboardActionListener().onKey(KEYCODE_SHIFT_LONGPRESS, null); 95 invalidateAllKeys(); 96 return true; 97 } else if (key.codes[0] == '0' && getKeyboard() == mPhoneKeyboard) { 98 // Long pressing on 0 in phone number keypad gives you a '+'. 99 getOnKeyboardActionListener().onKey('+', null); 100 return true; 101 } else { 102 return super.onLongPress(key); 103 } 104 } 105 106 /** 107 * This function checks to see if we need to handle any sudden jumps in the pointer location 108 * that could be due to a multi-touch being treated as a move by the firmware or hardware. 109 * Once a sudden jump is detected, all subsequent move events are discarded 110 * until an UP is received.<P> 111 * When a sudden jump is detected, an UP event is simulated at the last position and when 112 * the sudden moves subside, a DOWN event is simulated for the second key. 113 * @param me the motion event 114 * @return true if the event was consumed, so that it doesn't continue to be handled by 115 * KeyboardView. 116 */ 117 private boolean handleSuddenJump(MotionEvent me) { 118 final int action = me.getAction(); 119 final int x = (int) me.getX(); 120 final int y = (int) me.getY(); 121 boolean result = false; 122 123 // Real multi-touch event? Stop looking for sudden jumps 124 if (me.getPointerCount() > 1) { 125 mDisableDisambiguation = true; 126 } 127 if (mDisableDisambiguation) { 128 // If UP, reset the multi-touch flag 129 if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false; 130 return false; 131 } 132 133 switch (action) { 134 case MotionEvent.ACTION_DOWN: 135 // Reset the "session" 136 mDroppingEvents = false; 137 mDisableDisambiguation = false; 138 break; 139 case MotionEvent.ACTION_MOVE: 140 // Is this a big jump? 141 final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y); 142 if (distanceSquare > mJumpThresholdSquare) { 143 // If we're not yet dropping events, start dropping and send an UP event 144 if (!mDroppingEvents) { 145 mDroppingEvents = true; 146 // Send an up event 147 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 148 MotionEvent.ACTION_UP, 149 mLastX, mLastY, me.getMetaState()); 150 super.onTouchEvent(translated); 151 translated.recycle(); 152 } 153 result = true; 154 } else if (mDroppingEvents) { 155 // If moves are small and we're already dropping events, continue dropping 156 result = true; 157 } 158 break; 159 case MotionEvent.ACTION_UP: 160 if (mDroppingEvents) { 161 // Send a down event first, as we dropped a bunch of sudden jumps and assume that 162 // the user is releasing the touch on the second key. 163 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 164 MotionEvent.ACTION_DOWN, 165 x, y, me.getMetaState()); 166 super.onTouchEvent(translated); 167 translated.recycle(); 168 mDroppingEvents = false; 169 // Let the up event get processed as well, result = false 170 } 171 break; 172 } 173 // Track the previous coordinate 174 mLastX = x; 175 mLastY = y; 176 return result; 177 } 178 179 @Override 180 public boolean onTouchEvent(MotionEvent me) { 181 LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); 182 if (DEBUG_LINE) { 183 mLastX = (int) me.getX(); 184 mLastY = (int) me.getY(); 185 invalidate(); 186 } 187 // If an extension keyboard is visible or this is an extension keyboard, don't look 188 // for sudden jumps. Otherwise, if there was a sudden jump, return without processing the 189 // actual motion event. 190 if (!mExtensionVisible && !mIsExtensionType 191 && handleSuddenJump(me)) return true; 192 193 // Reset any bounding box controls in the keyboard 194 if (me.getAction() == MotionEvent.ACTION_DOWN) { 195 keyboard.keyReleased(); 196 } 197 198 if (me.getAction() == MotionEvent.ACTION_UP) { 199 int languageDirection = keyboard.getLanguageChangeDirection(); 200 if (languageDirection != 0) { 201 getOnKeyboardActionListener().onKey( 202 languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE, 203 null); 204 me.setAction(MotionEvent.ACTION_CANCEL); 205 keyboard.keyReleased(); 206 return super.onTouchEvent(me); 207 } 208 } 209 210 // If we don't have an extension keyboard, don't go any further. 211 if (keyboard.getExtension() == 0) { 212 return super.onTouchEvent(me); 213 } 214 if (me.getY() < 0) { 215 if (mExtensionVisible) { 216 int action = me.getAction(); 217 if (mFirstEvent) action = MotionEvent.ACTION_DOWN; 218 mFirstEvent = false; 219 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 220 action, 221 me.getX(), me.getY() + mExtension.getHeight(), me.getMetaState()); 222 boolean result = mExtension.onTouchEvent(translated); 223 translated.recycle(); 224 if (me.getAction() == MotionEvent.ACTION_UP 225 || me.getAction() == MotionEvent.ACTION_CANCEL) { 226 closeExtension(); 227 } 228 return result; 229 } else { 230 if (openExtension()) { 231 MotionEvent cancel = MotionEvent.obtain(me.getDownTime(), me.getEventTime(), 232 MotionEvent.ACTION_CANCEL, me.getX() - 100, me.getY() - 100, 0); 233 super.onTouchEvent(cancel); 234 cancel.recycle(); 235 if (mExtension.getHeight() > 0) { 236 MotionEvent translated = MotionEvent.obtain(me.getEventTime(), 237 me.getEventTime(), 238 MotionEvent.ACTION_DOWN, 239 me.getX(), me.getY() + mExtension.getHeight(), 240 me.getMetaState()); 241 mExtension.onTouchEvent(translated); 242 translated.recycle(); 243 } else { 244 mFirstEvent = true; 245 } 246 // Stop processing multi-touch errors 247 mDisableDisambiguation = true; 248 } 249 return true; 250 } 251 } else if (mExtensionVisible) { 252 closeExtension(); 253 // Send a down event into the main keyboard first 254 MotionEvent down = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), 255 MotionEvent.ACTION_DOWN, 256 me.getX(), me.getY(), me.getMetaState()); 257 super.onTouchEvent(down); 258 down.recycle(); 259 // Send the actual event 260 return super.onTouchEvent(me); 261 } else { 262 return super.onTouchEvent(me); 263 } 264 } 265 266 private void setExtensionType(boolean isExtensionType) { 267 mIsExtensionType = isExtensionType; 268 } 269 270 private boolean openExtension() { 271 if (((LatinKeyboard) getKeyboard()).getExtension() == 0) return false; 272 makePopupWindow(); 273 mExtensionVisible = true; 274 return true; 275 } 276 277 private void makePopupWindow() { 278 if (mExtensionPopup == null) { 279 int[] windowLocation = new int[2]; 280 mExtensionPopup = new PopupWindow(getContext()); 281 mExtensionPopup.setBackgroundDrawable(null); 282 LayoutInflater li = (LayoutInflater) getContext().getSystemService( 283 Context.LAYOUT_INFLATER_SERVICE); 284 mExtension = (LatinKeyboardView) li.inflate(R.layout.input_trans, null); 285 mExtension.setExtensionType(true); 286 mExtension.setOnKeyboardActionListener( 287 new ExtensionKeyboardListener(getOnKeyboardActionListener())); 288 mExtension.setPopupParent(this); 289 mExtension.setPopupOffset(0, -windowLocation[1]); 290 Keyboard keyboard; 291 mExtension.setKeyboard(keyboard = new LatinKeyboard(getContext(), 292 ((LatinKeyboard) getKeyboard()).getExtension())); 293 mExtensionPopup.setContentView(mExtension); 294 mExtensionPopup.setWidth(getWidth()); 295 mExtensionPopup.setHeight(keyboard.getHeight()); 296 mExtensionPopup.setAnimationStyle(-1); 297 getLocationInWindow(windowLocation); 298 // TODO: Fix the "- 30". 299 mExtension.setPopupOffset(0, -windowLocation[1] - 30); 300 mExtensionPopup.showAtLocation(this, 0, 0, -keyboard.getHeight() 301 + windowLocation[1]); 302 } else { 303 mExtension.setVisibility(VISIBLE); 304 } 305 } 306 307 @Override 308 public void closing() { 309 super.closing(); 310 if (mExtensionPopup != null && mExtensionPopup.isShowing()) { 311 mExtensionPopup.dismiss(); 312 mExtensionPopup = null; 313 } 314 } 315 316 private void closeExtension() { 317 mExtension.closing(); 318 mExtension.setVisibility(INVISIBLE); 319 mExtensionVisible = false; 320 } 321 322 private static class ExtensionKeyboardListener implements OnKeyboardActionListener { 323 private OnKeyboardActionListener mTarget; 324 ExtensionKeyboardListener(OnKeyboardActionListener target) { 325 mTarget = target; 326 } 327 public void onKey(int primaryCode, int[] keyCodes) { 328 mTarget.onKey(primaryCode, keyCodes); 329 } 330 public void onPress(int primaryCode) { 331 mTarget.onPress(primaryCode); 332 } 333 public void onRelease(int primaryCode) { 334 mTarget.onRelease(primaryCode); 335 } 336 public void onText(CharSequence text) { 337 mTarget.onText(text); 338 } 339 public void swipeDown() { 340 // Don't pass through 341 } 342 public void swipeLeft() { 343 // Don't pass through 344 } 345 public void swipeRight() { 346 // Don't pass through 347 } 348 public void swipeUp() { 349 // Don't pass through 350 } 351 } 352 353 /**************************** INSTRUMENTATION *******************************/ 354 355 static final boolean DEBUG_AUTO_PLAY = false; 356 static final boolean DEBUG_LINE = false; 357 private static final int MSG_TOUCH_DOWN = 1; 358 private static final int MSG_TOUCH_UP = 2; 359 360 Handler mHandler2; 361 362 private String mStringToPlay; 363 private int mStringIndex; 364 private boolean mDownDelivered; 365 private Key[] mAsciiKeys = new Key[256]; 366 private boolean mPlaying; 367 private int mLastX; 368 private int mLastY; 369 private Paint mPaint; 370 371 private void setKeyboardLocal(Keyboard k) { 372 if (DEBUG_AUTO_PLAY) { 373 findKeys(); 374 if (mHandler2 == null) { 375 mHandler2 = new Handler() { 376 @Override 377 public void handleMessage(Message msg) { 378 removeMessages(MSG_TOUCH_DOWN); 379 removeMessages(MSG_TOUCH_UP); 380 if (mPlaying == false) return; 381 382 switch (msg.what) { 383 case MSG_TOUCH_DOWN: 384 if (mStringIndex >= mStringToPlay.length()) { 385 mPlaying = false; 386 return; 387 } 388 char c = mStringToPlay.charAt(mStringIndex); 389 while (c > 255 || mAsciiKeys[(int) c] == null) { 390 mStringIndex++; 391 if (mStringIndex >= mStringToPlay.length()) { 392 mPlaying = false; 393 return; 394 } 395 c = mStringToPlay.charAt(mStringIndex); 396 } 397 int x = mAsciiKeys[c].x + 10; 398 int y = mAsciiKeys[c].y + 26; 399 MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(), 400 SystemClock.uptimeMillis(), 401 MotionEvent.ACTION_DOWN, x, y, 0); 402 LatinKeyboardView.this.dispatchTouchEvent(me); 403 me.recycle(); 404 sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else 405 // happens 406 mDownDelivered = true; 407 break; 408 case MSG_TOUCH_UP: 409 char cUp = mStringToPlay.charAt(mStringIndex); 410 int x2 = mAsciiKeys[cUp].x + 10; 411 int y2 = mAsciiKeys[cUp].y + 26; 412 mStringIndex++; 413 414 MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(), 415 SystemClock.uptimeMillis(), 416 MotionEvent.ACTION_UP, x2, y2, 0); 417 LatinKeyboardView.this.dispatchTouchEvent(me2); 418 me2.recycle(); 419 sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else 420 // happens 421 mDownDelivered = false; 422 break; 423 } 424 } 425 }; 426 427 } 428 } 429 } 430 431 private void findKeys() { 432 List<Key> keys = getKeyboard().getKeys(); 433 // Get the keys on this keyboard 434 for (int i = 0; i < keys.size(); i++) { 435 int code = keys.get(i).codes[0]; 436 if (code >= 0 && code <= 255) { 437 mAsciiKeys[code] = keys.get(i); 438 } 439 } 440 } 441 442 void startPlaying(String s) { 443 if (!DEBUG_AUTO_PLAY) return; 444 if (s == null) return; 445 mStringToPlay = s.toLowerCase(); 446 mPlaying = true; 447 mDownDelivered = false; 448 mStringIndex = 0; 449 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); 450 } 451 452 @Override 453 public void draw(Canvas c) { 454 super.draw(c); 455 if (DEBUG_AUTO_PLAY && mPlaying) { 456 mHandler2.removeMessages(MSG_TOUCH_DOWN); 457 mHandler2.removeMessages(MSG_TOUCH_UP); 458 if (mDownDelivered) { 459 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); 460 } else { 461 mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); 462 } 463 } 464 if (DEBUG_LINE) { 465 if (mPaint == null) { 466 mPaint = new Paint(); 467 mPaint.setColor(0x80FFFFFF); 468 mPaint.setAntiAlias(false); 469 } 470 c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint); 471 c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); 472 } 473 } 474} 475