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 17 package com.android.inputmethod.pinyin; 18 19 import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo; 20 21 import java.util.Vector; 22 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.Canvas; 27 import android.graphics.Paint; 28 import android.graphics.RectF; 29 import android.graphics.Paint.FontMetricsInt; 30 import android.graphics.drawable.Drawable; 31 import android.os.Handler; 32 import android.util.AttributeSet; 33 import android.view.GestureDetector; 34 import android.view.MotionEvent; 35 import android.view.View; 36 37 /** 38 * View to show candidate list. There two candidate view instances which are 39 * used to show animation when user navigates between pages. 40 */ 41 public class CandidateView extends View { 42 /** 43 * The minimum width to show a item. 44 */ 45 private static final float MIN_ITEM_WIDTH = 22; 46 47 /** 48 * Suspension points used to display long items. 49 */ 50 private static final String SUSPENSION_POINTS = "..."; 51 52 /** 53 * The width to draw candidates. 54 */ 55 private int mContentWidth; 56 57 /** 58 * The height to draw candidate content. 59 */ 60 private int mContentHeight; 61 62 /** 63 * Whether footnotes are displayed. Footnote is shown when hardware keyboard 64 * is available. 65 */ 66 private boolean mShowFootnote = true; 67 68 /** 69 * Balloon hint for candidate press/release. 70 */ 71 private BalloonHint mBalloonHint; 72 73 /** 74 * Desired position of the balloon to the input view. 75 */ 76 private int mHintPositionToInputView[] = new int[2]; 77 78 /** 79 * Decoding result to show. 80 */ 81 private DecodingInfo mDecInfo; 82 83 /** 84 * Listener used to notify IME that user clicks a candidate, or navigate 85 * between them. 86 */ 87 private CandidateViewListener mCvListener; 88 89 /** 90 * Used to notify the container to update the status of forward/backward 91 * arrows. 92 */ 93 private ArrowUpdater mArrowUpdater; 94 95 /** 96 * If true, update the arrow status when drawing candidates. 97 */ 98 private boolean mUpdateArrowStatusWhenDraw = false; 99 100 /** 101 * Page number of the page displayed in this view. 102 */ 103 private int mPageNo; 104 105 /** 106 * Active candidate position in this page. 107 */ 108 private int mActiveCandInPage; 109 110 /** 111 * Used to decided whether the active candidate should be highlighted or 112 * not. If user changes focus to composing view (The view to show Pinyin 113 * string), the highlight in candidate view should be removed. 114 */ 115 private boolean mEnableActiveHighlight = true; 116 117 /** 118 * The page which is just calculated. 119 */ 120 private int mPageNoCalculated = -1; 121 122 /** 123 * The Drawable used to display as the background of the high-lighted item. 124 */ 125 private Drawable mActiveCellDrawable; 126 127 /** 128 * The Drawable used to display as separators between candidates. 129 */ 130 private Drawable mSeparatorDrawable; 131 132 /** 133 * Color to draw normal candidates generated by IME. 134 */ 135 private int mImeCandidateColor; 136 137 /** 138 * Color to draw normal candidates Recommended by application. 139 */ 140 private int mRecommendedCandidateColor; 141 142 /** 143 * Color to draw the normal(not highlighted) candidates, it can be one of 144 * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}. 145 */ 146 private int mNormalCandidateColor; 147 148 /** 149 * Color to draw the active(highlighted) candidates, including candidates 150 * from IME and candidates from application. 151 */ 152 private int mActiveCandidateColor; 153 154 /** 155 * Text size to draw candidates generated by IME. 156 */ 157 private int mImeCandidateTextSize; 158 159 /** 160 * Text size to draw candidates recommended by application. 161 */ 162 private int mRecommendedCandidateTextSize; 163 164 /** 165 * The current text size to draw candidates. It can be one of 166 * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}. 167 */ 168 private int mCandidateTextSize; 169 170 /** 171 * Paint used to draw candidates. 172 */ 173 private Paint mCandidatesPaint; 174 175 /** 176 * Used to draw footnote. 177 */ 178 private Paint mFootnotePaint; 179 180 /** 181 * The width to show suspension points. 182 */ 183 private float mSuspensionPointsWidth; 184 185 /** 186 * Rectangle used to draw the active candidate. 187 */ 188 private RectF mActiveCellRect; 189 190 /** 191 * Left and right margins for a candidate. It is specified in xml, and is 192 * the minimum margin for a candidate. The actual gap between two candidates 193 * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}. 194 * getIntrinsicWidth(). Because length of candidate is not fixed, there can 195 * be some extra space after the last candidate in the current page. In 196 * order to achieve best look-and-feel, this extra space will be divided and 197 * allocated to each candidates. 198 */ 199 private float mCandidateMargin; 200 201 /** 202 * Left and right extra margins for a candidate. 203 */ 204 private float mCandidateMarginExtra; 205 206 /** 207 * Rectangles for the candidates in this page. 208 **/ 209 private Vector<RectF> mCandRects; 210 211 /** 212 * FontMetricsInt used to measure the size of candidates. 213 */ 214 private FontMetricsInt mFmiCandidates; 215 216 /** 217 * FontMetricsInt used to measure the size of footnotes. 218 */ 219 private FontMetricsInt mFmiFootnote; 220 221 private PressTimer mTimer = new PressTimer(); 222 223 private GestureDetector mGestureDetector; 224 225 private int mLocationTmp[] = new int[2]; 226 227 public CandidateView(Context context, AttributeSet attrs) { 228 super(context, attrs); 229 230 Resources r = context.getResources(); 231 232 Configuration conf = r.getConfiguration(); 233 if (conf.keyboard == Configuration.KEYBOARD_NOKEYS 234 || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { 235 mShowFootnote = false; 236 } 237 238 mActiveCellDrawable = r.getDrawable(R.drawable.candidate_hl_bg); 239 mSeparatorDrawable = r.getDrawable(R.drawable.candidates_vertical_line); 240 mCandidateMargin = r.getDimension(R.dimen.candidate_margin_left_right); 241 242 mImeCandidateColor = r.getColor(R.color.candidate_color); 243 mRecommendedCandidateColor = r.getColor(R.color.recommended_candidate_color); 244 mNormalCandidateColor = mImeCandidateColor; 245 mActiveCandidateColor = r.getColor(R.color.active_candidate_color); 246 247 mCandidatesPaint = new Paint(); 248 mCandidatesPaint.setAntiAlias(true); 249 250 mFootnotePaint = new Paint(); 251 mFootnotePaint.setAntiAlias(true); 252 mFootnotePaint.setColor(r.getColor(R.color.footnote_color)); 253 mActiveCellRect = new RectF(); 254 255 mCandRects = new Vector<RectF>(); 256 } 257 258 @Override 259 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 260 int mOldWidth = getMeasuredWidth(); 261 int mOldHeight = getMeasuredHeight(); 262 263 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), 264 widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), 265 heightMeasureSpec)); 266 267 if (mOldWidth != getMeasuredWidth() || mOldHeight != getMeasuredHeight()) { 268 onSizeChanged(); 269 } 270 } 271 272 public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint, 273 GestureDetector gestureDetector, CandidateViewListener cvListener) { 274 mArrowUpdater = arrowUpdater; 275 mBalloonHint = balloonHint; 276 mGestureDetector = gestureDetector; 277 mCvListener = cvListener; 278 } 279 280 public void setDecodingInfo(DecodingInfo decInfo) { 281 if (null == decInfo) return; 282 mDecInfo = decInfo; 283 mPageNoCalculated = -1; 284 285 if (mDecInfo.candidatesFromApp()) { 286 mNormalCandidateColor = mRecommendedCandidateColor; 287 mCandidateTextSize = mRecommendedCandidateTextSize; 288 } else { 289 mNormalCandidateColor = mImeCandidateColor; 290 mCandidateTextSize = mImeCandidateTextSize; 291 } 292 if (mCandidatesPaint.getTextSize() != mCandidateTextSize) { 293 mCandidatesPaint.setTextSize(mCandidateTextSize); 294 mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); 295 mSuspensionPointsWidth = 296 mCandidatesPaint.measureText(SUSPENSION_POINTS); 297 } 298 299 // Remove any pending timer for the previous list. 300 mTimer.removeTimer(); 301 } 302 303 public int getActiveCandiatePosInPage() { 304 return mActiveCandInPage; 305 } 306 307 public int getActiveCandiatePosGlobal() { 308 return mDecInfo.mPageStart.get(mPageNo) + mActiveCandInPage; 309 } 310 311 /** 312 * Show a page in the decoding result set previously. 313 * 314 * @param pageNo Which page to show. 315 * @param activeCandInPage Which candidate should be set as active item. 316 * @param enableActiveHighlight When false, active item will not be 317 * highlighted. 318 */ 319 public void showPage(int pageNo, int activeCandInPage, 320 boolean enableActiveHighlight) { 321 if (null == mDecInfo) return; 322 mPageNo = pageNo; 323 mActiveCandInPage = activeCandInPage; 324 if (mEnableActiveHighlight != enableActiveHighlight) { 325 mEnableActiveHighlight = enableActiveHighlight; 326 } 327 328 if (!calculatePage(mPageNo)) { 329 mUpdateArrowStatusWhenDraw = true; 330 } else { 331 mUpdateArrowStatusWhenDraw = false; 332 } 333 334 invalidate(); 335 } 336 337 public void enableActiveHighlight(boolean enableActiveHighlight) { 338 if (enableActiveHighlight == mEnableActiveHighlight) return; 339 340 mEnableActiveHighlight = enableActiveHighlight; 341 invalidate(); 342 } 343 344 public boolean activeCursorForward() { 345 if (!mDecInfo.pageReady(mPageNo)) return false; 346 int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) 347 - mDecInfo.mPageStart.get(mPageNo); 348 if (mActiveCandInPage + 1 < pageSize) { 349 showPage(mPageNo, mActiveCandInPage + 1, true); 350 return true; 351 } 352 return false; 353 } 354 355 public boolean activeCurseBackward() { 356 if (mActiveCandInPage > 0) { 357 showPage(mPageNo, mActiveCandInPage - 1, true); 358 return true; 359 } 360 return false; 361 } 362 363 private void onSizeChanged() { 364 mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight; 365 mContentHeight = (int) ((getMeasuredHeight() - mPaddingTop - mPaddingBottom) * 0.95f); 366 /** 367 * How to decide the font size if the height for display is given? 368 * Now it is implemented in a stupid way. 369 */ 370 int textSize = 1; 371 mCandidatesPaint.setTextSize(textSize); 372 mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); 373 while (mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight) { 374 textSize++; 375 mCandidatesPaint.setTextSize(textSize); 376 mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); 377 } 378 379 mImeCandidateTextSize = textSize; 380 mRecommendedCandidateTextSize = textSize * 3 / 4; 381 if (null == mDecInfo) { 382 mCandidateTextSize = mImeCandidateTextSize; 383 mCandidatesPaint.setTextSize(mCandidateTextSize); 384 mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); 385 mSuspensionPointsWidth = 386 mCandidatesPaint.measureText(SUSPENSION_POINTS); 387 } else { 388 // Reset the decoding information to update members for painting. 389 setDecodingInfo(mDecInfo); 390 } 391 392 textSize = 1; 393 mFootnotePaint.setTextSize(textSize); 394 mFmiFootnote = mFootnotePaint.getFontMetricsInt(); 395 while (mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2) { 396 textSize++; 397 mFootnotePaint.setTextSize(textSize); 398 mFmiFootnote = mFootnotePaint.getFontMetricsInt(); 399 } 400 textSize--; 401 mFootnotePaint.setTextSize(textSize); 402 mFmiFootnote = mFootnotePaint.getFontMetricsInt(); 403 404 // When the size is changed, the first page will be displayed. 405 mPageNo = 0; 406 mActiveCandInPage = 0; 407 } 408 409 private boolean calculatePage(int pageNo) { 410 if (pageNo == mPageNoCalculated) return true; 411 412 mContentWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight; 413 mContentHeight = (int) ((getMeasuredHeight() - mPaddingTop - mPaddingBottom) * 0.95f); 414 415 if (mContentWidth <= 0 || mContentHeight <= 0) return false; 416 417 int candSize = mDecInfo.mCandidatesList.size(); 418 419 // If the size of page exists, only calculate the extra margin. 420 boolean onlyExtraMargin = false; 421 int fromPage = mDecInfo.mPageStart.size() - 1; 422 if (mDecInfo.mPageStart.size() > pageNo + 1) { 423 onlyExtraMargin = true; 424 fromPage = pageNo; 425 } 426 427 // If the previous pages have no information, calculate them first. 428 for (int p = fromPage; p <= pageNo; p++) { 429 int pStart = mDecInfo.mPageStart.get(p); 430 int pSize = 0; 431 int charNum = 0; 432 float lastItemWidth = 0; 433 434 float xPos; 435 xPos = 0; 436 xPos += mSeparatorDrawable.getIntrinsicWidth(); 437 while (xPos < mContentWidth && pStart + pSize < candSize) { 438 int itemPos = pStart + pSize; 439 String itemStr = mDecInfo.mCandidatesList.get(itemPos); 440 float itemWidth = mCandidatesPaint.measureText(itemStr); 441 if (itemWidth < MIN_ITEM_WIDTH) itemWidth = MIN_ITEM_WIDTH; 442 443 itemWidth += mCandidateMargin * 2; 444 itemWidth += mSeparatorDrawable.getIntrinsicWidth(); 445 if (xPos + itemWidth < mContentWidth || 0 == pSize) { 446 xPos += itemWidth; 447 lastItemWidth = itemWidth; 448 pSize++; 449 charNum += itemStr.length(); 450 } else { 451 break; 452 } 453 } 454 if (!onlyExtraMargin) { 455 mDecInfo.mPageStart.add(pStart + pSize); 456 mDecInfo.mCnToPage.add(mDecInfo.mCnToPage.get(p) + charNum); 457 } 458 459 float marginExtra = (mContentWidth - xPos) / pSize / 2; 460 461 if (mContentWidth - xPos > lastItemWidth) { 462 // Must be the last page, because if there are more items, 463 // the next item's width must be less than lastItemWidth. 464 // In this case, if the last margin is less than the current 465 // one, the last margin can be used, so that the 466 // look-and-feeling will be the same as the previous page. 467 if (mCandidateMarginExtra <= marginExtra) { 468 marginExtra = mCandidateMarginExtra; 469 } 470 } else if (pSize == 1) { 471 marginExtra = 0; 472 } 473 mCandidateMarginExtra = marginExtra; 474 } 475 mPageNoCalculated = pageNo; 476 return true; 477 } 478 479 @Override 480 protected void onDraw(Canvas canvas) { 481 super.onDraw(canvas); 482 // The invisible candidate view(the one which is not in foreground) can 483 // also be called to drawn, but its decoding result and candidate list 484 // may be empty. 485 if (null == mDecInfo || mDecInfo.isCandidatesListEmpty()) return; 486 487 // Calculate page. If the paging information is ready, the function will 488 // return at once. 489 calculatePage(mPageNo); 490 491 int pStart = mDecInfo.mPageStart.get(mPageNo); 492 int pSize = mDecInfo.mPageStart.get(mPageNo + 1) - pStart; 493 float candMargin = mCandidateMargin + mCandidateMarginExtra; 494 if (mActiveCandInPage > pSize - 1) { 495 mActiveCandInPage = pSize - 1; 496 } 497 498 mCandRects.removeAllElements(); 499 500 float xPos = mPaddingLeft; 501 int yPos = (getMeasuredHeight() - 502 (mFmiCandidates.bottom - mFmiCandidates.top)) / 2 503 - mFmiCandidates.top; 504 xPos += drawVerticalSeparator(canvas, xPos); 505 for (int i = 0; i < pSize; i++) { 506 float footnoteSize = 0; 507 String footnote = null; 508 if (mShowFootnote) { 509 footnote = Integer.toString(i + 1); 510 footnoteSize = mFootnotePaint.measureText(footnote); 511 assert (footnoteSize < candMargin); 512 } 513 String cand = mDecInfo.mCandidatesList.get(pStart + i); 514 float candidateWidth = mCandidatesPaint.measureText(cand); 515 float centerOffset = 0; 516 if (candidateWidth < MIN_ITEM_WIDTH) { 517 centerOffset = (MIN_ITEM_WIDTH - candidateWidth) / 2; 518 candidateWidth = MIN_ITEM_WIDTH; 519 } 520 521 float itemTotalWidth = candidateWidth + 2 * candMargin; 522 523 if (mActiveCandInPage == i && mEnableActiveHighlight) { 524 mActiveCellRect.set(xPos, mPaddingTop + 1, xPos 525 + itemTotalWidth, getHeight() - mPaddingBottom - 1); 526 mActiveCellDrawable.setBounds((int) mActiveCellRect.left, 527 (int) mActiveCellRect.top, (int) mActiveCellRect.right, 528 (int) mActiveCellRect.bottom); 529 mActiveCellDrawable.draw(canvas); 530 } 531 532 if (mCandRects.size() < pSize) mCandRects.add(new RectF()); 533 mCandRects.elementAt(i).set(xPos - 1, yPos + mFmiCandidates.top, 534 xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom); 535 536 // Draw footnote 537 if (mShowFootnote) { 538 canvas.drawText(footnote, xPos + (candMargin - footnoteSize) 539 / 2, yPos, mFootnotePaint); 540 } 541 542 // Left margin 543 xPos += candMargin; 544 if (candidateWidth > mContentWidth - xPos - centerOffset) { 545 cand = getLimitedCandidateForDrawing(cand, 546 mContentWidth - xPos - centerOffset); 547 } 548 if (mActiveCandInPage == i && mEnableActiveHighlight) { 549 mCandidatesPaint.setColor(mActiveCandidateColor); 550 } else { 551 mCandidatesPaint.setColor(mNormalCandidateColor); 552 } 553 canvas.drawText(cand, xPos + centerOffset, yPos, 554 mCandidatesPaint); 555 556 // Candidate and right margin 557 xPos += candidateWidth + candMargin; 558 559 // Draw the separator between candidates. 560 xPos += drawVerticalSeparator(canvas, xPos); 561 } 562 563 // Update the arrow status of the container. 564 if (null != mArrowUpdater && mUpdateArrowStatusWhenDraw) { 565 mArrowUpdater.updateArrowStatus(); 566 mUpdateArrowStatusWhenDraw = false; 567 } 568 } 569 570 private String getLimitedCandidateForDrawing(String rawCandidate, 571 float widthToDraw) { 572 int subLen = rawCandidate.length(); 573 if (subLen <= 1) return rawCandidate; 574 do { 575 subLen--; 576 float width = mCandidatesPaint.measureText(rawCandidate, 0, subLen); 577 if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) { 578 return rawCandidate.substring(0, subLen) + 579 SUSPENSION_POINTS; 580 } 581 } while (true); 582 } 583 584 private float drawVerticalSeparator(Canvas canvas, float xPos) { 585 mSeparatorDrawable.setBounds((int) xPos, mPaddingTop, (int) xPos 586 + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight() 587 - mPaddingBottom); 588 mSeparatorDrawable.draw(canvas); 589 return mSeparatorDrawable.getIntrinsicWidth(); 590 } 591 592 private int mapToItemInPage(int x, int y) { 593 // mCandRects.size() == 0 happens when the page is set, but 594 // touch events occur before onDraw(). It usually happens with 595 // monkey test. 596 if (!mDecInfo.pageReady(mPageNo) || mPageNoCalculated != mPageNo 597 || mCandRects.size() == 0) { 598 return -1; 599 } 600 601 int pageStart = mDecInfo.mPageStart.get(mPageNo); 602 int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) - pageStart; 603 if (mCandRects.size() < pageSize) { 604 return -1; 605 } 606 607 // If not found, try to find the nearest one. 608 float nearestDis = Float.MAX_VALUE; 609 int nearest = -1; 610 for (int i = 0; i < pageSize; i++) { 611 RectF r = mCandRects.elementAt(i); 612 if (r.left < x && r.right > x && r.top < y && r.bottom > y) { 613 return i; 614 } 615 float disx = (r.left + r.right) / 2 - x; 616 float disy = (r.top + r.bottom) / 2 - y; 617 float dis = disx * disx + disy * disy; 618 if (dis < nearestDis) { 619 nearestDis = dis; 620 nearest = i; 621 } 622 } 623 624 return nearest; 625 } 626 627 // Because the candidate view under the current focused one may also get 628 // touching events. Here we just bypass the event to the container and let 629 // it decide which view should handle the event. 630 @Override 631 public boolean onTouchEvent(MotionEvent event) { 632 return super.onTouchEvent(event); 633 } 634 635 public boolean onTouchEventReal(MotionEvent event) { 636 // The page in the background can also be touched. 637 if (null == mDecInfo || !mDecInfo.pageReady(mPageNo) 638 || mPageNoCalculated != mPageNo) return true; 639 640 int x, y; 641 x = (int) event.getX(); 642 y = (int) event.getY(); 643 644 if (mGestureDetector.onTouchEvent(event)) { 645 mTimer.removeTimer(); 646 mBalloonHint.delayedDismiss(0); 647 return true; 648 } 649 650 int clickedItemInPage = -1; 651 652 switch (event.getAction()) { 653 case MotionEvent.ACTION_UP: 654 clickedItemInPage = mapToItemInPage(x, y); 655 if (clickedItemInPage >= 0) { 656 invalidate(); 657 mCvListener.onClickChoice(clickedItemInPage 658 + mDecInfo.mPageStart.get(mPageNo)); 659 } 660 mBalloonHint.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS); 661 break; 662 663 case MotionEvent.ACTION_DOWN: 664 clickedItemInPage = mapToItemInPage(x, y); 665 if (clickedItemInPage >= 0) { 666 showBalloon(clickedItemInPage, true); 667 mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, 668 clickedItemInPage); 669 } 670 break; 671 672 case MotionEvent.ACTION_CANCEL: 673 break; 674 675 case MotionEvent.ACTION_MOVE: 676 clickedItemInPage = mapToItemInPage(x, y); 677 if (clickedItemInPage >= 0 678 && (clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer 679 .getPageToShow())) { 680 showBalloon(clickedItemInPage, true); 681 mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, 682 clickedItemInPage); 683 } 684 } 685 return true; 686 } 687 688 private void showBalloon(int candPos, boolean delayedShow) { 689 mBalloonHint.removeTimer(); 690 691 RectF r = mCandRects.elementAt(candPos); 692 int desired_width = (int) (r.right - r.left); 693 int desired_height = (int) (r.bottom - r.top); 694 mBalloonHint.setBalloonConfig(mDecInfo.mCandidatesList 695 .get(mDecInfo.mPageStart.get(mPageNo) + candPos), 44, true, 696 mImeCandidateColor, desired_width, desired_height); 697 698 getLocationOnScreen(mLocationTmp); 699 mHintPositionToInputView[0] = mLocationTmp[0] 700 + (int) (r.left - (mBalloonHint.getWidth() - desired_width) / 2); 701 mHintPositionToInputView[1] = -mBalloonHint.getHeight(); 702 703 long delay = BalloonHint.TIME_DELAY_SHOW; 704 if (!delayedShow) delay = 0; 705 mBalloonHint.dismiss(); 706 if (!mBalloonHint.isShowing()) { 707 mBalloonHint.delayedShow(delay, mHintPositionToInputView); 708 } else { 709 mBalloonHint.delayedUpdate(0, mHintPositionToInputView, -1, -1); 710 } 711 } 712 713 private class PressTimer extends Handler implements Runnable { 714 private boolean mTimerPending = false; 715 private int mPageNoToShow; 716 private int mActiveCandOfPage; 717 718 public PressTimer() { 719 super(); 720 } 721 722 public void startTimer(long afterMillis, int pageNo, int activeInPage) { 723 mTimer.removeTimer(); 724 postDelayed(this, afterMillis); 725 mTimerPending = true; 726 mPageNoToShow = pageNo; 727 mActiveCandOfPage = activeInPage; 728 } 729 730 public int getPageToShow() { 731 return mPageNoToShow; 732 } 733 734 public int getActiveCandOfPageToShow() { 735 return mActiveCandOfPage; 736 } 737 738 public boolean removeTimer() { 739 if (mTimerPending) { 740 mTimerPending = false; 741 removeCallbacks(this); 742 return true; 743 } 744 return false; 745 } 746 747 public boolean isPending() { 748 return mTimerPending; 749 } 750 751 public void run() { 752 if (mPageNoToShow >= 0 && mActiveCandOfPage >= 0) { 753 // Always enable to highlight the clicked one. 754 showPage(mPageNoToShow, mActiveCandOfPage, true); 755 invalidate(); 756 } 757 mTimerPending = false; 758 } 759 } 760 } 761