From e9a0e66716dab4dd3184d009d8920de1961efdfa Mon Sep 17 00:00:00 2001 From: Amin Bandali Date: Mon, 16 Dec 2024 21:45:41 -0500 Subject: Rename to Kelar Keyboard (org.kelar.inputmethod.latin) --- .../suggestions/SuggestionStripLayoutHelper.java | 650 +++++++++++++++++++++ 1 file changed, 650 insertions(+) create mode 100644 java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java (limited to 'java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java') diff --git a/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java new file mode 100644 index 000000000..6e95de414 --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -0,0 +1,650 @@ +/* + * Copyright (C) 2013 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 org.kelar.inputmethod.latin.suggestions; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.kelar.inputmethod.accessibility.AccessibilityUtils; +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.PunctuationSuggestions; +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.settings.Settings; +import org.kelar.inputmethod.latin.settings.SettingsValues; +import org.kelar.inputmethod.latin.utils.ResourceUtils; +import org.kelar.inputmethod.latin.utils.ViewLayoutUtils; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class SuggestionStripLayoutHelper { + private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; + private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; + private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; + private static final int PUNCTUATIONS_IN_STRIP = 5; + private static final float MIN_TEXT_XSCALE = 0.70f; + + public final int mPadding; + public final int mDividerWidth; + public final int mSuggestionsStripHeight; + private final int mSuggestionsCountInStrip; + public final int mMoreSuggestionsRowHeight; + private int mMaxMoreSuggestionsRow; + public final float mMinMoreSuggestionsWidth; + public final int mMoreSuggestionsBottomGap; + private boolean mMoreSuggestionsAvailable; + + // The index of these {@link ArrayList} is the position in the suggestion strip. The indices + // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. + // The position of the most important suggestion is in {@link #mCenterPositionInStrip} + private final ArrayList mWordViews; + private final ArrayList mDividerViews; + private final ArrayList mDebugInfoViews; + + private final int mColorValidTypedWord; + private final int mColorTypedWord; + private final int mColorAutoCorrect; + private final int mColorSuggested; + private final float mAlphaObsoleted; + private final float mCenterSuggestionWeight; + private final int mCenterPositionInStrip; + private final int mTypedWordPositionWhenAutocorrect; + private final Drawable mMoreSuggestionsHint; + private static final String MORE_SUGGESTIONS_HINT = "\u2026"; + + private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); + private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); + + private final int mSuggestionStripOptions; + // These constants are the flag values of + // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. + private static final int AUTO_CORRECT_BOLD = 0x01; + private static final int AUTO_CORRECT_UNDERLINE = 0x02; + private static final int VALID_TYPED_WORD_BOLD = 0x04; + + public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, + final int defStyle, final ArrayList wordViews, + final ArrayList dividerViews, final ArrayList debugInfoViews) { + mWordViews = wordViews; + mDividerViews = dividerViews; + mDebugInfoViews = debugInfoViews; + + final TextView wordView = wordViews.get(0); + final View dividerView = dividerViews.get(0); + mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight(); + dividerView.measure( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mDividerWidth = dividerView.getMeasuredWidth(); + + final Resources res = wordView.getResources(); + mSuggestionsStripHeight = res.getDimensionPixelSize( + R.dimen.config_suggestions_strip_height); + + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); + mSuggestionStripOptions = a.getInt( + R.styleable.SuggestionStripView_suggestionStripOptions, 0); + mAlphaObsoleted = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); + mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); + mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0); + mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0); + mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0); + mSuggestionsCountInStrip = a.getInt( + R.styleable.SuggestionStripView_suggestionsCountInStrip, + DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); + mCenterSuggestionWeight = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_centerSuggestionPercentile, + DEFAULT_CENTER_SUGGESTION_PERCENTILE); + mMaxMoreSuggestionsRow = a.getInt( + R.styleable.SuggestionStripView_maxMoreSuggestionsRow, + DEFAULT_MAX_MORE_SUGGESTIONS_ROW); + mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, + R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); + a.recycle(); + + mMoreSuggestionsHint = getMoreSuggestionsHint(res, + res.getDimension(R.dimen.config_more_suggestions_hint_text_size), + mColorAutoCorrect); + mCenterPositionInStrip = mSuggestionsCountInStrip / 2; + // Assuming there are at least three suggestions. Also, note that the suggestions are + // laid out according to script direction, so this is left of the center for LTR scripts + // and right of the center for RTL scripts. + mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; + mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( + R.dimen.config_more_suggestions_bottom_gap); + mMoreSuggestionsRowHeight = res.getDimensionPixelSize( + R.dimen.config_more_suggestions_row_height); + } + + public int getMaxMoreSuggestionsRow() { + return mMaxMoreSuggestionsRow; + } + + private int getMoreSuggestionsHeight() { + return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; + } + + public void setMoreSuggestionsHeight(final int remainingHeight) { + final int currentHeight = getMoreSuggestionsHeight(); + if (currentHeight <= remainingHeight) { + return; + } + + mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) + / mMoreSuggestionsRowHeight; + } + + private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize, + final int color) { + final Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextAlign(Align.CENTER); + paint.setTextSize(textSize); + paint.setColor(color); + final Rect bounds = new Rect(); + paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds); + final int width = Math.round(bounds.width() + 0.5f); + final int height = Math.round(bounds.height() + 0.5f); + final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(buffer); + canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint); + BitmapDrawable bitmapDrawable = new BitmapDrawable(res, buffer); + bitmapDrawable.setTargetDensity(canvas); + return bitmapDrawable; + } + + private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { + if (indexInSuggestedWords >= suggestedWords.size()) { + return null; + } + final String word = suggestedWords.getLabel(indexInSuggestedWords); + // TODO: don't use the index to decide whether this is the auto-correction/typed word, as + // this is brittle + final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect + && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + final boolean isTypedWordValid = suggestedWords.mTypedWordValid + && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; + if (!isAutoCorrection && !isTypedWordValid) { + return word; + } + + final Spannable spannedWord = new SpannableString(word); + final int options = mSuggestionStripOptions; + if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) + || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { + addStyleSpan(spannedWord, BOLD_SPAN); + } + if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { + addStyleSpan(spannedWord, UNDERLINE_SPAN); + } + return spannedWord; + } + + /** + * Convert an index of {@link SuggestedWords} to position in the suggestion strip. + * @param indexInSuggestedWords the index of {@link SuggestedWords}. + * @param suggestedWords the suggested words list + * @return Non-negative integer of the position in the suggestion strip. + * Negative integer if the word of the index shouldn't be shown on the suggestion strip. + */ + private int getPositionInSuggestionStrip(final int indexInSuggestedWords, + final SuggestedWords suggestedWords) { + final SettingsValues settingsValues = Settings.getInstance().getCurrent(); + final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle, + settingsValues.mGestureFloatingPreviewTextEnabled, + settingsValues.mShouldShowLxxSuggestionUi); + return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect, + settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord, + mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect); + } + + @UsedForTesting + static boolean shouldOmitTypedWord(final int inputStyle, + final boolean gestureFloatingPreviewTextEnabled, + final boolean shouldShowUiToAcceptTypedWord) { + final boolean omitTypedWord = (inputStyle == SuggestedWords.INPUT_STYLE_TYPING) + || (inputStyle == SuggestedWords.INPUT_STYLE_TAIL_BATCH) + || (inputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH + && gestureFloatingPreviewTextEnabled); + return shouldShowUiToAcceptTypedWord && omitTypedWord; + } + + @UsedForTesting + static int getPositionInSuggestionStrip(final int indexInSuggestedWords, + final boolean willAutoCorrect, final boolean omitTypedWord, + final int centerPositionInStrip, final int typedWordPositionWhenAutoCorrect) { + if (omitTypedWord) { + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD) { + // Ignore. + return -1; + } + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION) { + // Center in the suggestion strip. + return centerPositionInStrip; + } + // If neither of those, the order in the suggestion strip is left of the center first + // then right of the center, to both edges of the suggestion strip. + // For example, center-1, center+1, center-2, center+2, and so on. + final int n = indexInSuggestedWords; + final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); + final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; + return positionInSuggestionStrip; + } + final int indexToDisplayMostImportantSuggestion; + final int indexToDisplaySecondMostImportantSuggestion; + if (willAutoCorrect) { + indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; + indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; + } else { + indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; + indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; + } + if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) { + // Center in the suggestion strip. + return centerPositionInStrip; + } + if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) { + // Center-1. + return typedWordPositionWhenAutoCorrect; + } + // If neither of those, the order in the suggestion strip is right of the center first + // then left of the center, to both edges of the suggestion strip. + // For example, Center+1, center-2, center+2, center-3, and so on. + final int n = indexInSuggestedWords + 1; + final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); + final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; + return positionInSuggestionStrip; + } + + private int getSuggestionTextColor(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { + // Use identity for strings, not #equals : it's the typed word if it's the same object + final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf( + SuggestedWordInfo.KIND_TYPED); + + final int color; + if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION + && suggestedWords.mWillAutoCorrect) { + color = mColorAutoCorrect; + } else if (isTypedWord && suggestedWords.mTypedWordValid) { + color = mColorValidTypedWord; + } else if (isTypedWord) { + color = mColorTypedWord; + } else { + color = mColorSuggested; + } + if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { + return applyAlpha(color, mAlphaObsoleted); + } + return color; + } + + private static int applyAlpha(final int color, final float alpha) { + final int newAlpha = (int)(Color.alpha(color) * alpha); + return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + private static void addDivider(final ViewGroup stripView, final View dividerView) { + stripView.addView(dividerView); + final LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams)dividerView.getLayoutParams(); + params.gravity = Gravity.CENTER; + } + + /** + * Layout suggestions to the suggestions strip. And returns the start index of more + * suggestions. + * + * @param suggestedWords suggestions to be shown in the suggestions strip. + * @param stripView the suggestions strip view. + * @param placerView the view where the debug info will be placed. + * @return the start index of more suggestions. + */ + public int layoutAndReturnStartIndexOfMoreSuggestions( + final Context context, + final SuggestedWords suggestedWords, + final ViewGroup stripView, + final ViewGroup placerView) { + if (suggestedWords.isPunctuationSuggestions()) { + return layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( + (PunctuationSuggestions)suggestedWords, stripView); + } + + final int wordCountToShow = suggestedWords.getWordCountToShow( + Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi); + final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions( + suggestedWords, mSuggestionsCountInStrip); + final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); + final int stripWidth = stripView.getWidth(); + final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); + if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth, + centerWordView.getPaint()) < MIN_TEXT_XSCALE) { + // Layout only the most relevant suggested word at the center of the suggestion strip + // by consolidating all slots in the strip. + final int countInStrip = 1; + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); + layoutWord(context, mCenterPositionInStrip, stripWidth - mPadding); + stripView.addView(centerWordView); + setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); + if (SuggestionStripView.DBG) { + layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth); + } + final Integer lastIndex = (Integer)centerWordView.getTag(); + return (lastIndex == null ? 0 : lastIndex) + 1; + } + + final int countInStrip = mSuggestionsCountInStrip; + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); + @SuppressWarnings("unused") + int x = 0; + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + final View divider = mDividerViews.get(positionInStrip); + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, divider); + x += divider.getMeasuredWidth(); + } + + final int width = getSuggestionWidth(positionInStrip, stripWidth); + final TextView wordView = layoutWord(context, positionInStrip, width); + stripView.addView(wordView); + setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), + ViewGroup.LayoutParams.MATCH_PARENT); + x += wordView.getMeasuredWidth(); + + if (SuggestionStripView.DBG) { + layoutDebugInfo(positionInStrip, placerView, x); + } + } + return startIndexOfMoreSuggestions; + } + + /** + * Format appropriately the suggested word in {@link #mWordViews} specified by + * positionInStrip. When the suggested word doesn't exist, the corresponding + * {@link TextView} will be disabled and never respond to user interaction. The suggested word + * may be shrunk or ellipsized to fit in the specified width. + * + * The positionInStrip argument is the index in the suggestion strip. The indices + * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. + * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This + * usually doesn't match the index in suggedtedWords -- see + * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}. + * + * @param positionInStrip the position in the suggestion strip. + * @param width the maximum width for layout in pixels. + * @return the {@link TextView} containing the suggested word appropriately formatted. + */ + private TextView layoutWord(final Context context, final int positionInStrip, final int width) { + final TextView wordView = mWordViews.get(positionInStrip); + final CharSequence word = wordView.getText(); + if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { + // TODO: This "more suggestions hint" should have a nicely designed icon. + wordView.setCompoundDrawablesWithIntrinsicBounds( + null, null, null, mMoreSuggestionsHint); + // HACK: Align with other TextViews that have no compound drawables. + wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); + } else { + wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } + // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. + // Use a simple {@link String} to avoid the issue. + wordView.setContentDescription( + TextUtils.isEmpty(word) + ? context.getResources().getString(R.string.spoken_empty_suggestion) + : word.toString()); + final CharSequence text = getEllipsizedTextWithSettingScaleX( + word, width, wordView.getPaint()); + final float scaleX = wordView.getTextScaleX(); + wordView.setText(text); // TextView.setText() resets text scale x to 1.0. + wordView.setTextScaleX(scaleX); + // A wordView should be disabled when word is empty in order to + // make it unclickable. + // With accessibility touch exploration on, wordView should be enabled even + // when it is empty to avoid announcing as "disabled". + wordView.setEnabled(!TextUtils.isEmpty(word) + || AccessibilityUtils.getInstance().isTouchExplorationEnabled()); + return wordView; + } + + private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, + final int x) { + final TextView debugInfoView = mDebugInfoViews.get(positionInStrip); + final CharSequence debugInfo = debugInfoView.getText(); + if (debugInfo == null) { + return; + } + placerView.addView(debugInfoView); + debugInfoView.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + final int infoWidth = debugInfoView.getMeasuredWidth(); + final int y = debugInfoView.getMeasuredHeight(); + ViewLayoutUtils.placeViewAt( + debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight()); + } + + private int getSuggestionWidth(final int positionInStrip, final int maxWidth) { + final int paddings = mPadding * mSuggestionsCountInStrip; + final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); + final int availableWidth = maxWidth - paddings - dividers; + return (int)(availableWidth * getSuggestionWeight(positionInStrip)); + } + + private float getSuggestionWeight(final int positionInStrip) { + if (positionInStrip == mCenterPositionInStrip) { + return mCenterSuggestionWeight; + } + // TODO: Revisit this for cases of 5 or more suggestions + return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); + } + + private int setupWordViewsAndReturnStartIndexOfMoreSuggestions( + final SuggestedWords suggestedWords, final int maxSuggestionInStrip) { + // Clear all suggestions first + for (int positionInStrip = 0; positionInStrip < maxSuggestionInStrip; ++positionInStrip) { + final TextView wordView = mWordViews.get(positionInStrip); + wordView.setText(null); + wordView.setTag(null); + // Make this inactive for touches in {@link #layoutWord(int,int)}. + if (SuggestionStripView.DBG) { + mDebugInfoViews.get(positionInStrip).setText(null); + } + } + int count = 0; + int indexInSuggestedWords; + for (indexInSuggestedWords = 0; indexInSuggestedWords < suggestedWords.size() + && count < maxSuggestionInStrip; indexInSuggestedWords++) { + final int positionInStrip = + getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); + if (positionInStrip < 0) { + continue; + } + final TextView wordView = mWordViews.get(positionInStrip); + // {@link TextView#getTag()} is used to get the index in suggestedWords at + // {@link SuggestionStripView#onClick(View)}. + wordView.setTag(indexInSuggestedWords); + wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); + wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); + if (SuggestionStripView.DBG) { + mDebugInfoViews.get(positionInStrip).setText( + suggestedWords.getDebugString(indexInSuggestedWords)); + } + count++; + } + return indexInSuggestedWords; + } + + private int layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( + final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { + final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, mDividerViews.get(positionInStrip)); + } + + final TextView wordView = mWordViews.get(positionInStrip); + final String punctuation = punctuationSuggestions.getLabel(positionInStrip); + // {@link TextView#getTag()} is used to get the index in suggestedWords at + // {@link SuggestionStripView#onClick(View)}. + wordView.setTag(positionInStrip); + wordView.setText(punctuation); + wordView.setContentDescription(punctuation); + wordView.setTextScaleX(1.0f); + wordView.setCompoundDrawables(null, null, null, null); + wordView.setTextColor(mColorAutoCorrect); + stripView.addView(wordView); + setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); + } + mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); + return countInStrip; + } + + public void layoutImportantNotice(final View importantNoticeStrip, + final String importantNoticeTitle) { + final TextView titleView = (TextView)importantNoticeStrip.findViewById( + R.id.important_notice_title); + final int width = titleView.getWidth() - titleView.getPaddingLeft() + - titleView.getPaddingRight(); + titleView.setTextColor(mColorAutoCorrect); + titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0. + final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); + titleView.setTextScaleX(titleScaleX); + } + + static void setLayoutWeight(final View v, final float weight, final int height) { + final ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; + llp.weight = weight; + llp.width = 0; + llp.height = height; + } + } + + private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth, + final TextPaint paint) { + paint.setTextScaleX(1.0f); + final int width = getTextWidth(text, paint); + if (width <= maxWidth || maxWidth <= 0) { + return 1.0f; + } + return maxWidth / (float) width; + } + + @Nullable + private static CharSequence getEllipsizedTextWithSettingScaleX( + @Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) { + if (text == null) { + return null; + } + final float scaleX = getTextScaleX(text, maxWidth, paint); + if (scaleX >= MIN_TEXT_XSCALE) { + paint.setTextScaleX(scaleX); + return text; + } + + // text must be ellipsized with minimum text scale x. + paint.setTextScaleX(MIN_TEXT_XSCALE); + final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN); + final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN); + // TextUtils.ellipsize erases any span object existed after ellipsized point. + // We have to restore these spans afterward. + final CharSequence ellipsizedText = TextUtils.ellipsize( + text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE); + if (!hasBoldStyle && !hasUnderlineStyle) { + return ellipsizedText; + } + final Spannable spannableText = (ellipsizedText instanceof Spannable) + ? (Spannable)ellipsizedText : new SpannableString(ellipsizedText); + if (hasBoldStyle) { + addStyleSpan(spannableText, BOLD_SPAN); + } + if (hasUnderlineStyle) { + addStyleSpan(spannableText, UNDERLINE_SPAN); + } + return spannableText; + } + + private static boolean hasStyleSpan(@Nullable final CharSequence text, + final CharacterStyle style) { + if (text instanceof Spanned) { + return ((Spanned)text).getSpanStart(style) >= 0; + } + return false; + } + + private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) { + text.removeSpan(style); + text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) { + if (TextUtils.isEmpty(text)) { + return 0; + } + final int length = text.length(); + final float[] widths = new float[length]; + final int count; + final Typeface savedTypeface = paint.getTypeface(); + try { + paint.setTypeface(getTextTypeface(text)); + count = paint.getTextWidths(text, 0, length, widths); + } finally { + paint.setTypeface(savedTypeface); + } + int width = 0; + for (int i = 0; i < count; i++) { + width += Math.round(widths[i] + 0.5f); + } + return width; + } + + private static Typeface getTextTypeface(@Nullable final CharSequence text) { + return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; + } +} -- cgit v1.2.3-83-g751a