Home | History | Annotate | Download | only in pinyin
      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