/* * 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 com.android.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 com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.PunctuationSuggestions; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.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; } }