/* * Copyright (C) 2008 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.latin; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.Paint.Align; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.widget.PopupWindow; import android.widget.TextView; public class CandidateView extends View { private static final int OUT_OF_BOUNDS = -1; private static final List EMPTY_LIST = new ArrayList(); private LatinIME mService; private List mSuggestions = EMPTY_LIST; private boolean mShowingCompletions; private CharSequence mSelectedString; private int mSelectedIndex; private int mTouchX = OUT_OF_BOUNDS; private Drawable mSelectionHighlight; private boolean mTypedWordValid; private boolean mHaveMinimalSuggestion; private Rect mBgPadding; private TextView mPreviewText; private PopupWindow mPreviewPopup; private int mCurrentWordIndex; private Drawable mDivider; private static final int MAX_SUGGESTIONS = 32; private static final int SCROLL_PIXELS = 20; private static final int MSG_REMOVE_PREVIEW = 1; private static final int MSG_REMOVE_THROUGH_PREVIEW = 2; private int[] mWordWidth = new int[MAX_SUGGESTIONS]; private int[] mWordX = new int[MAX_SUGGESTIONS]; private int mPopupPreviewX; private int mPopupPreviewY; private static final int X_GAP = 10; private int mColorNormal; private int mColorRecommended; private int mColorOther; private Paint mPaint; private int mDescent; private boolean mScrolled; private boolean mShowingAddToDictionary; private CharSequence mAddToDictionaryHint; private int mTargetScrollX; private int mMinTouchableWidth; private int mTotalWidth; private GestureDetector mGestureDetector; Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_REMOVE_PREVIEW: mPreviewText.setVisibility(GONE); break; case MSG_REMOVE_THROUGH_PREVIEW: mPreviewText.setVisibility(GONE); if (mTouchX != OUT_OF_BOUNDS) { removeHighlight(); } break; } } }; /** * Construct a CandidateView for showing suggested words for completion. * @param context * @param attrs */ public CandidateView(Context context, AttributeSet attrs) { super(context, attrs); mSelectionHighlight = context.getResources().getDrawable( R.drawable.list_selector_background_pressed); LayoutInflater inflate = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); Resources res = context.getResources(); mPreviewPopup = new PopupWindow(context); mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null); mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); mColorNormal = res.getColor(R.color.candidate_normal); mColorRecommended = res.getColor(R.color.candidate_recommended); mColorOther = res.getColor(R.color.candidate_other); mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider); mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary); mPaint = new Paint(); mPaint.setColor(mColorNormal); mPaint.setAntiAlias(true); mPaint.setTextSize(mPreviewText.getTextSize()); mPaint.setStrokeWidth(0); mPaint.setTextAlign(Align.CENTER); mDescent = (int) mPaint.descent(); // 50 pixels for a 160dpi device would mean about 0.3 inch mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50); // Slightly reluctant to scroll to be able to easily choose the suggestion // 50 pixels for a 160dpi device would mean about 0.3 inch final int touchSlop = (int) (getResources().getDisplayMetrics().density * 50); final int touchSlopSquare = touchSlop * touchSlop; mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent me) { if (mSuggestions.size() > 0) { if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) { longPressFirstWord(); } } } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { final int deltaX = (int) (e2.getX() - e1.getX()); final int deltaY = (int) (e2.getY() - e1.getY()); final int distance = (deltaX * deltaX) + (deltaY * deltaY); if (distance < touchSlopSquare) { return false; } final int width = getWidth(); mScrolled = true; int scrollX = getScrollX(); scrollX += (int) distanceX; if (scrollX < 0) { scrollX = 0; } if (distanceX > 0 && scrollX + width > mTotalWidth) { scrollX -= (int) distanceX; } mTargetScrollX = scrollX; scrollTo(scrollX, getScrollY()); hidePreview(); invalidate(); return true; } }); setWillNotDraw(false); setHorizontalScrollBarEnabled(false); setVerticalScrollBarEnabled(false); scrollTo(0, getScrollY()); } /** * A connection back to the service to communicate with the text field * @param listener */ public void setService(LatinIME listener) { mService = listener; } @Override public int computeHorizontalScrollRange() { return mTotalWidth; } /** * If the canvas is null, then only touch calculations are performed to pick the target * candidate. */ @Override protected void onDraw(Canvas canvas) { if (canvas != null) { super.onDraw(canvas); } mTotalWidth = 0; if (mSuggestions == null) return; final int height = getHeight(); if (mBgPadding == null) { mBgPadding = new Rect(0, 0, 0, 0); if (getBackground() != null) { getBackground().getPadding(mBgPadding); } mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight()); } int x = 0; final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); final Rect bgPadding = mBgPadding; final Paint paint = mPaint; final int touchX = mTouchX; final int scrollX = getScrollX(); final boolean scrolled = mScrolled; final boolean typedWordValid = mTypedWordValid; final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2; boolean existsAutoCompletion = false; for (int i = 0; i < count; i++) { CharSequence suggestion = mSuggestions.get(i); if (suggestion == null) continue; paint.setColor(mColorNormal); if (mHaveMinimalSuggestion && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) { paint.setTypeface(Typeface.DEFAULT_BOLD); paint.setColor(mColorRecommended); existsAutoCompletion = true; } else if (i != 0) { paint.setColor(mColorOther); } final int wordWidth; if (mWordWidth[i] != 0) { wordWidth = mWordWidth[i]; } else { float textWidth = paint.measureText(suggestion, 0, suggestion.length()); wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2); mWordWidth[i] = wordWidth; } mWordX[i] = x; if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled && touchX != OUT_OF_BOUNDS) { if (canvas != null && !mShowingAddToDictionary) { canvas.translate(x, 0); mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height); mSelectionHighlight.draw(canvas); canvas.translate(-x, 0); showPreview(i, null); } mSelectedString = suggestion; mSelectedIndex = i; } if (canvas != null) { canvas.drawText(suggestion, 0, suggestion.length(), x + wordWidth / 2, y, paint); paint.setColor(mColorOther); canvas.translate(x + wordWidth, 0); // Draw a divider unless it's after the hint if (!(mShowingAddToDictionary && i == 1)) { mDivider.draw(canvas); } canvas.translate(-x - wordWidth, 0); } paint.setTypeface(Typeface.DEFAULT); x += wordWidth; } mService.onAutoCompletionStateChanged(existsAutoCompletion); mTotalWidth = x; if (mTargetScrollX != scrollX) { scrollToTarget(); } } private void scrollToTarget() { int scrollX = getScrollX(); if (mTargetScrollX > scrollX) { scrollX += SCROLL_PIXELS; if (scrollX >= mTargetScrollX) { scrollX = mTargetScrollX; scrollTo(scrollX, getScrollY()); requestLayout(); } else { scrollTo(scrollX, getScrollY()); } } else { scrollX -= SCROLL_PIXELS; if (scrollX <= mTargetScrollX) { scrollX = mTargetScrollX; scrollTo(scrollX, getScrollY()); requestLayout(); } else { scrollTo(scrollX, getScrollY()); } } invalidate(); } public void setSuggestions(List suggestions, boolean completions, boolean typedWordValid, boolean haveMinimalSuggestion) { clear(); if (suggestions != null) { mSuggestions = new ArrayList(suggestions); } mShowingCompletions = completions; mTypedWordValid = typedWordValid; scrollTo(0, getScrollY()); mTargetScrollX = 0; mHaveMinimalSuggestion = haveMinimalSuggestion; // Compute the total width onDraw(null); invalidate(); requestLayout(); } public boolean isShowingAddToDictionaryHint() { return mShowingAddToDictionary; } public void showAddToDictionaryHint(CharSequence word) { ArrayList suggestions = new ArrayList(); suggestions.add(word); suggestions.add(mAddToDictionaryHint); setSuggestions(suggestions, false, false, false); mShowingAddToDictionary = true; } public boolean dismissAddToDictionaryHint() { if (!mShowingAddToDictionary) return false; clear(); return true; } public void scrollPrev() { int i = 0; final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int firstItem = 0; // Actually just before the first item, if at the boundary while (i < count) { if (mWordX[i] < getScrollX() && mWordX[i] + mWordWidth[i] >= getScrollX() - 1) { firstItem = i; break; } i++; } int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth(); if (leftEdge < 0) leftEdge = 0; updateScrollPosition(leftEdge); } public void scrollNext() { int i = 0; int scrollX = getScrollX(); int targetX = scrollX; final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int rightEdge = scrollX + getWidth(); while (i < count) { if (mWordX[i] <= rightEdge && mWordX[i] + mWordWidth[i] >= rightEdge) { targetX = Math.min(mWordX[i], mTotalWidth - getWidth()); break; } i++; } updateScrollPosition(targetX); } private void updateScrollPosition(int targetX) { if (targetX != getScrollX()) { // TODO: Animate mTargetScrollX = targetX; requestLayout(); invalidate(); mScrolled = true; } } /* package */ List getSuggestions() { return mSuggestions; } public void clear() { // Don't call mSuggestions.clear() because it's being used for logging // in LatinIME.pickSuggestionManually(). mSuggestions = EMPTY_LIST; mTouchX = OUT_OF_BOUNDS; mSelectedString = null; mSelectedIndex = -1; mShowingAddToDictionary = false; invalidate(); Arrays.fill(mWordWidth, 0); Arrays.fill(mWordX, 0); if (mPreviewPopup.isShowing()) { mPreviewPopup.dismiss(); } } @Override public boolean onTouchEvent(MotionEvent me) { if (mGestureDetector.onTouchEvent(me)) { return true; } int action = me.getAction(); int x = (int) me.getX(); int y = (int) me.getY(); mTouchX = x; switch (action) { case MotionEvent.ACTION_DOWN: mScrolled = false; invalidate(); break; case MotionEvent.ACTION_MOVE: if (y <= 0) { // Fling up!? if (mSelectedString != null) { // If there are completions from the application, we don't change the state to // STATE_PICKED_SUGGESTION if (!mShowingCompletions) { // This "acceptedSuggestion" will not be counted as a word because // it will be counted in pickSuggestion instead. TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); } mService.pickSuggestionManually(mSelectedIndex, mSelectedString); mSelectedString = null; mSelectedIndex = -1; } } invalidate(); break; case MotionEvent.ACTION_UP: if (!mScrolled) { if (mSelectedString != null) { if (mShowingAddToDictionary) { longPressFirstWord(); clear(); } else { if (!mShowingCompletions) { TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); } mService.pickSuggestionManually(mSelectedIndex, mSelectedString); } } } mSelectedString = null; mSelectedIndex = -1; removeHighlight(); hidePreview(); requestLayout(); break; } return true; } private void hidePreview() { mCurrentWordIndex = OUT_OF_BOUNDS; if (mPreviewPopup.isShowing()) { mHandler.sendMessageDelayed(mHandler .obtainMessage(MSG_REMOVE_PREVIEW), 60); } } private void showPreview(int wordIndex, String altText) { int oldWordIndex = mCurrentWordIndex; mCurrentWordIndex = wordIndex; // If index changed or changing text if (oldWordIndex != mCurrentWordIndex || altText != null) { if (wordIndex == OUT_OF_BOUNDS) { hidePreview(); } else { CharSequence word = altText != null? altText : mSuggestions.get(wordIndex); mPreviewText.setText(word); mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2); final int popupWidth = wordWidth + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight(); final int popupHeight = mPreviewText.getMeasuredHeight(); //mPreviewText.setVisibility(INVISIBLE); mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX() + (mWordWidth[wordIndex] - wordWidth) / 2; mPopupPreviewY = - popupHeight; mHandler.removeMessages(MSG_REMOVE_PREVIEW); int [] offsetInWindow = new int[2]; getLocationInWindow(offsetInWindow); if (mPreviewPopup.isShowing()) { mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1], popupWidth, popupHeight); } else { mPreviewPopup.setWidth(popupWidth); mPreviewPopup.setHeight(popupHeight); mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX, mPopupPreviewY + offsetInWindow[1]); } mPreviewText.setVisibility(VISIBLE); } } } private void removeHighlight() { mTouchX = OUT_OF_BOUNDS; invalidate(); } private void longPressFirstWord() { CharSequence word = mSuggestions.get(0); if (word.length() < 2) return; if (mService.addWordToDictionary(word.toString())) { showPreview(0, getContext().getResources().getString(R.string.added_word, word)); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); hidePreview(); } }