• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
CandidateView(Context context, AttributeSet attrs)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint, GestureDetector gestureDetector, CandidateViewListener cvListener)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 
setDecodingInfo(DecodingInfo decInfo)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 
getActiveCandiatePosInPage()303     public int getActiveCandiatePosInPage() {
304         return mActiveCandInPage;
305     }
306 
getActiveCandiatePosGlobal()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      */
showPage(int pageNo, int activeCandInPage, boolean enableActiveHighlight)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 
enableActiveHighlight(boolean enableActiveHighlight)337     public void enableActiveHighlight(boolean enableActiveHighlight) {
338         if (enableActiveHighlight == mEnableActiveHighlight) return;
339 
340         mEnableActiveHighlight = enableActiveHighlight;
341         invalidate();
342     }
343 
activeCursorForward()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 
activeCurseBackward()355     public boolean activeCurseBackward() {
356         if (mActiveCandInPage > 0) {
357             showPage(mPageNo, mActiveCandInPage - 1, true);
358             return true;
359         }
360         return false;
361     }
362 
onSizeChanged()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 
calculatePage(int pageNo)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
onDraw(Canvas canvas)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