| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.pinyin; |
| |
| import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo; |
| |
| import java.util.Vector; |
| |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.RectF; |
| import android.graphics.Paint.FontMetricsInt; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.util.AttributeSet; |
| import android.view.GestureDetector; |
| import android.view.MotionEvent; |
| import android.view.View; |
| |
| /** |
| * View to show candidate list. There two candidate view instances which are |
| * used to show animation when user navigates between pages. |
| */ |
| public class CandidateView extends View { |
| /** |
| * The minimum width to show a item. |
| */ |
| private static final float MIN_ITEM_WIDTH = 22; |
| |
| /** |
| * Suspension points used to display long items. |
| */ |
| private static final String SUSPENSION_POINTS = "..."; |
| |
| /** |
| * The width to draw candidates. |
| */ |
| private int mContentWidth; |
| |
| /** |
| * The height to draw candidate content. |
| */ |
| private int mContentHeight; |
| |
| /** |
| * Whether footnotes are displayed. Footnote is shown when hardware keyboard |
| * is available. |
| */ |
| private boolean mShowFootnote = true; |
| |
| /** |
| * Balloon hint for candidate press/release. |
| */ |
| private BalloonHint mBalloonHint; |
| |
| /** |
| * Desired position of the balloon to the input view. |
| */ |
| private int mHintPositionToInputView[] = new int[2]; |
| |
| /** |
| * Decoding result to show. |
| */ |
| private DecodingInfo mDecInfo; |
| |
| /** |
| * Listener used to notify IME that user clicks a candidate, or navigate |
| * between them. |
| */ |
| private CandidateViewListener mCvListener; |
| |
| /** |
| * Used to notify the container to update the status of forward/backward |
| * arrows. |
| */ |
| private ArrowUpdater mArrowUpdater; |
| |
| /** |
| * If true, update the arrow status when drawing candidates. |
| */ |
| private boolean mUpdateArrowStatusWhenDraw = false; |
| |
| /** |
| * Page number of the page displayed in this view. |
| */ |
| private int mPageNo; |
| |
| /** |
| * Active candidate position in this page. |
| */ |
| private int mActiveCandInPage; |
| |
| /** |
| * Used to decided whether the active candidate should be highlighted or |
| * not. If user changes focus to composing view (The view to show Pinyin |
| * string), the highlight in candidate view should be removed. |
| */ |
| private boolean mEnableActiveHighlight = true; |
| |
| /** |
| * The page which is just calculated. |
| */ |
| private int mPageNoCalculated = -1; |
| |
| /** |
| * The Drawable used to display as the background of the high-lighted item. |
| */ |
| private Drawable mActiveCellDrawable; |
| |
| /** |
| * The Drawable used to display as separators between candidates. |
| */ |
| private Drawable mSeparatorDrawable; |
| |
| /** |
| * Color to draw normal candidates generated by IME. |
| */ |
| private int mImeCandidateColor; |
| |
| /** |
| * Color to draw normal candidates Recommended by application. |
| */ |
| private int mRecommendedCandidateColor; |
| |
| /** |
| * Color to draw the normal(not highlighted) candidates, it can be one of |
| * {@link #mImeCandidateColor} or {@link #mRecommendedCandidateColor}. |
| */ |
| private int mNormalCandidateColor; |
| |
| /** |
| * Color to draw the active(highlighted) candidates, including candidates |
| * from IME and candidates from application. |
| */ |
| private int mActiveCandidateColor; |
| |
| /** |
| * Text size to draw candidates generated by IME. |
| */ |
| private int mImeCandidateTextSize; |
| |
| /** |
| * Text size to draw candidates recommended by application. |
| */ |
| private int mRecommendedCandidateTextSize; |
| |
| /** |
| * The current text size to draw candidates. It can be one of |
| * {@link #mImeCandidateTextSize} or {@link #mRecommendedCandidateTextSize}. |
| */ |
| private int mCandidateTextSize; |
| |
| /** |
| * Paint used to draw candidates. |
| */ |
| private Paint mCandidatesPaint; |
| |
| /** |
| * Used to draw footnote. |
| */ |
| private Paint mFootnotePaint; |
| |
| /** |
| * The width to show suspension points. |
| */ |
| private float mSuspensionPointsWidth; |
| |
| /** |
| * Rectangle used to draw the active candidate. |
| */ |
| private RectF mActiveCellRect; |
| |
| /** |
| * Left and right margins for a candidate. It is specified in xml, and is |
| * the minimum margin for a candidate. The actual gap between two candidates |
| * is 2 * {@link #mCandidateMargin} + {@link #mSeparatorDrawable}. |
| * getIntrinsicWidth(). Because length of candidate is not fixed, there can |
| * be some extra space after the last candidate in the current page. In |
| * order to achieve best look-and-feel, this extra space will be divided and |
| * allocated to each candidates. |
| */ |
| private float mCandidateMargin; |
| |
| /** |
| * Left and right extra margins for a candidate. |
| */ |
| private float mCandidateMarginExtra; |
| |
| /** |
| * Rectangles for the candidates in this page. |
| **/ |
| private Vector<RectF> mCandRects; |
| |
| /** |
| * FontMetricsInt used to measure the size of candidates. |
| */ |
| private FontMetricsInt mFmiCandidates; |
| |
| /** |
| * FontMetricsInt used to measure the size of footnotes. |
| */ |
| private FontMetricsInt mFmiFootnote; |
| |
| private PressTimer mTimer = new PressTimer(); |
| |
| private GestureDetector mGestureDetector; |
| |
| private int mLocationTmp[] = new int[2]; |
| |
| public CandidateView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| |
| Resources r = context.getResources(); |
| |
| Configuration conf = r.getConfiguration(); |
| if (conf.keyboard == Configuration.KEYBOARD_NOKEYS |
| || conf.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { |
| mShowFootnote = false; |
| } |
| |
| mActiveCellDrawable = r.getDrawable(R.drawable.candidate_hl_bg); |
| mSeparatorDrawable = r.getDrawable(R.drawable.candidates_vertical_line); |
| mCandidateMargin = r.getDimension(R.dimen.candidate_margin_left_right); |
| |
| mImeCandidateColor = r.getColor(R.color.candidate_color); |
| mRecommendedCandidateColor = r.getColor(R.color.recommended_candidate_color); |
| mNormalCandidateColor = mImeCandidateColor; |
| mActiveCandidateColor = r.getColor(R.color.active_candidate_color); |
| |
| mCandidatesPaint = new Paint(); |
| mCandidatesPaint.setAntiAlias(true); |
| |
| mFootnotePaint = new Paint(); |
| mFootnotePaint.setAntiAlias(true); |
| mFootnotePaint.setColor(r.getColor(R.color.footnote_color)); |
| mActiveCellRect = new RectF(); |
| |
| mCandRects = new Vector<RectF>(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| int mOldWidth = mMeasuredWidth; |
| int mOldHeight = mMeasuredHeight; |
| |
| setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), |
| widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), |
| heightMeasureSpec)); |
| |
| if (mOldWidth != mMeasuredWidth || mOldHeight != mMeasuredHeight) { |
| onSizeChanged(); |
| } |
| } |
| |
| public void initialize(ArrowUpdater arrowUpdater, BalloonHint balloonHint, |
| GestureDetector gestureDetector, CandidateViewListener cvListener) { |
| mArrowUpdater = arrowUpdater; |
| mBalloonHint = balloonHint; |
| mGestureDetector = gestureDetector; |
| mCvListener = cvListener; |
| } |
| |
| public void setDecodingInfo(DecodingInfo decInfo) { |
| if (null == decInfo) return; |
| mDecInfo = decInfo; |
| mPageNoCalculated = -1; |
| |
| if (mDecInfo.candidatesFromApp()) { |
| mNormalCandidateColor = mRecommendedCandidateColor; |
| mCandidateTextSize = mRecommendedCandidateTextSize; |
| } else { |
| mNormalCandidateColor = mImeCandidateColor; |
| mCandidateTextSize = mImeCandidateTextSize; |
| } |
| if (mCandidatesPaint.getTextSize() != mCandidateTextSize) { |
| mCandidatesPaint.setTextSize(mCandidateTextSize); |
| mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); |
| mSuspensionPointsWidth = |
| mCandidatesPaint.measureText(SUSPENSION_POINTS); |
| } |
| |
| // Remove any pending timer for the previous list. |
| mTimer.removeTimer(); |
| } |
| |
| public int getActiveCandiatePosInPage() { |
| return mActiveCandInPage; |
| } |
| |
| public int getActiveCandiatePosGlobal() { |
| return mDecInfo.mPageStart.get(mPageNo) + mActiveCandInPage; |
| } |
| |
| /** |
| * Show a page in the decoding result set previously. |
| * |
| * @param pageNo Which page to show. |
| * @param activeCandInPage Which candidate should be set as active item. |
| * @param enableActiveHighlight When false, active item will not be |
| * highlighted. |
| */ |
| public void showPage(int pageNo, int activeCandInPage, |
| boolean enableActiveHighlight) { |
| if (null == mDecInfo) return; |
| mPageNo = pageNo; |
| mActiveCandInPage = activeCandInPage; |
| if (mEnableActiveHighlight != enableActiveHighlight) { |
| mEnableActiveHighlight = enableActiveHighlight; |
| } |
| |
| if (!calculatePage(mPageNo)) { |
| mUpdateArrowStatusWhenDraw = true; |
| } else { |
| mUpdateArrowStatusWhenDraw = false; |
| } |
| |
| invalidate(); |
| } |
| |
| public void enableActiveHighlight(boolean enableActiveHighlight) { |
| if (enableActiveHighlight == mEnableActiveHighlight) return; |
| |
| mEnableActiveHighlight = enableActiveHighlight; |
| invalidate(); |
| } |
| |
| public boolean activeCursorForward() { |
| if (!mDecInfo.pageReady(mPageNo)) return false; |
| int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) |
| - mDecInfo.mPageStart.get(mPageNo); |
| if (mActiveCandInPage + 1 < pageSize) { |
| showPage(mPageNo, mActiveCandInPage + 1, true); |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean activeCurseBackward() { |
| if (mActiveCandInPage > 0) { |
| showPage(mPageNo, mActiveCandInPage - 1, true); |
| return true; |
| } |
| return false; |
| } |
| |
| private void onSizeChanged() { |
| mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; |
| mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f); |
| /** |
| * How to decide the font size if the height for display is given? |
| * Now it is implemented in a stupid way. |
| */ |
| int textSize = 1; |
| mCandidatesPaint.setTextSize(textSize); |
| mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); |
| while (mFmiCandidates.bottom - mFmiCandidates.top < mContentHeight) { |
| textSize++; |
| mCandidatesPaint.setTextSize(textSize); |
| mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); |
| } |
| |
| mImeCandidateTextSize = textSize; |
| mRecommendedCandidateTextSize = textSize * 3 / 4; |
| if (null == mDecInfo) { |
| mCandidateTextSize = mImeCandidateTextSize; |
| mCandidatesPaint.setTextSize(mCandidateTextSize); |
| mFmiCandidates = mCandidatesPaint.getFontMetricsInt(); |
| mSuspensionPointsWidth = |
| mCandidatesPaint.measureText(SUSPENSION_POINTS); |
| } else { |
| // Reset the decoding information to update members for painting. |
| setDecodingInfo(mDecInfo); |
| } |
| |
| textSize = 1; |
| mFootnotePaint.setTextSize(textSize); |
| mFmiFootnote = mFootnotePaint.getFontMetricsInt(); |
| while (mFmiFootnote.bottom - mFmiFootnote.top < mContentHeight / 2) { |
| textSize++; |
| mFootnotePaint.setTextSize(textSize); |
| mFmiFootnote = mFootnotePaint.getFontMetricsInt(); |
| } |
| textSize--; |
| mFootnotePaint.setTextSize(textSize); |
| mFmiFootnote = mFootnotePaint.getFontMetricsInt(); |
| |
| // When the size is changed, the first page will be displayed. |
| mPageNo = 0; |
| mActiveCandInPage = 0; |
| } |
| |
| private boolean calculatePage(int pageNo) { |
| if (pageNo == mPageNoCalculated) return true; |
| |
| mContentWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight; |
| mContentHeight = (int) ((mMeasuredHeight - mPaddingTop - mPaddingBottom) * 0.95f); |
| |
| if (mContentWidth <= 0 || mContentHeight <= 0) return false; |
| |
| int candSize = mDecInfo.mCandidatesList.size(); |
| |
| // If the size of page exists, only calculate the extra margin. |
| boolean onlyExtraMargin = false; |
| int fromPage = mDecInfo.mPageStart.size() - 1; |
| if (mDecInfo.mPageStart.size() > pageNo + 1) { |
| onlyExtraMargin = true; |
| fromPage = pageNo; |
| } |
| |
| // If the previous pages have no information, calculate them first. |
| for (int p = fromPage; p <= pageNo; p++) { |
| int pStart = mDecInfo.mPageStart.get(p); |
| int pSize = 0; |
| int charNum = 0; |
| float lastItemWidth = 0; |
| |
| float xPos; |
| xPos = 0; |
| xPos += mSeparatorDrawable.getIntrinsicWidth(); |
| while (xPos < mContentWidth && pStart + pSize < candSize) { |
| int itemPos = pStart + pSize; |
| String itemStr = mDecInfo.mCandidatesList.get(itemPos); |
| float itemWidth = mCandidatesPaint.measureText(itemStr); |
| if (itemWidth < MIN_ITEM_WIDTH) itemWidth = MIN_ITEM_WIDTH; |
| |
| itemWidth += mCandidateMargin * 2; |
| itemWidth += mSeparatorDrawable.getIntrinsicWidth(); |
| if (xPos + itemWidth < mContentWidth || 0 == pSize) { |
| xPos += itemWidth; |
| lastItemWidth = itemWidth; |
| pSize++; |
| charNum += itemStr.length(); |
| } else { |
| break; |
| } |
| } |
| if (!onlyExtraMargin) { |
| mDecInfo.mPageStart.add(pStart + pSize); |
| mDecInfo.mCnToPage.add(mDecInfo.mCnToPage.get(p) + charNum); |
| } |
| |
| float marginExtra = (mContentWidth - xPos) / pSize / 2; |
| |
| if (mContentWidth - xPos > lastItemWidth) { |
| // Must be the last page, because if there are more items, |
| // the next item's width must be less than lastItemWidth. |
| // In this case, if the last margin is less than the current |
| // one, the last margin can be used, so that the |
| // look-and-feeling will be the same as the previous page. |
| if (mCandidateMarginExtra <= marginExtra) { |
| marginExtra = mCandidateMarginExtra; |
| } |
| } else if (pSize == 1) { |
| marginExtra = 0; |
| } |
| mCandidateMarginExtra = marginExtra; |
| } |
| mPageNoCalculated = pageNo; |
| return true; |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| // The invisible candidate view(the one which is not in foreground) can |
| // also be called to drawn, but its decoding result and candidate list |
| // may be empty. |
| if (null == mDecInfo || mDecInfo.isCandidatesListEmpty()) return; |
| |
| // Calculate page. If the paging information is ready, the function will |
| // return at once. |
| calculatePage(mPageNo); |
| |
| int pStart = mDecInfo.mPageStart.get(mPageNo); |
| int pSize = mDecInfo.mPageStart.get(mPageNo + 1) - pStart; |
| float candMargin = mCandidateMargin + mCandidateMarginExtra; |
| if (mActiveCandInPage > pSize - 1) { |
| mActiveCandInPage = pSize - 1; |
| } |
| |
| mCandRects.removeAllElements(); |
| |
| float xPos = mPaddingLeft; |
| int yPos = (getMeasuredHeight() - |
| (mFmiCandidates.bottom - mFmiCandidates.top)) / 2 |
| - mFmiCandidates.top; |
| xPos += drawVerticalSeparator(canvas, xPos); |
| for (int i = 0; i < pSize; i++) { |
| float footnoteSize = 0; |
| String footnote = null; |
| if (mShowFootnote) { |
| footnote = Integer.toString(i + 1); |
| footnoteSize = mFootnotePaint.measureText(footnote); |
| assert (footnoteSize < candMargin); |
| } |
| String cand = mDecInfo.mCandidatesList.get(pStart + i); |
| float candidateWidth = mCandidatesPaint.measureText(cand); |
| float centerOffset = 0; |
| if (candidateWidth < MIN_ITEM_WIDTH) { |
| centerOffset = (MIN_ITEM_WIDTH - candidateWidth) / 2; |
| candidateWidth = MIN_ITEM_WIDTH; |
| } |
| |
| float itemTotalWidth = candidateWidth + 2 * candMargin; |
| |
| if (mActiveCandInPage == i && mEnableActiveHighlight) { |
| mActiveCellRect.set(xPos, mPaddingTop + 1, xPos |
| + itemTotalWidth, getHeight() - mPaddingBottom - 1); |
| mActiveCellDrawable.setBounds((int) mActiveCellRect.left, |
| (int) mActiveCellRect.top, (int) mActiveCellRect.right, |
| (int) mActiveCellRect.bottom); |
| mActiveCellDrawable.draw(canvas); |
| } |
| |
| if (mCandRects.size() < pSize) mCandRects.add(new RectF()); |
| mCandRects.elementAt(i).set(xPos - 1, yPos + mFmiCandidates.top, |
| xPos + itemTotalWidth + 1, yPos + mFmiCandidates.bottom); |
| |
| // Draw footnote |
| if (mShowFootnote) { |
| canvas.drawText(footnote, xPos + (candMargin - footnoteSize) |
| / 2, yPos, mFootnotePaint); |
| } |
| |
| // Left margin |
| xPos += candMargin; |
| if (candidateWidth > mContentWidth - xPos - centerOffset) { |
| cand = getLimitedCandidateForDrawing(cand, |
| mContentWidth - xPos - centerOffset); |
| } |
| if (mActiveCandInPage == i && mEnableActiveHighlight) { |
| mCandidatesPaint.setColor(mActiveCandidateColor); |
| } else { |
| mCandidatesPaint.setColor(mNormalCandidateColor); |
| } |
| canvas.drawText(cand, xPos + centerOffset, yPos, |
| mCandidatesPaint); |
| |
| // Candidate and right margin |
| xPos += candidateWidth + candMargin; |
| |
| // Draw the separator between candidates. |
| xPos += drawVerticalSeparator(canvas, xPos); |
| } |
| |
| // Update the arrow status of the container. |
| if (null != mArrowUpdater && mUpdateArrowStatusWhenDraw) { |
| mArrowUpdater.updateArrowStatus(); |
| mUpdateArrowStatusWhenDraw = false; |
| } |
| } |
| |
| private String getLimitedCandidateForDrawing(String rawCandidate, |
| float widthToDraw) { |
| int subLen = rawCandidate.length(); |
| if (subLen <= 1) return rawCandidate; |
| do { |
| subLen--; |
| float width = mCandidatesPaint.measureText(rawCandidate, 0, subLen); |
| if (width + mSuspensionPointsWidth <= widthToDraw || 1 >= subLen) { |
| return rawCandidate.substring(0, subLen) + |
| SUSPENSION_POINTS; |
| } |
| } while (true); |
| } |
| |
| private float drawVerticalSeparator(Canvas canvas, float xPos) { |
| mSeparatorDrawable.setBounds((int) xPos, mPaddingTop, (int) xPos |
| + mSeparatorDrawable.getIntrinsicWidth(), getMeasuredHeight() |
| - mPaddingBottom); |
| mSeparatorDrawable.draw(canvas); |
| return mSeparatorDrawable.getIntrinsicWidth(); |
| } |
| |
| private int mapToItemInPage(int x, int y) { |
| // mCandRects.size() == 0 happens when the page is set, but |
| // touch events occur before onDraw(). It usually happens with |
| // monkey test. |
| if (!mDecInfo.pageReady(mPageNo) || mPageNoCalculated != mPageNo |
| || mCandRects.size() == 0) { |
| return -1; |
| } |
| |
| int pageStart = mDecInfo.mPageStart.get(mPageNo); |
| int pageSize = mDecInfo.mPageStart.get(mPageNo + 1) - pageStart; |
| if (mCandRects.size() < pageSize) { |
| return -1; |
| } |
| |
| // If not found, try to find the nearest one. |
| float nearestDis = Float.MAX_VALUE; |
| int nearest = -1; |
| for (int i = 0; i < pageSize; i++) { |
| RectF r = mCandRects.elementAt(i); |
| if (r.left < x && r.right > x && r.top < y && r.bottom > y) { |
| return i; |
| } |
| float disx = (r.left + r.right) / 2 - x; |
| float disy = (r.top + r.bottom) / 2 - y; |
| float dis = disx * disx + disy * disy; |
| if (dis < nearestDis) { |
| nearestDis = dis; |
| nearest = i; |
| } |
| } |
| |
| return nearest; |
| } |
| |
| // Because the candidate view under the current focused one may also get |
| // touching events. Here we just bypass the event to the container and let |
| // it decide which view should handle the event. |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| return super.onTouchEvent(event); |
| } |
| |
| public boolean onTouchEventReal(MotionEvent event) { |
| // The page in the background can also be touched. |
| if (null == mDecInfo || !mDecInfo.pageReady(mPageNo) |
| || mPageNoCalculated != mPageNo) return true; |
| |
| int x, y; |
| x = (int) event.getX(); |
| y = (int) event.getY(); |
| |
| if (mGestureDetector.onTouchEvent(event)) { |
| mTimer.removeTimer(); |
| mBalloonHint.delayedDismiss(0); |
| return true; |
| } |
| |
| int clickedItemInPage = -1; |
| |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_UP: |
| clickedItemInPage = mapToItemInPage(x, y); |
| if (clickedItemInPage >= 0) { |
| invalidate(); |
| mCvListener.onClickChoice(clickedItemInPage |
| + mDecInfo.mPageStart.get(mPageNo)); |
| } |
| mBalloonHint.delayedDismiss(BalloonHint.TIME_DELAY_DISMISS); |
| break; |
| |
| case MotionEvent.ACTION_DOWN: |
| clickedItemInPage = mapToItemInPage(x, y); |
| if (clickedItemInPage >= 0) { |
| showBalloon(clickedItemInPage, true); |
| mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, |
| clickedItemInPage); |
| } |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| clickedItemInPage = mapToItemInPage(x, y); |
| if (clickedItemInPage >= 0 |
| && (clickedItemInPage != mTimer.getActiveCandOfPageToShow() || mPageNo != mTimer |
| .getPageToShow())) { |
| showBalloon(clickedItemInPage, true); |
| mTimer.startTimer(BalloonHint.TIME_DELAY_SHOW, mPageNo, |
| clickedItemInPage); |
| } |
| } |
| return true; |
| } |
| |
| private void showBalloon(int candPos, boolean delayedShow) { |
| mBalloonHint.removeTimer(); |
| |
| RectF r = mCandRects.elementAt(candPos); |
| int desired_width = (int) (r.right - r.left); |
| int desired_height = (int) (r.bottom - r.top); |
| mBalloonHint.setBalloonConfig(mDecInfo.mCandidatesList |
| .get(mDecInfo.mPageStart.get(mPageNo) + candPos), 44, true, |
| mImeCandidateColor, desired_width, desired_height); |
| |
| getLocationOnScreen(mLocationTmp); |
| mHintPositionToInputView[0] = mLocationTmp[0] |
| + (int) (r.left - (mBalloonHint.getWidth() - desired_width) / 2); |
| mHintPositionToInputView[1] = -mBalloonHint.getHeight(); |
| |
| long delay = BalloonHint.TIME_DELAY_SHOW; |
| if (!delayedShow) delay = 0; |
| mBalloonHint.dismiss(); |
| if (!mBalloonHint.isShowing()) { |
| mBalloonHint.delayedShow(delay, mHintPositionToInputView); |
| } else { |
| mBalloonHint.delayedUpdate(0, mHintPositionToInputView, -1, -1); |
| } |
| } |
| |
| private class PressTimer extends Handler implements Runnable { |
| private boolean mTimerPending = false; |
| private int mPageNoToShow; |
| private int mActiveCandOfPage; |
| |
| public PressTimer() { |
| super(); |
| } |
| |
| public void startTimer(long afterMillis, int pageNo, int activeInPage) { |
| mTimer.removeTimer(); |
| postDelayed(this, afterMillis); |
| mTimerPending = true; |
| mPageNoToShow = pageNo; |
| mActiveCandOfPage = activeInPage; |
| } |
| |
| public int getPageToShow() { |
| return mPageNoToShow; |
| } |
| |
| public int getActiveCandOfPageToShow() { |
| return mActiveCandOfPage; |
| } |
| |
| public boolean removeTimer() { |
| if (mTimerPending) { |
| mTimerPending = false; |
| removeCallbacks(this); |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean isPending() { |
| return mTimerPending; |
| } |
| |
| public void run() { |
| if (mPageNoToShow >= 0 && mActiveCandOfPage >= 0) { |
| // Always enable to highlight the clicked one. |
| showPage(mPageNoToShow, mActiveCandOfPage, true); |
| invalidate(); |
| } |
| mTimerPending = false; |
| } |
| } |
| } |