diff options
Diffstat (limited to 'src/com/android/inputmethod/pinyin/CandidateView.java')
-rw-r--r-- | src/com/android/inputmethod/pinyin/CandidateView.java | 760 |
1 files changed, 760 insertions, 0 deletions
diff --git a/src/com/android/inputmethod/pinyin/CandidateView.java b/src/com/android/inputmethod/pinyin/CandidateView.java new file mode 100644 index 0000000..8dc1bf1 --- /dev/null +++ b/src/com/android/inputmethod/pinyin/CandidateView.java @@ -0,0 +1,760 @@ +/* + * 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; + } + } +} |