SuggestionStripLayoutHelper.java revision 72ac390ce64fe2825ac59029402e5f372303c8c3
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin.suggestions;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Paint;
26import android.graphics.Paint.Align;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.graphics.drawable.BitmapDrawable;
30import android.graphics.drawable.Drawable;
31import android.text.Spannable;
32import android.text.SpannableString;
33import android.text.Spanned;
34import android.text.TextPaint;
35import android.text.TextUtils;
36import android.text.style.CharacterStyle;
37import android.text.style.StyleSpan;
38import android.text.style.UnderlineSpan;
39import android.util.AttributeSet;
40import android.view.Gravity;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.View.OnClickListener;
44import android.view.ViewGroup;
45import android.widget.LinearLayout;
46import android.widget.TextView;
47
48import com.android.inputmethod.keyboard.ViewLayoutUtils;
49import com.android.inputmethod.latin.AutoCorrection;
50import com.android.inputmethod.latin.LatinImeLogger;
51import com.android.inputmethod.latin.R;
52import com.android.inputmethod.latin.ResourceUtils;
53import com.android.inputmethod.latin.SuggestedWords;
54import com.android.inputmethod.latin.Utils;
55
56import java.util.ArrayList;
57
58final class SuggestionStripLayoutHelper {
59    private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
60    private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
61    private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
62    private static final int PUNCTUATIONS_IN_STRIP = 5;
63    private static final float MIN_TEXT_XSCALE = 0.70f;
64
65    public final int mPadding;
66    public final int mDividerWidth;
67    public final int mSuggestionsStripHeight;
68    public final int mSuggestionsCountInStrip;
69    public final int mMoreSuggestionsRowHeight;
70    private int mMaxMoreSuggestionsRow;
71    public final float mMinMoreSuggestionsWidth;
72    public final int mMoreSuggestionsBottomGap;
73    public boolean mMoreSuggestionsAvailable;
74
75    // The index of these {@link ArrayList} is the position in the suggestion strip. The indices
76    // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
77    // The position of the most important suggestion is in {@link #mCenterPositionInStrip}
78    private final ArrayList<TextView> mWordViews;
79    private final ArrayList<View> mDividerViews;
80    private final ArrayList<TextView> mDebugInfoViews;
81
82    private final int mColorValidTypedWord;
83    private final int mColorTypedWord;
84    private final int mColorAutoCorrect;
85    private final int mColorSuggested;
86    private final float mAlphaObsoleted;
87    private final float mCenterSuggestionWeight;
88    private final int mCenterPositionInStrip;
89    private final Drawable mMoreSuggestionsHint;
90    private static final String MORE_SUGGESTIONS_HINT = "\u2026";
91    private static final String LEFTWARDS_ARROW = "\u2190";
92
93    private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
94    private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
95
96    private final int mSuggestionStripOption;
97    // These constants are the flag values of
98    // {@link R.styleable#SuggestionStripView_suggestionStripOption} attribute.
99    private static final int AUTO_CORRECT_BOLD = 0x01;
100    private static final int AUTO_CORRECT_UNDERLINE = 0x02;
101    private static final int VALID_TYPED_WORD_BOLD = 0x04;
102
103    private final TextView mWordToSaveView;
104    private final TextView mLeftwardsArrowView;
105    private final TextView mHintToSaveView;
106
107    public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs,
108            final int defStyle, final ArrayList<TextView> wordViews,
109            final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) {
110        mWordViews = wordViews;
111        mDividerViews = dividerViews;
112        mDebugInfoViews = debugInfoViews;
113
114        final TextView wordView = wordViews.get(0);
115        final View dividerView = dividerViews.get(0);
116        mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight();
117        dividerView.measure(
118                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
119        mDividerWidth = dividerView.getMeasuredWidth();
120
121        final Resources res = wordView.getResources();
122        mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
123
124        final TypedArray a = context.obtainStyledAttributes(attrs,
125                R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
126        mSuggestionStripOption = a.getInt(
127                R.styleable.SuggestionStripView_suggestionStripOption, 0);
128        final float alphaValidTypedWord = ResourceUtils.getFraction(a,
129                R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
130        final float alphaTypedWord = ResourceUtils.getFraction(a,
131                R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
132        final float alphaAutoCorrect = ResourceUtils.getFraction(a,
133                R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
134        final float alphaSuggested = ResourceUtils.getFraction(a,
135                R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
136        mAlphaObsoleted = ResourceUtils.getFraction(a,
137                R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
138        mColorValidTypedWord = applyAlpha(a.getColor(
139                R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
140        mColorTypedWord = applyAlpha(a.getColor(
141                R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
142        mColorAutoCorrect = applyAlpha(a.getColor(
143                R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
144        mColorSuggested = applyAlpha(a.getColor(
145                R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
146        mSuggestionsCountInStrip = a.getInt(
147                R.styleable.SuggestionStripView_suggestionsCountInStrip,
148                DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
149        mCenterSuggestionWeight = ResourceUtils.getFraction(a,
150                R.styleable.SuggestionStripView_centerSuggestionPercentile,
151                DEFAULT_CENTER_SUGGESTION_PERCENTILE);
152        mMaxMoreSuggestionsRow = a.getInt(
153                R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
154                DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
155        mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
156                R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
157        a.recycle();
158
159        mMoreSuggestionsHint = getMoreSuggestionsHint(res,
160                res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
161        mCenterPositionInStrip = mSuggestionsCountInStrip / 2;
162        mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
163                R.dimen.more_suggestions_bottom_gap);
164        mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height);
165
166        final LayoutInflater inflater = LayoutInflater.from(context);
167        mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
168        mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
169        mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
170    }
171
172    public int getMaxMoreSuggestionsRow() {
173        return mMaxMoreSuggestionsRow;
174    }
175
176    private int getMoreSuggestionsHeight() {
177        return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
178    }
179
180    public int setMoreSuggestionsHeight(final int remainingHeight) {
181        final int currentHeight = getMoreSuggestionsHeight();
182        if (currentHeight <= remainingHeight) {
183            return currentHeight;
184        }
185
186        mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
187                / mMoreSuggestionsRowHeight;
188        final int newHeight = getMoreSuggestionsHeight();
189        return newHeight;
190    }
191
192    private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize,
193            final int color) {
194        final Paint paint = new Paint();
195        paint.setAntiAlias(true);
196        paint.setTextAlign(Align.CENTER);
197        paint.setTextSize(textSize);
198        paint.setColor(color);
199        final Rect bounds = new Rect();
200        paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
201        final int width = Math.round(bounds.width() + 0.5f);
202        final int height = Math.round(bounds.height() + 0.5f);
203        final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
204        final Canvas canvas = new Canvas(buffer);
205        canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
206        return new BitmapDrawable(res, buffer);
207    }
208
209    private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords,
210            final int indexInSuggestedWords) {
211        if (indexInSuggestedWords >= suggestedWords.size()) {
212            return null;
213        }
214        final String word = suggestedWords.getWord(indexInSuggestedWords);
215        final boolean isAutoCorrect = indexInSuggestedWords == 1
216                && suggestedWords.willAutoCorrect();
217        final boolean isTypedWordValid = indexInSuggestedWords == 0
218                && suggestedWords.mTypedWordValid;
219        if (!isAutoCorrect && !isTypedWordValid) {
220            return word;
221        }
222
223        final int len = word.length();
224        final Spannable spannedWord = new SpannableString(word);
225        final int option = mSuggestionStripOption;
226        if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
227                || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
228            spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
229        }
230        if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
231            spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
232        }
233        return spannedWord;
234    }
235
236    private int getIndexInSuggestedWords(final int positionInStrip,
237            final SuggestedWords suggestedWords) {
238        // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
239        // suggestions.
240        final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect()
241                ? SuggestedWords.INDEX_OF_AUTO_CORRECTION : SuggestedWords.INDEX_OF_TYPED_WORD;
242        if (positionInStrip == mCenterPositionInStrip) {
243            return mostImportantIndexInSuggestedWords;
244        }
245        if (positionInStrip == mostImportantIndexInSuggestedWords) {
246            return mCenterPositionInStrip;
247        }
248        return positionInStrip;
249    }
250
251    private int getSuggestionTextColor(final int positionInStrip,
252            final SuggestedWords suggestedWords) {
253        final int indexInSuggestedWords = getIndexInSuggestedWords(positionInStrip, suggestedWords);
254        // TODO: Need to revisit this logic with bigram suggestions
255        final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD);
256
257        final int color;
258        if (positionInStrip == mCenterPositionInStrip && suggestedWords.willAutoCorrect()) {
259            color = mColorAutoCorrect;
260        } else if (positionInStrip == mCenterPositionInStrip && suggestedWords.mTypedWordValid) {
261            color = mColorValidTypedWord;
262        } else if (isSuggested) {
263            color = mColorSuggested;
264        } else {
265            color = mColorTypedWord;
266        }
267        if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
268            // If we auto-correct, then the autocorrection is in slot 0 and the typed word
269            // is in slot 1.
270            if (positionInStrip == mCenterPositionInStrip
271                    && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
272                            suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION),
273                            suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD))) {
274                return 0xFFFF0000;
275            }
276        }
277
278        if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
279            return applyAlpha(color, mAlphaObsoleted);
280        }
281        return color;
282    }
283
284    private static int applyAlpha(final int color, final float alpha) {
285        final int newAlpha = (int)(Color.alpha(color) * alpha);
286        return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
287    }
288
289    private static void addDivider(final ViewGroup stripView, final View dividerView) {
290        stripView.addView(dividerView);
291        final LinearLayout.LayoutParams params =
292                (LinearLayout.LayoutParams)dividerView.getLayoutParams();
293        params.gravity = Gravity.CENTER;
294    }
295
296    public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView,
297            final ViewGroup placerView) {
298        if (suggestedWords.mIsPunctuationSuggestions) {
299            layoutPunctuationSuggestions(suggestedWords, stripView);
300            return;
301        }
302
303        final int countInStrip = mSuggestionsCountInStrip;
304        setupWordViewsTextAndColor(suggestedWords, countInStrip);
305        mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
306        int x = 0;
307        for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
308            if (positionInStrip != 0) {
309                final View divider = mDividerViews.get(positionInStrip);
310                // Add divider if this isn't the left most suggestion in suggestions strip.
311                addDivider(stripView, divider);
312                x += divider.getMeasuredWidth();
313            }
314
315            final int width = getSuggestionWidth(positionInStrip, placerView.getWidth());
316            final TextView wordView = layoutWord(positionInStrip, width);
317            stripView.addView(wordView);
318            setLayoutWeight(wordView, getSuggestionWeight(positionInStrip),
319                    ViewGroup.LayoutParams.MATCH_PARENT);
320            x += wordView.getMeasuredWidth();
321
322            if (SuggestionStripView.DBG) {
323                layoutDebugInfo(positionInStrip, placerView, x);
324            }
325        }
326    }
327
328    /**
329     * Format appropriately the suggested word in {@link #mWordViews} specified by
330     * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding
331     * {@link TextView} will be disabled and never respond to user interaction. The suggested word
332     * may be shrunk or ellipsized to fit in the specified width.
333     *
334     * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices
335     * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
336     * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This
337     * usually doesn't match the index in <code>suggedtedWords</code> -- see
338     * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
339     *
340     * @param positionInStrip the position in the suggestion strip.
341     * @param width the maximum width for layout in pixels.
342     * @return the {@link TextView} containing the suggested word appropriately formatted.
343     */
344    private TextView layoutWord(final int positionInStrip, final int width) {
345        final TextView wordView = mWordViews.get(positionInStrip);
346        final CharSequence word = wordView.getText();
347        if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) {
348            // TODO: This "more suggestions hint" should have a nicely designed icon.
349            wordView.setCompoundDrawablesWithIntrinsicBounds(
350                    null, null, null, mMoreSuggestionsHint);
351            // HACK: Align with other TextViews that have no compound drawables.
352            wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
353        } else {
354            wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
355        }
356
357        // Disable this suggestion if the suggestion is null or empty.
358        wordView.setEnabled(!TextUtils.isEmpty(word));
359        final CharSequence text = getEllipsizedText(word, width, wordView.getPaint());
360        final float scaleX = wordView.getTextScaleX();
361        wordView.setText(text); // TextView.setText() resets text scale x to 1.0.
362        wordView.setTextScaleX(scaleX);
363        return wordView;
364    }
365
366    private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView,
367            final int x) {
368        final TextView debugInfoView = mDebugInfoViews.get(positionInStrip);
369        final CharSequence debugInfo = debugInfoView.getText();
370        if (debugInfo == null) {
371            return;
372        }
373        placerView.addView(debugInfoView);
374        debugInfoView.measure(
375                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
376        final int infoWidth = debugInfoView.getMeasuredWidth();
377        final int y = debugInfoView.getMeasuredHeight();
378        ViewLayoutUtils.placeViewAt(
379                debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
380    }
381
382    private int getSuggestionWidth(final int positionInStrip, final int maxWidth) {
383        final int paddings = mPadding * mSuggestionsCountInStrip;
384        final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
385        final int availableWidth = maxWidth - paddings - dividers;
386        return (int)(availableWidth * getSuggestionWeight(positionInStrip));
387    }
388
389    private float getSuggestionWeight(final int positionInStrip) {
390        if (positionInStrip == mCenterPositionInStrip) {
391            return mCenterSuggestionWeight;
392        }
393        // TODO: Revisit this for cases of 5 or more suggestions
394        return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
395    }
396
397    private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords,
398            final int countInStrip) {
399        final int count = Math.min(suggestedWords.size(), countInStrip);
400        for (int positionInStrip = 0; positionInStrip < count; positionInStrip++) {
401            final int indexInSuggestedWords =
402                    getIndexInSuggestedWords(positionInStrip, suggestedWords);
403            final TextView wordView = mWordViews.get(positionInStrip);
404            // {@link TextView#getTag()} is used to get the index in suggestedWords at
405            // {@link SuggestionStripView#onClick(View)}.
406            wordView.setTag(indexInSuggestedWords);
407            wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords));
408            wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords));
409            if (SuggestionStripView.DBG) {
410                mDebugInfoViews.get(positionInStrip).setText(
411                        Utils.getDebugInfo(suggestedWords, indexInSuggestedWords));
412            }
413        }
414        for (int positionInStrip = count; positionInStrip < countInStrip; positionInStrip++) {
415            mWordViews.get(positionInStrip).setText(null);
416            // Make this inactive for touches in {@link #layoutWord(int,int)}.
417            if (SuggestionStripView.DBG) {
418                mDebugInfoViews.get(positionInStrip).setText(null);
419            }
420        }
421    }
422
423    private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
424            final ViewGroup stripView) {
425        final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
426        for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
427            if (positionInStrip != 0) {
428                // Add divider if this isn't the left most suggestion in suggestions strip.
429                addDivider(stripView, mDividerViews.get(positionInStrip));
430            }
431
432            final TextView wordView = mWordViews.get(positionInStrip);
433            wordView.setEnabled(true);
434            wordView.setTextColor(mColorAutoCorrect);
435            final String punctuation = suggestedWords.getWord(positionInStrip);
436            wordView.setText(punctuation);
437            wordView.setTextScaleX(1.0f);
438            wordView.setCompoundDrawables(null, null, null, null);
439            stripView.addView(wordView);
440            setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight);
441        }
442        mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
443    }
444
445    public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView,
446            final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
447        final int width = stripWidth - mDividerWidth - mPadding * 2;
448
449        final TextView wordView = mWordToSaveView;
450        wordView.setTextColor(mColorTypedWord);
451        final int wordWidth = (int)(width * mCenterSuggestionWeight);
452        final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
453        final float wordScaleX = wordView.getTextScaleX();
454        wordView.setTag(word);
455        wordView.setText(text);
456        wordView.setTextScaleX(wordScaleX);
457        stripView.addView(wordView);
458        setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
459
460        stripView.addView(mDividerViews.get(0));
461
462        final TextView leftArrowView = mLeftwardsArrowView;
463        leftArrowView.setTextColor(mColorAutoCorrect);
464        leftArrowView.setText(LEFTWARDS_ARROW);
465        stripView.addView(leftArrowView);
466
467        final TextView hintView = mHintToSaveView;
468        hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
469        hintView.setTextColor(mColorAutoCorrect);
470        final int hintWidth = width - wordWidth - leftArrowView.getWidth();
471        final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
472        hintView.setText(hintText);
473        hintView.setTextScaleX(hintScaleX);
474        stripView.addView(hintView);
475        setLayoutWeight(
476                hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
477
478        wordView.setOnClickListener(listener);
479        leftArrowView.setOnClickListener(listener);
480        hintView.setOnClickListener(listener);
481    }
482
483    public CharSequence getAddToDictionaryWord() {
484        return (CharSequence)mWordToSaveView.getTag();
485    }
486
487    public boolean isAddToDictionaryShowing(final View v) {
488        return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
489    }
490
491    private static void setLayoutWeight(final View v, final float weight, final int height) {
492        final ViewGroup.LayoutParams lp = v.getLayoutParams();
493        if (lp instanceof LinearLayout.LayoutParams) {
494            final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
495            llp.weight = weight;
496            llp.width = 0;
497            llp.height = height;
498        }
499    }
500
501    private static float getTextScaleX(final CharSequence text, final int maxWidth,
502            final TextPaint paint) {
503        paint.setTextScaleX(1.0f);
504        final int width = getTextWidth(text, paint);
505        if (width <= maxWidth) {
506            return 1.0f;
507        }
508        return maxWidth / (float)width;
509    }
510
511    private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth,
512            final TextPaint paint) {
513        if (text == null) {
514            return null;
515        }
516        final float scaleX = getTextScaleX(text, maxWidth, paint);
517        if (scaleX >= MIN_TEXT_XSCALE) {
518            paint.setTextScaleX(scaleX);
519            return text;
520        }
521
522        // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
523        // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
524        final CharSequence ellipsized = TextUtils.ellipsize(
525                text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
526        paint.setTextScaleX(MIN_TEXT_XSCALE);
527        return ellipsized;
528    }
529
530    private static int getTextWidth(final CharSequence text, final TextPaint paint) {
531        if (TextUtils.isEmpty(text)) {
532            return 0;
533        }
534        final Typeface savedTypeface = paint.getTypeface();
535        paint.setTypeface(getTextTypeface(text));
536        final int len = text.length();
537        final float[] widths = new float[len];
538        final int count = paint.getTextWidths(text, 0, len, widths);
539        int width = 0;
540        for (int i = 0; i < count; i++) {
541            width += Math.round(widths[i] + 0.5f);
542        }
543        paint.setTypeface(savedTypeface);
544        return width;
545    }
546
547    private static Typeface getTextTypeface(final CharSequence text) {
548        if (!(text instanceof SpannableString)) {
549            return Typeface.DEFAULT;
550        }
551
552        final SpannableString ss = (SpannableString)text;
553        final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
554        if (styles.length == 0) {
555            return Typeface.DEFAULT;
556        }
557
558        if (styles[0].getStyle() == Typeface.BOLD) {
559            return Typeface.DEFAULT_BOLD;
560        }
561        // TODO: BOLD_ITALIC, ITALIC case?
562        return Typeface.DEFAULT;
563    }
564}
565