1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.content.res.TypedArray;
18import android.text.Layout;
19import android.util.AttributeSet;
20import android.util.TypedValue;
21import android.widget.TextView;
22
23import android.support.v17.leanback.R;
24
25/**
26 * <p>A {@link android.widget.TextView} that adjusts text size automatically in response
27 * to certain trigger conditions, such as text that wraps over multiple lines.</p>
28 * @hide
29 */
30class ResizingTextView extends TextView {
31
32    /**
33     * Trigger text resize when text flows into the last line of a multi-line text view.
34     */
35    public static final int TRIGGER_MAX_LINES = 0x01;
36
37    private int mTriggerConditions; // Union of trigger conditions
38    private int mResizedTextSize;
39    // Note: Maintaining line spacing turned out not to be useful, and will be removed in
40    // the next round of design for this class (b/18736630). For now it simply defaults to false.
41    private boolean mMaintainLineSpacing;
42    private int mResizedPaddingAdjustmentTop;
43    private int mResizedPaddingAdjustmentBottom;
44
45    private boolean mIsResized = false;
46    // Remember default properties in case we need to restore them
47    private boolean mDefaultsInitialized = false;
48    private int mDefaultTextSize;
49    private float mDefaultLineSpacingExtra;
50    private int mDefaultPaddingTop;
51    private int mDefaultPaddingBottom;
52
53    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
54        super(ctx, attrs, defStyleAttr);
55        TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.lbResizingTextView,
56                defStyleAttr, defStyleRes);
57
58        try {
59            mTriggerConditions = a.getInt(
60                    R.styleable.lbResizingTextView_resizeTrigger, TRIGGER_MAX_LINES);
61            mResizedTextSize = a.getDimensionPixelSize(
62                    R.styleable.lbResizingTextView_resizedTextSize, -1);
63            mMaintainLineSpacing = a.getBoolean(
64                    R.styleable.lbResizingTextView_maintainLineSpacing, false);
65            mResizedPaddingAdjustmentTop = a.getDimensionPixelOffset(
66                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentTop, 0);
67            mResizedPaddingAdjustmentBottom = a.getDimensionPixelOffset(
68                    R.styleable.lbResizingTextView_resizedPaddingAdjustmentBottom, 0);
69        } finally {
70            a.recycle();
71        }
72    }
73
74    public ResizingTextView(Context ctx, AttributeSet attrs, int defStyleAttr) {
75        this(ctx, attrs, defStyleAttr, 0);
76    }
77
78    public ResizingTextView(Context ctx, AttributeSet attrs) {
79        // TODO We should define our own style that inherits from TextViewStyle, to set defaults
80        // for new styleables,  We then pass the appropriate R.attr up the constructor chain here.
81        this(ctx, attrs, android.R.attr.textViewStyle);
82    }
83
84    public ResizingTextView(Context ctx) {
85        this(ctx, null);
86    }
87
88    /**
89     * @return the trigger conditions used to determine whether resize occurs
90     */
91    public int getTriggerConditions() {
92        return mTriggerConditions;
93    }
94
95    /**
96     * Set the trigger conditions used to determine whether resize occurs. Pass
97     * a union of trigger condition constants, such as {@link ResizingTextView#TRIGGER_MAX_LINES}.
98     *
99     * @param conditions A union of trigger condition constants
100     */
101    public void setTriggerConditions(int conditions) {
102        if (mTriggerConditions != conditions) {
103            mTriggerConditions = conditions;
104            // Always request a layout when trigger conditions change
105            requestLayout();
106        }
107    }
108
109    /**
110     * @return the resized text size
111     */
112    public int getResizedTextSize() {
113        return mResizedTextSize;
114    }
115
116    /**
117     * Set the text size for resized text.
118     *
119     * @param size The text size for resized text
120     */
121    public void setResizedTextSize(int size) {
122        if (mResizedTextSize != size) {
123            mResizedTextSize = size;
124            resizeParamsChanged();
125        }
126    }
127
128    /**
129     * @return whether or not to maintain line spacing when resizing text.
130     * The default is true.
131     */
132    public boolean getMaintainLineSpacing() {
133        return mMaintainLineSpacing;
134    }
135
136    /**
137     * Set whether or not to maintain line spacing when resizing text.
138     * The default is true.
139     *
140     * @param maintain Whether or not to maintain line spacing
141     */
142    public void setMaintainLineSpacing(boolean maintain) {
143        if (mMaintainLineSpacing != maintain) {
144            mMaintainLineSpacing = maintain;
145            resizeParamsChanged();
146        }
147    }
148
149    /**
150     * @return desired adjustment to top padding for resized text
151     */
152    public int getResizedPaddingAdjustmentTop() {
153        return mResizedPaddingAdjustmentTop;
154    }
155
156    /**
157     * Set the desired adjustment to top padding for resized text.
158     *
159     * @param adjustment The adjustment to top padding, in pixels
160     */
161    public void setResizedPaddingAdjustmentTop(int adjustment) {
162        if (mResizedPaddingAdjustmentTop != adjustment) {
163            mResizedPaddingAdjustmentTop = adjustment;
164            resizeParamsChanged();
165        }
166    }
167
168    /**
169     * @return desired adjustment to bottom padding for resized text
170     */
171    public int getResizedPaddingAdjustmentBottom() {
172        return mResizedPaddingAdjustmentBottom;
173    }
174
175    /**
176     * Set the desired adjustment to bottom padding for resized text.
177     *
178     * @param adjustment The adjustment to bottom padding, in pixels
179     */
180    public void setResizedPaddingAdjustmentBottom(int adjustment) {
181        if (mResizedPaddingAdjustmentBottom != adjustment) {
182            mResizedPaddingAdjustmentBottom = adjustment;
183            resizeParamsChanged();
184        }
185    }
186
187    private void resizeParamsChanged() {
188        // If we're not resized, then changing resize parameters doesn't
189        // affect layout, so don't bother requesting.
190        if (mIsResized) {
191            requestLayout();
192        }
193    }
194
195    @Override
196    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
197        if (!mDefaultsInitialized) {
198            mDefaultTextSize = (int) getTextSize();
199            mDefaultLineSpacingExtra = getLineSpacingExtra();
200            mDefaultPaddingTop = getPaddingTop();
201            mDefaultPaddingBottom = getPaddingBottom();
202            mDefaultsInitialized = true;
203        }
204
205        // Always try first to measure with defaults. Otherwise, we may think we can get away
206        // with larger text sizes later when we actually can't.
207        setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
208        setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
209        setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
210
211        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
212
213        boolean resizeText = false;
214
215        final Layout layout = getLayout();
216        if (layout != null) {
217            if ((mTriggerConditions & TRIGGER_MAX_LINES) > 0) {
218                final int lineCount = layout.getLineCount();
219                final int maxLines = getMaxLines();
220                if (maxLines > 1) {
221                    resizeText = lineCount == maxLines;
222                }
223            }
224        }
225
226        final int currentSizePx = (int) getTextSize();
227        boolean remeasure = false;
228        if (resizeText) {
229            if (mResizedTextSize != -1 && currentSizePx != mResizedTextSize) {
230                setTextSize(TypedValue.COMPLEX_UNIT_PX, mResizedTextSize);
231                remeasure = true;
232            }
233            // Check for other desired adjustments in addition to the text size
234            final float targetLineSpacingExtra = mDefaultLineSpacingExtra +
235                    mDefaultTextSize - mResizedTextSize;
236            if (mMaintainLineSpacing && getLineSpacingExtra() != targetLineSpacingExtra) {
237                setLineSpacing(targetLineSpacingExtra, getLineSpacingMultiplier());
238                remeasure = true;
239            }
240            final int paddingTop = mDefaultPaddingTop + mResizedPaddingAdjustmentTop;
241            final int paddingBottom = mDefaultPaddingBottom + mResizedPaddingAdjustmentBottom;
242            if (getPaddingTop() != paddingTop || getPaddingBottom() != paddingBottom) {
243                setPaddingTopAndBottom(paddingTop, paddingBottom);
244                remeasure = true;
245            }
246        } else {
247            // Use default size, line spacing, and padding
248            if (mResizedTextSize != -1 && currentSizePx != mDefaultTextSize) {
249                setTextSize(TypedValue.COMPLEX_UNIT_PX, mDefaultTextSize);
250                remeasure = true;
251            }
252            if (mMaintainLineSpacing && getLineSpacingExtra() != mDefaultLineSpacingExtra) {
253                setLineSpacing(mDefaultLineSpacingExtra, getLineSpacingMultiplier());
254                remeasure = true;
255            }
256            if (getPaddingTop() != mDefaultPaddingTop ||
257                    getPaddingBottom() != mDefaultPaddingBottom) {
258                setPaddingTopAndBottom(mDefaultPaddingTop, mDefaultPaddingBottom);
259                remeasure = true;
260            }
261        }
262        mIsResized = resizeText;
263        if (remeasure) {
264            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
265        }
266    }
267
268    private void setPaddingTopAndBottom(int paddingTop, int paddingBottom) {
269        if (isPaddingRelative()) {
270            setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), paddingBottom);
271        } else {
272            setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), paddingBottom);
273        }
274    }
275}
276