1/*
2 * Copyright (C) 2015 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 android.support.v4.widget;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.graphics.drawable.Drawable;
22import android.os.Build;
23import android.support.annotation.DrawableRes;
24import android.support.annotation.IntDef;
25import android.support.annotation.NonNull;
26import android.support.annotation.Nullable;
27import android.support.annotation.RequiresApi;
28import android.support.annotation.RestrictTo;
29import android.support.annotation.StyleRes;
30import android.support.v4.os.BuildCompat;
31import android.util.Log;
32import android.util.TypedValue;
33import android.view.View;
34import android.widget.TextView;
35
36import java.lang.annotation.Retention;
37import java.lang.annotation.RetentionPolicy;
38import java.lang.reflect.Field;
39
40/**
41 * Helper for accessing features in {@link TextView}.
42 */
43public final class TextViewCompat {
44
45    /**
46     * The TextView does not auto-size text (default).
47     */
48    public static final int AUTO_SIZE_TEXT_TYPE_NONE = TextView.AUTO_SIZE_TEXT_TYPE_NONE;
49
50    /**
51     * The TextView scales text size both horizontally and vertically to fit within the
52     * container.
53     */
54    public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM;
55
56    /** @hide */
57    @RestrictTo(LIBRARY_GROUP)
58    @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
59    @Retention(RetentionPolicy.SOURCE)
60    public @interface AutoSizeTextType {}
61
62    // Hide constructor
63    private TextViewCompat() {}
64
65    static class TextViewCompatBaseImpl {
66        private static final String LOG_TAG = "TextViewCompatBase";
67        private static final int LINES = 1;
68
69        private static Field sMaximumField;
70        private static boolean sMaximumFieldFetched;
71        private static Field sMaxModeField;
72        private static boolean sMaxModeFieldFetched;
73
74        private static Field sMinimumField;
75        private static boolean sMinimumFieldFetched;
76        private static Field sMinModeField;
77        private static boolean sMinModeFieldFetched;
78
79        public void setCompoundDrawablesRelative(@NonNull TextView textView,
80                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
81                @Nullable Drawable bottom) {
82            textView.setCompoundDrawables(start, top, end, bottom);
83        }
84
85        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
86                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
87                @Nullable Drawable bottom) {
88            textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
89        }
90
91        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
92                @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
93                @DrawableRes int bottom) {
94            textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
95        }
96
97        private static Field retrieveField(String fieldName) {
98            Field field = null;
99            try {
100                field = TextView.class.getDeclaredField(fieldName);
101                field.setAccessible(true);
102            } catch (NoSuchFieldException e) {
103                Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field.");
104            }
105            return field;
106        }
107
108        private static int retrieveIntFromField(Field field, TextView textView) {
109            try {
110                return field.getInt(textView);
111            } catch (IllegalAccessException e) {
112                Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field.");
113            }
114            return -1;
115        }
116
117        public int getMaxLines(TextView textView) {
118            if (!sMaxModeFieldFetched) {
119                sMaxModeField = retrieveField("mMaxMode");
120                sMaxModeFieldFetched = true;
121            }
122            if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) {
123                // If the max mode is using lines, we can grab the maximum value
124                if (!sMaximumFieldFetched) {
125                    sMaximumField = retrieveField("mMaximum");
126                    sMaximumFieldFetched = true;
127                }
128                if (sMaximumField != null) {
129                    return retrieveIntFromField(sMaximumField, textView);
130                }
131            }
132            return -1;
133        }
134
135        public int getMinLines(TextView textView) {
136            if (!sMinModeFieldFetched) {
137                sMinModeField = retrieveField("mMinMode");
138                sMinModeFieldFetched = true;
139            }
140            if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) {
141                // If the min mode is using lines, we can grab the maximum value
142                if (!sMinimumFieldFetched) {
143                    sMinimumField = retrieveField("mMinimum");
144                    sMinimumFieldFetched = true;
145                }
146                if (sMinimumField != null) {
147                    return retrieveIntFromField(sMinimumField, textView);
148                }
149            }
150            return -1;
151        }
152
153        @SuppressWarnings("deprecation")
154        public void setTextAppearance(TextView textView, @StyleRes int resId) {
155            textView.setTextAppearance(textView.getContext(), resId);
156        }
157
158        public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
159            return textView.getCompoundDrawables();
160        }
161
162        public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
163            if (textView instanceof AutoSizeableTextView) {
164                ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType);
165            }
166        }
167
168        public void setAutoSizeTextTypeUniformWithConfiguration(
169                TextView textView,
170                int autoSizeMinTextSize,
171                int autoSizeMaxTextSize,
172                int autoSizeStepGranularity,
173                int unit) throws IllegalArgumentException {
174            if (textView instanceof AutoSizeableTextView) {
175                ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
176                        autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
177            }
178        }
179
180        public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView,
181                @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
182            if (textView instanceof AutoSizeableTextView) {
183                ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
184                        presetSizes, unit);
185            }
186        }
187
188        public int getAutoSizeTextType(TextView textView) {
189            if (textView instanceof AutoSizeableTextView) {
190                return ((AutoSizeableTextView) textView).getAutoSizeTextType();
191            }
192            return AUTO_SIZE_TEXT_TYPE_NONE;
193        }
194
195        public int getAutoSizeStepGranularity(TextView textView) {
196            if (textView instanceof AutoSizeableTextView) {
197                return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity();
198            }
199            return -1;
200        }
201
202        public int getAutoSizeMinTextSize(TextView textView) {
203            if (textView instanceof AutoSizeableTextView) {
204                return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize();
205            }
206            return -1;
207        }
208
209        public int getAutoSizeMaxTextSize(TextView textView) {
210            if (textView instanceof AutoSizeableTextView) {
211                return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize();
212            }
213            return -1;
214        }
215
216        public int[] getAutoSizeTextAvailableSizes(TextView textView) {
217            if (textView instanceof AutoSizeableTextView) {
218                return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes();
219            }
220            return new int[0];
221        }
222    }
223
224    @RequiresApi(16)
225    static class TextViewCompatApi16Impl extends TextViewCompatBaseImpl {
226        @Override
227        public int getMaxLines(TextView textView) {
228            return textView.getMaxLines();
229        }
230
231        @Override
232        public int getMinLines(TextView textView) {
233            return textView.getMinLines();
234        }
235    }
236
237    @RequiresApi(17)
238    static class TextViewCompatApi17Impl extends TextViewCompatApi16Impl {
239        @Override
240        public void setCompoundDrawablesRelative(@NonNull TextView textView,
241                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
242                @Nullable Drawable bottom) {
243            boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
244            textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
245        }
246
247        @Override
248        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
249                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
250                @Nullable Drawable bottom) {
251            boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
252            textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
253                    rtl ? start : end,  bottom);
254        }
255
256        @Override
257        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
258                @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
259                @DrawableRes int bottom) {
260            boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
261            textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
262                    rtl ? start : end, bottom);
263        }
264
265        @Override
266        public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
267            final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
268            final Drawable[] compounds = textView.getCompoundDrawables();
269            if (rtl) {
270                // If we're on RTL, we need to invert the horizontal result like above
271                final Drawable start = compounds[2];
272                final Drawable end = compounds[0];
273                compounds[0] = start;
274                compounds[2] = end;
275            }
276            return compounds;
277        }
278    }
279
280    @RequiresApi(18)
281    static class TextViewCompatApi18Impl extends TextViewCompatApi17Impl {
282        @Override
283        public void setCompoundDrawablesRelative(@NonNull TextView textView,
284                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
285                @Nullable Drawable bottom) {
286            textView.setCompoundDrawablesRelative(start, top, end, bottom);
287        }
288
289        @Override
290        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
291                @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
292                @Nullable Drawable bottom) {
293            textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
294        }
295
296        @Override
297        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
298                @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
299                @DrawableRes int bottom) {
300            textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
301        }
302
303        @Override
304        public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
305            return textView.getCompoundDrawablesRelative();
306        }
307    }
308
309    @RequiresApi(23)
310    static class TextViewCompatApi23Impl extends TextViewCompatApi18Impl {
311        @Override
312        public void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
313            textView.setTextAppearance(resId);
314        }
315    }
316
317    @RequiresApi(27)
318    static class TextViewCompatApi27Impl extends TextViewCompatApi23Impl {
319        @Override
320        public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
321            textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
322        }
323
324        @Override
325        public void setAutoSizeTextTypeUniformWithConfiguration(
326                TextView textView,
327                int autoSizeMinTextSize,
328                int autoSizeMaxTextSize,
329                int autoSizeStepGranularity,
330                int unit) throws IllegalArgumentException {
331            textView.setAutoSizeTextTypeUniformWithConfiguration(
332                    autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
333        }
334
335        @Override
336        public void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView,
337                @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
338            textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
339        }
340
341        @Override
342        public int getAutoSizeTextType(TextView textView) {
343            return textView.getAutoSizeTextType();
344        }
345
346        @Override
347        public int getAutoSizeStepGranularity(TextView textView) {
348            return textView.getAutoSizeStepGranularity();
349        }
350
351        @Override
352        public int getAutoSizeMinTextSize(TextView textView) {
353            return textView.getAutoSizeMinTextSize();
354        }
355
356        @Override
357        public int getAutoSizeMaxTextSize(TextView textView) {
358            return textView.getAutoSizeMaxTextSize();
359        }
360
361        @Override
362        public int[] getAutoSizeTextAvailableSizes(TextView textView) {
363            return textView.getAutoSizeTextAvailableSizes();
364        }
365    }
366
367    static final TextViewCompatBaseImpl IMPL;
368
369    static {
370        if (BuildCompat.isAtLeastOMR1()) {
371            IMPL = new TextViewCompatApi27Impl();
372        } else if (Build.VERSION.SDK_INT >= 23) {
373            IMPL = new TextViewCompatApi23Impl();
374        } else if (Build.VERSION.SDK_INT >= 18) {
375            IMPL = new TextViewCompatApi18Impl();
376        } else if (Build.VERSION.SDK_INT >= 17) {
377            IMPL = new TextViewCompatApi17Impl();
378        } else if (Build.VERSION.SDK_INT >= 16) {
379            IMPL = new TextViewCompatApi16Impl();
380        } else {
381            IMPL = new TextViewCompatBaseImpl();
382        }
383    }
384
385    /**
386     * Sets the Drawables (if any) to appear to the start of, above, to the end
387     * of, and below the text. Use {@code null} if you do not want a Drawable
388     * there. The Drawables must already have had {@link Drawable#setBounds}
389     * called.
390     * <p/>
391     * Calling this method will overwrite any Drawables previously set using
392     * {@link TextView#setCompoundDrawables} or related methods.
393     *
394     * @param textView The TextView against which to invoke the method.
395     * @attr name android:drawableStart
396     * @attr name android:drawableTop
397     * @attr name android:drawableEnd
398     * @attr name android:drawableBottom
399     */
400    public static void setCompoundDrawablesRelative(@NonNull TextView textView,
401            @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
402            @Nullable Drawable bottom) {
403        IMPL.setCompoundDrawablesRelative(textView, start, top, end, bottom);
404    }
405
406    /**
407     * Sets the Drawables (if any) to appear to the start of, above, to the end
408     * of, and below the text. Use {@code null} if you do not want a Drawable
409     * there. The Drawables' bounds will be set to their intrinsic bounds.
410     * <p/>
411     * Calling this method will overwrite any Drawables previously set using
412     * {@link TextView#setCompoundDrawables} or related methods.
413     *
414     * @param textView The TextView against which to invoke the method.
415     * @attr name android:drawableStart
416     * @attr name android:drawableTop
417     * @attr name android:drawableEnd
418     * @attr name android:drawableBottom
419     */
420    public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
421            @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
422            @Nullable Drawable bottom) {
423        IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom);
424    }
425
426    /**
427     * Sets the Drawables (if any) to appear to the start of, above, to the end
428     * of, and below the text. Use 0 if you do not want a Drawable there. The
429     * Drawables' bounds will be set to their intrinsic bounds.
430     * <p/>
431     * Calling this method will overwrite any Drawables previously set using
432     * {@link TextView#setCompoundDrawables} or related methods.
433     *
434     * @param textView The TextView against which to invoke the method.
435     * @param start    Resource identifier of the start Drawable.
436     * @param top      Resource identifier of the top Drawable.
437     * @param end      Resource identifier of the end Drawable.
438     * @param bottom   Resource identifier of the bottom Drawable.
439     * @attr name android:drawableStart
440     * @attr name android:drawableTop
441     * @attr name android:drawableEnd
442     * @attr name android:drawableBottom
443     */
444    public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
445            @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
446            @DrawableRes int bottom) {
447        IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom);
448    }
449
450    /**
451     * Returns the maximum number of lines displayed in the given TextView, or -1 if the maximum
452     * height was set in pixels instead.
453     */
454    public static int getMaxLines(@NonNull TextView textView) {
455        return IMPL.getMaxLines(textView);
456    }
457
458    /**
459     * Returns the minimum number of lines displayed in the given TextView, or -1 if the minimum
460     * height was set in pixels instead.
461     */
462    public static int getMinLines(@NonNull TextView textView) {
463        return IMPL.getMinLines(textView);
464    }
465
466    /**
467     * Sets the text appearance from the specified style resource.
468     * <p>
469     * Use a framework-defined {@code TextAppearance} style like
470     * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}.
471     *
472     * @param textView The TextView against which to invoke the method.
473     * @param resId    The resource identifier of the style to apply.
474     */
475    public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
476        IMPL.setTextAppearance(textView, resId);
477    }
478
479    /**
480     * Returns drawables for the start, top, end, and bottom borders from the given text view.
481     */
482    public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
483        return IMPL.getCompoundDrawablesRelative(textView);
484    }
485
486    /**
487     * Specify whether this widget should automatically scale the text to try to perfectly fit
488     * within the layout bounds by using the default auto-size configuration.
489     *
490     * @param autoSizeTextType the type of auto-size. Must be one of
491     *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
492     *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
493     *
494     * @attr name android:autoSizeTextType
495     */
496    public static void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
497        IMPL.setAutoSizeTextTypeWithDefaults(textView, autoSizeTextType);
498    }
499
500    /**
501     * Specify whether this widget should automatically scale the text to try to perfectly fit
502     * within the layout bounds. If all the configuration params are valid the type of auto-size is
503     * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
504     *
505     * @param autoSizeMinTextSize the minimum text size available for auto-size
506     * @param autoSizeMaxTextSize the maximum text size available for auto-size
507     * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
508     *                                the minimum and maximum text size in order to build the set of
509     *                                text sizes the system uses to choose from when auto-sizing
510     * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
511     *             possible dimension units
512     *
513     * @throws IllegalArgumentException if any of the configuration params are invalid.
514     *
515     * @attr name android:autoSizeTextType
516     * @attr name android:autoSizeTextType
517     * @attr name android:autoSizeMinTextSize
518     * @attr name android:autoSizeMaxTextSize
519     * @attr name android:autoSizeStepGranularity
520     */
521    public static void setAutoSizeTextTypeUniformWithConfiguration(
522            TextView textView,
523            int autoSizeMinTextSize,
524            int autoSizeMaxTextSize,
525            int autoSizeStepGranularity,
526            int unit) throws IllegalArgumentException {
527        IMPL.setAutoSizeTextTypeUniformWithConfiguration(textView, autoSizeMinTextSize,
528                autoSizeMaxTextSize, autoSizeStepGranularity, unit);
529    }
530
531    /**
532     * Specify whether this widget should automatically scale the text to try to perfectly fit
533     * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
534     * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
535     *
536     * @param presetSizes an {@code int} array of sizes in pixels
537     * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
538     *             the possible dimension units
539     *
540     * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
541     *_
542     * @attr name android:autoSizeTextType
543     * @attr name android:autoSizePresetSizes
544     */
545    public static void setAutoSizeTextTypeUniformWithPresetSizes(TextView textView,
546            @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
547        IMPL.setAutoSizeTextTypeUniformWithPresetSizes(textView, presetSizes, unit);
548    }
549
550    /**
551     * Returns the type of auto-size set for this widget.
552     *
553     * @return an {@code int} corresponding to one of the auto-size types:
554     *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
555     *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
556     *
557     * @attr name android:autoSizeTextType
558     */
559    public static int getAutoSizeTextType(TextView textView) {
560        return IMPL.getAutoSizeTextType(textView);
561    }
562
563    /**
564     * @return the current auto-size step granularity in pixels.
565     *
566     * @attr name android:autoSizeStepGranularity
567     */
568    public static int getAutoSizeStepGranularity(TextView textView) {
569        return IMPL.getAutoSizeStepGranularity(textView);
570    }
571
572    /**
573     * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
574     *         if auto-size has not been configured this function returns {@code -1}.
575     *
576     * @attr name android:autoSizeMinTextSize
577     */
578    public static int getAutoSizeMinTextSize(TextView textView) {
579        return IMPL.getAutoSizeMinTextSize(textView);
580    }
581
582    /**
583     * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
584     *         if auto-size has not been configured this function returns {@code -1}.
585     *
586     * @attr name android:autoSizeMaxTextSize
587     */
588    public static int getAutoSizeMaxTextSize(TextView textView) {
589        return IMPL.getAutoSizeMaxTextSize(textView);
590    }
591
592    /**
593     * @return the current auto-size {@code int} sizes array (in pixels).
594     *
595     * @attr name android:autoSizePresetSizes
596     */
597    public static int[] getAutoSizeTextAvailableSizes(TextView textView) {
598        return IMPL.getAutoSizeTextAvailableSizes(textView);
599    }
600}
601