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 com.android.inputmethod.pinyin.PinyinIME.DecodingInfo;
20
21import java.util.Vector;
22
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.RectF;
29import android.graphics.Paint.FontMetricsInt;
30import android.graphics.drawable.Drawable;
31import android.os.Handler;
32import android.util.AttributeSet;
33import android.view.GestureDetector;
34import android.view.MotionEvent;
35import 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 */
41public 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