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