1/* 2 * Copyright (C) 2009 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.pinyin; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.inputmethodservice.InputMethodService; 22import android.os.Handler; 23import android.os.SystemClock; 24import android.os.SystemProperties; 25import android.util.AttributeSet; 26import android.view.GestureDetector; 27import android.view.Gravity; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.View.OnTouchListener; 31import android.widget.PopupWindow; 32import android.widget.RelativeLayout; 33import android.widget.ViewFlipper; 34 35/** 36 * The top container to host soft keyboard view(s). 37 */ 38public class SkbContainer extends RelativeLayout implements OnTouchListener { 39 /** 40 * For finger touch, user tends to press the bottom part of the target key, 41 * or he/she even presses the area out of it, so it is necessary to make a 42 * simple bias correction. If the input method runs on emulator, no bias 43 * correction will be used. 44 */ 45 private static final int Y_BIAS_CORRECTION = -10; 46 47 /** 48 * Used to skip these move events whose position is too close to the 49 * previous touch events. 50 */ 51 private static final int MOVE_TOLERANCE = 6; 52 53 /** 54 * If this member is true, PopupWindow is used to show on-key highlight 55 * effect. 56 */ 57 private static boolean POPUPWINDOW_FOR_PRESSED_UI = false; 58 59 /** 60 * The current soft keyboard layout. 61 * 62 * @see com.android.inputmethod.pinyin.InputModeSwitcher for detailed layout 63 * definitions. 64 */ 65 private int mSkbLayout = 0; 66 67 /** 68 * The input method service. 69 */ 70 private InputMethodService mService; 71 72 /** 73 * Input mode switcher used to switch between different modes like Chinese, 74 * English, etc. 75 */ 76 private InputModeSwitcher mInputModeSwitcher; 77 78 /** 79 * The gesture detector. 80 */ 81 private GestureDetector mGestureDetector; 82 83 private Environment mEnvironment; 84 85 private ViewFlipper mSkbFlipper; 86 87 /** 88 * The popup balloon hint for key press/release. 89 */ 90 private BalloonHint mBalloonPopup; 91 92 /** 93 * The on-key balloon hint for key press/release. 94 */ 95 private BalloonHint mBalloonOnKey = null; 96 97 /** The major sub soft keyboard. */ 98 private SoftKeyboardView mMajorView; 99 100 /** 101 * The last parameter when function {@link #toggleCandidateMode(boolean)} 102 * was called. 103 */ 104 private boolean mLastCandidatesShowing; 105 106 /** Used to indicate whether a popup soft keyboard is shown. */ 107 private boolean mPopupSkbShow = false; 108 109 /** 110 * Used to indicate whether a popup soft keyboard is just shown, and waits 111 * for the touch event to release. After the release, the popup window can 112 * response to touch events. 113 **/ 114 private boolean mPopupSkbNoResponse = false; 115 116 /** Popup sub keyboard. */ 117 private PopupWindow mPopupSkb; 118 119 /** The view of the popup sub soft keyboard. */ 120 private SoftKeyboardView mPopupSkbView; 121 122 private int mPopupX; 123 124 private int mPopupY; 125 126 /** 127 * When user presses a key, a timer is started, when it times out, it is 128 * necessary to detect whether user still holds the key. 129 */ 130 private volatile boolean mWaitForTouchUp = false; 131 132 /** 133 * When user drags on the soft keyboard and the distance is enough, this 134 * drag will be recognized as a gesture and a gesture-based action will be 135 * taken, in this situation, ignore the consequent events. 136 */ 137 private volatile boolean mDiscardEvent = false; 138 139 /** 140 * For finger touch, user tends to press the bottom part of the target key, 141 * or he/she even presses the area out of it, so it is necessary to make a 142 * simple bias correction in Y. 143 */ 144 private int mYBiasCorrection = 0; 145 146 /** 147 * The x coordination of the last touch event. 148 */ 149 private int mXLast; 150 151 /** 152 * The y coordination of the last touch event. 153 */ 154 private int mYLast; 155 156 /** 157 * The soft keyboard view. 158 */ 159 private SoftKeyboardView mSkv; 160 161 /** 162 * The position of the soft keyboard view in the container. 163 */ 164 private int mSkvPosInContainer[] = new int[2]; 165 166 /** 167 * The key pressed by user. 168 */ 169 private SoftKey mSoftKeyDown = null; 170 171 /** 172 * Used to timeout a press if user holds the key for a long time. 173 */ 174 private LongPressTimer mLongPressTimer; 175 176 /** 177 * For temporary use. 178 */ 179 private int mXyPosTmp[] = new int[2]; 180 181 public SkbContainer(Context context, AttributeSet attrs) { 182 super(context, attrs); 183 184 mEnvironment = Environment.getInstance(); 185 186 mLongPressTimer = new LongPressTimer(this); 187 188 // If it runs on an emulator, no bias correction 189 if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) { 190 mYBiasCorrection = 0; 191 } else { 192 mYBiasCorrection = Y_BIAS_CORRECTION; 193 } 194 mBalloonPopup = new BalloonHint(context, this, MeasureSpec.AT_MOST); 195 if (POPUPWINDOW_FOR_PRESSED_UI) { 196 mBalloonOnKey = new BalloonHint(context, this, MeasureSpec.AT_MOST); 197 } 198 199 mPopupSkb = new PopupWindow(mContext); 200 mPopupSkb.setBackgroundDrawable(null); 201 mPopupSkb.setClippingEnabled(false); 202 } 203 204 public void setService(InputMethodService service) { 205 mService = service; 206 } 207 208 public void setInputModeSwitcher(InputModeSwitcher inputModeSwitcher) { 209 mInputModeSwitcher = inputModeSwitcher; 210 } 211 212 public void setGestureDetector(GestureDetector gestureDetector) { 213 mGestureDetector = gestureDetector; 214 } 215 216 public boolean isCurrentSkbSticky() { 217 if (null == mMajorView) return true; 218 SoftKeyboard skb = mMajorView.getSoftKeyboard(); 219 if (null != skb) { 220 return skb.getStickyFlag(); 221 } 222 return true; 223 } 224 225 public void toggleCandidateMode(boolean candidatesShowing) { 226 if (null == mMajorView || !mInputModeSwitcher.isChineseText() 227 || mLastCandidatesShowing == candidatesShowing) return; 228 mLastCandidatesShowing = candidatesShowing; 229 230 SoftKeyboard skb = mMajorView.getSoftKeyboard(); 231 if (null == skb) return; 232 233 int state = mInputModeSwitcher.getTooggleStateForCnCand(); 234 if (!candidatesShowing) { 235 skb.disableToggleState(state, false); 236 skb.enableToggleStates(mInputModeSwitcher.getToggleStates()); 237 } else { 238 skb.enableToggleState(state, false); 239 } 240 241 mMajorView.invalidate(); 242 } 243 244 public void updateInputMode() { 245 int skbLayout = mInputModeSwitcher.getSkbLayout(); 246 if (mSkbLayout != skbLayout) { 247 mSkbLayout = skbLayout; 248 updateSkbLayout(); 249 } 250 251 mLastCandidatesShowing = false; 252 253 if (null == mMajorView) return; 254 255 SoftKeyboard skb = mMajorView.getSoftKeyboard(); 256 if (null == skb) return; 257 skb.enableToggleStates(mInputModeSwitcher.getToggleStates()); 258 invalidate(); 259 return; 260 } 261 262 private void updateSkbLayout() { 263 int screenWidth = mEnvironment.getScreenWidth(); 264 int keyHeight = mEnvironment.getKeyHeight(); 265 int skbHeight = mEnvironment.getSkbHeight(); 266 267 Resources r = mContext.getResources(); 268 if (null == mSkbFlipper) { 269 mSkbFlipper = (ViewFlipper) findViewById(R.id.alpha_floatable); 270 } 271 mMajorView = (SoftKeyboardView) mSkbFlipper.getChildAt(0); 272 273 SoftKeyboard majorSkb = null; 274 SkbPool skbPool = SkbPool.getInstance(); 275 276 switch (mSkbLayout) { 277 case R.xml.skb_qwerty: 278 majorSkb = skbPool.getSoftKeyboard(R.xml.skb_qwerty, 279 R.xml.skb_qwerty, screenWidth, skbHeight, mContext); 280 break; 281 282 case R.xml.skb_sym1: 283 majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym1, R.xml.skb_sym1, 284 screenWidth, skbHeight, mContext); 285 break; 286 287 case R.xml.skb_sym2: 288 majorSkb = skbPool.getSoftKeyboard(R.xml.skb_sym2, R.xml.skb_sym2, 289 screenWidth, skbHeight, mContext); 290 break; 291 292 case R.xml.skb_smiley: 293 majorSkb = skbPool.getSoftKeyboard(R.xml.skb_smiley, 294 R.xml.skb_smiley, screenWidth, skbHeight, mContext); 295 break; 296 297 case R.xml.skb_phone: 298 majorSkb = skbPool.getSoftKeyboard(R.xml.skb_phone, 299 R.xml.skb_phone, screenWidth, skbHeight, mContext); 300 break; 301 default: 302 } 303 304 if (null == majorSkb || !mMajorView.setSoftKeyboard(majorSkb)) { 305 return; 306 } 307 mMajorView.setBalloonHint(mBalloonOnKey, mBalloonPopup, false); 308 mMajorView.invalidate(); 309 } 310 311 private void responseKeyEvent(SoftKey sKey) { 312 if (null == sKey) return; 313 ((PinyinIME) mService).responseSoftKeyEvent(sKey); 314 return; 315 } 316 317 private SoftKeyboardView inKeyboardView(int x, int y, 318 int positionInParent[]) { 319 if (mPopupSkbShow) { 320 if (mPopupX <= x && mPopupX + mPopupSkb.getWidth() > x 321 && mPopupY <= y && mPopupY + mPopupSkb.getHeight() > y) { 322 positionInParent[0] = mPopupX; 323 positionInParent[1] = mPopupY; 324 mPopupSkbView.setOffsetToSkbContainer(positionInParent); 325 return mPopupSkbView; 326 } 327 return null; 328 } 329 330 return mMajorView; 331 } 332 333 private void popupSymbols() { 334 int popupResId = mSoftKeyDown.getPopupResId(); 335 if (popupResId > 0) { 336 int skbContainerWidth = getWidth(); 337 int skbContainerHeight = getHeight(); 338 // The paddings of the background are not included. 339 int miniSkbWidth = (int) (skbContainerWidth * 0.8); 340 int miniSkbHeight = (int) (skbContainerHeight * 0.23); 341 342 SkbPool skbPool = SkbPool.getInstance(); 343 SoftKeyboard skb = skbPool.getSoftKeyboard(popupResId, popupResId, 344 miniSkbWidth, miniSkbHeight, mContext); 345 if (null == skb) return; 346 347 mPopupX = (skbContainerWidth - skb.getSkbTotalWidth()) / 2; 348 mPopupY = (skbContainerHeight - skb.getSkbTotalHeight()) / 2; 349 350 if (null == mPopupSkbView) { 351 mPopupSkbView = new SoftKeyboardView(mContext, null); 352 mPopupSkbView.onMeasure(LayoutParams.WRAP_CONTENT, 353 LayoutParams.WRAP_CONTENT); 354 } 355 mPopupSkbView.setOnTouchListener(this); 356 mPopupSkbView.setSoftKeyboard(skb); 357 mPopupSkbView.setBalloonHint(mBalloonOnKey, mBalloonPopup, true); 358 359 mPopupSkb.setContentView(mPopupSkbView); 360 mPopupSkb.setWidth(skb.getSkbCoreWidth() 361 + mPopupSkbView.getPaddingLeft() 362 + mPopupSkbView.getPaddingRight()); 363 mPopupSkb.setHeight(skb.getSkbCoreHeight() 364 + mPopupSkbView.getPaddingTop() 365 + mPopupSkbView.getPaddingBottom()); 366 367 getLocationInWindow(mXyPosTmp); 368 mPopupSkb.showAtLocation(this, Gravity.NO_GRAVITY, mPopupX, mPopupY 369 + mXyPosTmp[1]); 370 mPopupSkbShow = true; 371 mPopupSkbNoResponse = true; 372 // Invalidate itself to dim the current soft keyboards. 373 dimSoftKeyboard(true); 374 resetKeyPress(0); 375 } 376 } 377 378 private void dimSoftKeyboard(boolean dimSkb) { 379 mMajorView.dimSoftKeyboard(dimSkb); 380 } 381 382 private void dismissPopupSkb() { 383 mPopupSkb.dismiss(); 384 mPopupSkbShow = false; 385 dimSoftKeyboard(false); 386 resetKeyPress(0); 387 } 388 389 private void resetKeyPress(long delay) { 390 mLongPressTimer.removeTimer(); 391 392 if (null != mSkv) { 393 mSkv.resetKeyPress(delay); 394 } 395 } 396 397 public boolean handleBack(boolean realAction) { 398 if (mPopupSkbShow) { 399 if (!realAction) return true; 400 401 dismissPopupSkb(); 402 mDiscardEvent = true; 403 return true; 404 } 405 return false; 406 } 407 408 public void dismissPopups() { 409 handleBack(true); 410 resetKeyPress(0); 411 } 412 413 @Override 414 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 415 Environment env = Environment.getInstance(); 416 int measuredWidth = env.getScreenWidth(); 417 int measuredHeight = getPaddingTop(); 418 measuredHeight += env.getSkbHeight(); 419 widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, 420 MeasureSpec.EXACTLY); 421 heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, 422 MeasureSpec.EXACTLY); 423 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 424 } 425 426 @Override 427 public boolean onTouchEvent(MotionEvent event) { 428 super.onTouchEvent(event); 429 430 if (mSkbFlipper.isFlipping()) { 431 resetKeyPress(0); 432 return true; 433 } 434 435 int x = (int) event.getX(); 436 int y = (int) event.getY(); 437 // Bias correction 438 y = y + mYBiasCorrection; 439 440 // Ignore short-distance movement event to get better performance. 441 if (event.getAction() == MotionEvent.ACTION_MOVE) { 442 if (Math.abs(x - mXLast) <= MOVE_TOLERANCE 443 && Math.abs(y - mYLast) <= MOVE_TOLERANCE) { 444 return true; 445 } 446 } 447 448 mXLast = x; 449 mYLast = y; 450 451 if (!mPopupSkbShow) { 452 if (mGestureDetector.onTouchEvent(event)) { 453 resetKeyPress(0); 454 mDiscardEvent = true; 455 return true; 456 } 457 } 458 459 switch (event.getAction()) { 460 case MotionEvent.ACTION_DOWN: 461 resetKeyPress(0); 462 463 mWaitForTouchUp = true; 464 mDiscardEvent = false; 465 466 mSkv = null; 467 mSoftKeyDown = null; 468 mSkv = inKeyboardView(x, y, mSkvPosInContainer); 469 if (null != mSkv) { 470 mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y 471 - mSkvPosInContainer[1], mLongPressTimer, false); 472 } 473 break; 474 475 case MotionEvent.ACTION_MOVE: 476 if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) { 477 break; 478 } 479 if (mDiscardEvent) { 480 resetKeyPress(0); 481 break; 482 } 483 484 if (mPopupSkbShow && mPopupSkbNoResponse) { 485 break; 486 } 487 488 SoftKeyboardView skv = inKeyboardView(x, y, mSkvPosInContainer); 489 if (null != skv) { 490 if (skv != mSkv) { 491 mSkv = skv; 492 mSoftKeyDown = mSkv.onKeyPress(x - mSkvPosInContainer[0], y 493 - mSkvPosInContainer[1], mLongPressTimer, true); 494 } else if (null != skv) { 495 if (null != mSkv) { 496 mSoftKeyDown = mSkv.onKeyMove( 497 x - mSkvPosInContainer[0], y 498 - mSkvPosInContainer[1]); 499 if (null == mSoftKeyDown) { 500 mDiscardEvent = true; 501 } 502 } 503 } 504 } 505 break; 506 507 case MotionEvent.ACTION_UP: 508 if (mDiscardEvent) { 509 resetKeyPress(0); 510 break; 511 } 512 513 mWaitForTouchUp = false; 514 515 // The view which got the {@link MotionEvent#ACTION_DOWN} event is 516 // always used to handle this event. 517 if (null != mSkv) { 518 mSkv.onKeyRelease(x - mSkvPosInContainer[0], y 519 - mSkvPosInContainer[1]); 520 } 521 522 if (!mPopupSkbShow || !mPopupSkbNoResponse) { 523 responseKeyEvent(mSoftKeyDown); 524 } 525 526 if (mSkv == mPopupSkbView && !mPopupSkbNoResponse) { 527 dismissPopupSkb(); 528 } 529 mPopupSkbNoResponse = false; 530 break; 531 532 case MotionEvent.ACTION_CANCEL: 533 break; 534 } 535 536 if (null == mSkv) { 537 return false; 538 } 539 540 return true; 541 } 542 543 // Function for interface OnTouchListener, it is used to handle touch events 544 // which will be delivered to the popup soft keyboard view. 545 public boolean onTouch(View v, MotionEvent event) { 546 // Translate the event to fit to the container. 547 MotionEvent newEv = MotionEvent.obtain(event.getDownTime(), event 548 .getEventTime(), event.getAction(), event.getX() + mPopupX, 549 event.getY() + mPopupY, event.getPressure(), event.getSize(), 550 event.getMetaState(), event.getXPrecision(), event 551 .getYPrecision(), event.getDeviceId(), event 552 .getEdgeFlags()); 553 boolean ret = onTouchEvent(newEv); 554 return ret; 555 } 556 557 class LongPressTimer extends Handler implements Runnable { 558 /** 559 * When user presses a key for a long time, the timeout interval to 560 * generate first {@link #LONG_PRESS_KEYNUM1} key events. 561 */ 562 public static final int LONG_PRESS_TIMEOUT1 = 500; 563 564 /** 565 * When user presses a key for a long time, after the first 566 * {@link #LONG_PRESS_KEYNUM1} key events, this timeout interval will be 567 * used. 568 */ 569 private static final int LONG_PRESS_TIMEOUT2 = 100; 570 571 /** 572 * When user presses a key for a long time, after the first 573 * {@link #LONG_PRESS_KEYNUM2} key events, this timeout interval will be 574 * used. 575 */ 576 private static final int LONG_PRESS_TIMEOUT3 = 100; 577 578 /** 579 * When user presses a key for a long time, after the first 580 * {@link #LONG_PRESS_KEYNUM1} key events, timeout interval 581 * {@link #LONG_PRESS_TIMEOUT2} will be used instead. 582 */ 583 public static final int LONG_PRESS_KEYNUM1 = 1; 584 585 /** 586 * When user presses a key for a long time, after the first 587 * {@link #LONG_PRESS_KEYNUM2} key events, timeout interval 588 * {@link #LONG_PRESS_TIMEOUT3} will be used instead. 589 */ 590 public static final int LONG_PRESS_KEYNUM2 = 3; 591 592 SkbContainer mSkbContainer; 593 594 private int mResponseTimes = 0; 595 596 public LongPressTimer(SkbContainer skbContainer) { 597 mSkbContainer = skbContainer; 598 } 599 600 public void startTimer() { 601 postAtTime(this, SystemClock.uptimeMillis() + LONG_PRESS_TIMEOUT1); 602 mResponseTimes = 0; 603 } 604 605 public boolean removeTimer() { 606 removeCallbacks(this); 607 return true; 608 } 609 610 public void run() { 611 if (mWaitForTouchUp) { 612 mResponseTimes++; 613 if (mSoftKeyDown.repeatable()) { 614 if (mSoftKeyDown.isUserDefKey()) { 615 if (1 == mResponseTimes) { 616 if (mInputModeSwitcher 617 .tryHandleLongPressSwitch(mSoftKeyDown.mKeyCode)) { 618 mDiscardEvent = true; 619 resetKeyPress(0); 620 } 621 } 622 } else { 623 responseKeyEvent(mSoftKeyDown); 624 long timeout; 625 if (mResponseTimes < LONG_PRESS_KEYNUM1) { 626 timeout = LONG_PRESS_TIMEOUT1; 627 } else if (mResponseTimes < LONG_PRESS_KEYNUM2) { 628 timeout = LONG_PRESS_TIMEOUT2; 629 } else { 630 timeout = LONG_PRESS_TIMEOUT3; 631 } 632 postAtTime(this, SystemClock.uptimeMillis() + timeout); 633 } 634 } else { 635 if (1 == mResponseTimes) { 636 popupSymbols(); 637 } 638 } 639 } 640 } 641 } 642} 643