1 /*
2  * Copyright (C) 2014 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 
17 package androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 import static androidx.appcompat.content.res.AppCompatResources.getColorStateList;
21 import static androidx.appcompat.widget.ThemeUtils.getDisabledThemeAttrColor;
22 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColor;
23 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColorStateList;
24 import static androidx.core.graphics.ColorUtils.compositeColors;
25 
26 import android.content.Context;
27 import android.content.res.ColorStateList;
28 import android.graphics.Bitmap;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.PorterDuff;
32 import android.graphics.PorterDuffColorFilter;
33 import android.graphics.Shader;
34 import android.graphics.drawable.BitmapDrawable;
35 import android.graphics.drawable.Drawable;
36 import android.graphics.drawable.LayerDrawable;
37 import android.util.Log;
38 
39 import androidx.annotation.ColorInt;
40 import androidx.annotation.DimenRes;
41 import androidx.annotation.DrawableRes;
42 import androidx.annotation.RestrictTo;
43 import androidx.appcompat.R;
44 import androidx.core.graphics.drawable.DrawableCompat;
45 
46 import org.jspecify.annotations.NonNull;
47 
48 /**
49  */
50 @RestrictTo(LIBRARY_GROUP_PREFIX)
51 public final class AppCompatDrawableManager {
52     private static final String TAG = "AppCompatDrawableManag";
53     private static final boolean DEBUG = false;
54     private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
55 
56     private static AppCompatDrawableManager INSTANCE;
57 
preload()58     public static synchronized void preload() {
59         if (INSTANCE == null) {
60             INSTANCE = new AppCompatDrawableManager();
61             INSTANCE.mResourceManager = ResourceManagerInternal.get();
62             INSTANCE.mResourceManager.setHooks(new ResourceManagerInternal.ResourceManagerHooks() {
63                 /**
64                  * Drawables which should be tinted with the value of
65                  * {@code R.attr.colorControlNormal}, using the default mode using a raw color
66                  * filter.
67                  */
68                 private final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = {
69                         R.drawable.abc_textfield_search_default_mtrl_alpha,
70                         R.drawable.abc_textfield_default_mtrl_alpha,
71                         R.drawable.abc_ab_share_pack_mtrl_alpha
72                 };
73 
74                 /**
75                  * Drawables which should be tinted with the value of
76                  * {@code R.attr.colorControlNormal}, using {@link DrawableCompat}'s tinting
77                  * functionality.
78                  */
79                 private final int[] TINT_COLOR_CONTROL_NORMAL = {
80                         R.drawable.abc_ic_commit_search_api_mtrl_alpha,
81                         R.drawable.abc_seekbar_tick_mark_material,
82                         R.drawable.abc_ic_menu_share_mtrl_alpha,
83                         R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
84                         R.drawable.abc_ic_menu_cut_mtrl_alpha,
85                         R.drawable.abc_ic_menu_selectall_mtrl_alpha,
86                         R.drawable.abc_ic_menu_paste_mtrl_am_alpha
87                 };
88 
89                 /**
90                  * Drawables which should be tinted with the value of
91                  * {@code R.attr.colorControlActivated}, using a color filter.
92                  */
93                 private final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = {
94                         R.drawable.abc_textfield_activated_mtrl_alpha,
95                         R.drawable.abc_textfield_search_activated_mtrl_alpha,
96                         R.drawable.abc_cab_background_top_mtrl_alpha,
97                         R.drawable.abc_text_cursor_material,
98                         R.drawable.abc_text_select_handle_left_mtrl,
99                         R.drawable.abc_text_select_handle_middle_mtrl,
100                         R.drawable.abc_text_select_handle_right_mtrl
101                 };
102 
103                 /**
104                  * Drawables which should be tinted with the value of
105                  * {@code android.R.attr.colorBackground}, using the
106                  * {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter.
107                  */
108                 private final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = {
109                         R.drawable.abc_popup_background_mtrl_mult,
110                         R.drawable.abc_cab_background_internal_bg,
111                         R.drawable.abc_menu_hardkey_panel_mtrl_mult
112                 };
113 
114                 /**
115                  * Drawables which should be tinted using a state list containing values of
116                  * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
117                  */
118                 private final int[] TINT_COLOR_CONTROL_STATE_LIST = {
119                         R.drawable.abc_tab_indicator_material,
120                         R.drawable.abc_textfield_search_material
121                 };
122 
123                 /**
124                  * Drawables which should be tinted using a state list containing values of
125                  * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
126                  * for the checked state.
127                  */
128                 private final int[] TINT_CHECKABLE_BUTTON_LIST = {
129                         R.drawable.abc_btn_check_material,
130                         R.drawable.abc_btn_radio_material,
131                         R.drawable.abc_btn_check_material_anim,
132                         R.drawable.abc_btn_radio_material_anim
133                 };
134 
135                 private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) {
136                     return createButtonColorStateList(context,
137                             getThemeAttrColor(context, R.attr.colorButtonNormal));
138                 }
139 
140                 private ColorStateList createBorderlessButtonColorStateList(
141                         @NonNull Context context) {
142                     // We ignore the custom tint for borderless buttons
143                     return createButtonColorStateList(context, Color.TRANSPARENT);
144                 }
145 
146                 private ColorStateList createColoredButtonColorStateList(
147                         @NonNull Context context) {
148                     return createButtonColorStateList(context,
149                             getThemeAttrColor(context, R.attr.colorAccent));
150                 }
151 
152                 private ColorStateList createButtonColorStateList(final @NonNull Context context,
153                         @ColorInt final int baseColor) {
154                     final int[][] states = new int[4][];
155                     final int[] colors = new int[4];
156                     int i = 0;
157 
158                     final int colorControlHighlight = getThemeAttrColor(context,
159                             R.attr.colorControlHighlight);
160                     final int disabledColor = getDisabledThemeAttrColor(context,
161                             R.attr.colorButtonNormal);
162 
163                     // Disabled state
164                     states[i] = ThemeUtils.DISABLED_STATE_SET;
165                     colors[i] = disabledColor;
166                     i++;
167 
168                     states[i] = ThemeUtils.PRESSED_STATE_SET;
169                     colors[i] = compositeColors(colorControlHighlight, baseColor);
170                     i++;
171 
172                     states[i] = ThemeUtils.FOCUSED_STATE_SET;
173                     colors[i] = compositeColors(colorControlHighlight, baseColor);
174                     i++;
175 
176                     // Default enabled state
177                     states[i] = ThemeUtils.EMPTY_STATE_SET;
178                     colors[i] = baseColor;
179                     i++;
180 
181                     return new ColorStateList(states, colors);
182                 }
183 
184                 private ColorStateList createSwitchThumbColorStateList(Context context) {
185                     final int[][] states = new int[3][];
186                     final int[] colors = new int[3];
187                     int i = 0;
188 
189                     final ColorStateList thumbColor = getThemeAttrColorStateList(context,
190                             R.attr.colorSwitchThumbNormal);
191 
192                     if (thumbColor != null && thumbColor.isStateful()) {
193                         // If colorSwitchThumbNormal is a valid ColorStateList, extract the
194                         // default and disabled colors from it
195 
196                         // Disabled state
197                         states[i] = ThemeUtils.DISABLED_STATE_SET;
198                         colors[i] = thumbColor.getColorForState(states[i], 0);
199                         i++;
200 
201                         states[i] = ThemeUtils.CHECKED_STATE_SET;
202                         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
203                         i++;
204 
205                         // Default enabled state
206                         states[i] = ThemeUtils.EMPTY_STATE_SET;
207                         colors[i] = thumbColor.getDefaultColor();
208                         i++;
209                     } else {
210                         // Else we'll use an approximation using the default disabled alpha
211 
212                         // Disabled state
213                         states[i] = ThemeUtils.DISABLED_STATE_SET;
214                         colors[i] = getDisabledThemeAttrColor(context,
215                                 R.attr.colorSwitchThumbNormal);
216                         i++;
217 
218                         states[i] = ThemeUtils.CHECKED_STATE_SET;
219                         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
220                         i++;
221 
222                         // Default enabled state
223                         states[i] = ThemeUtils.EMPTY_STATE_SET;
224                         colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
225                         i++;
226                     }
227 
228                     return new ColorStateList(states, colors);
229                 }
230 
231                 @Override
232                 public Drawable createDrawableFor(@NonNull ResourceManagerInternal resourceManager,
233                         @NonNull Context context, int resId) {
234                     if (resId == R.drawable.abc_cab_background_top_material) {
235                         return new LayerDrawable(new Drawable[]{
236                                 resourceManager.getDrawable(context,
237                                         R.drawable.abc_cab_background_internal_bg),
238                                 resourceManager.getDrawable(context,
239                                         R.drawable.abc_cab_background_top_mtrl_alpha)
240                         });
241                     }
242                     if (resId == R.drawable.abc_ratingbar_material) {
243                         return getRatingBarLayerDrawable(resourceManager, context,
244                                 R.dimen.abc_star_big);
245                     }
246                     if (resId == R.drawable.abc_ratingbar_indicator_material) {
247                         return getRatingBarLayerDrawable(resourceManager, context,
248                                 R.dimen.abc_star_medium);
249                     }
250                     if (resId == R.drawable.abc_ratingbar_small_material) {
251                         return getRatingBarLayerDrawable(resourceManager, context,
252                                 R.dimen.abc_star_small);
253                     }
254                     return null;
255                 }
256 
257                 private LayerDrawable getRatingBarLayerDrawable(
258                         @NonNull ResourceManagerInternal resourceManager,
259                         @NonNull Context context, @DimenRes int dimenResId) {
260                     int starSize = context.getResources().getDimensionPixelSize(dimenResId);
261 
262                     Drawable star = resourceManager.getDrawable(context,
263                             R.drawable.abc_star_black_48dp);
264                     Drawable halfStar = resourceManager.getDrawable(context,
265                             R.drawable.abc_star_half_black_48dp);
266 
267                     BitmapDrawable starBitmapDrawable;
268                     BitmapDrawable tiledStarBitmapDrawable;
269                     if ((star instanceof BitmapDrawable) && (star.getIntrinsicWidth() == starSize)
270                             && (star.getIntrinsicHeight() == starSize)) {
271                         // no need for extra conversion
272                         starBitmapDrawable = (BitmapDrawable) star;
273 
274                         tiledStarBitmapDrawable =
275                                 new BitmapDrawable(starBitmapDrawable.getBitmap());
276                     } else {
277                         Bitmap bitmapStar = Bitmap.createBitmap(starSize, starSize,
278                                 Bitmap.Config.ARGB_8888);
279                         Canvas canvasStar = new Canvas(bitmapStar);
280                         star.setBounds(0, 0, starSize, starSize);
281                         star.draw(canvasStar);
282                         starBitmapDrawable = new BitmapDrawable(bitmapStar);
283 
284                         tiledStarBitmapDrawable = new BitmapDrawable(bitmapStar);
285                     }
286                     tiledStarBitmapDrawable.setTileModeX(Shader.TileMode.REPEAT);
287 
288                     BitmapDrawable halfStarBitmapDrawable;
289                     if ((halfStar instanceof BitmapDrawable)
290                             && (halfStar.getIntrinsicWidth() == starSize)
291                             && (halfStar.getIntrinsicHeight() == starSize)) {
292                         // no need for extra conversion
293                         halfStarBitmapDrawable = (BitmapDrawable) halfStar;
294                     } else {
295                         Bitmap bitmapHalfStar = Bitmap.createBitmap(starSize, starSize,
296                                 Bitmap.Config.ARGB_8888);
297                         Canvas canvasHalfStar = new Canvas(bitmapHalfStar);
298                         halfStar.setBounds(0, 0, starSize, starSize);
299                         halfStar.draw(canvasHalfStar);
300                         halfStarBitmapDrawable = new BitmapDrawable(bitmapHalfStar);
301                     }
302 
303                     LayerDrawable result = new LayerDrawable(new Drawable[]{
304                             starBitmapDrawable, halfStarBitmapDrawable, tiledStarBitmapDrawable
305                     });
306                     result.setId(0, android.R.id.background);
307                     result.setId(1, android.R.id.secondaryProgress);
308                     result.setId(2, android.R.id.progress);
309                     return result;
310                 }
311 
312                 private void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
313                     d = d.mutate();
314                     d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE
315                             : mode));
316                 }
317 
318                 @Override
319                 public boolean tintDrawable(@NonNull Context context, int resId,
320                         @NonNull Drawable drawable) {
321                     if (resId == R.drawable.abc_seekbar_track_material) {
322                         LayerDrawable ld = (LayerDrawable) drawable;
323                         setPorterDuffColorFilter(
324                                 ld.findDrawableByLayerId(android.R.id.background),
325                                 getThemeAttrColor(context, R.attr.colorControlNormal),
326                                 DEFAULT_MODE);
327                         setPorterDuffColorFilter(
328                                 ld.findDrawableByLayerId(android.R.id.secondaryProgress),
329                                 getThemeAttrColor(context, R.attr.colorControlNormal),
330                                 DEFAULT_MODE);
331                         setPorterDuffColorFilter(
332                                 ld.findDrawableByLayerId(android.R.id.progress),
333                                 getThemeAttrColor(context, R.attr.colorControlActivated),
334                                 DEFAULT_MODE);
335                         return true;
336                     } else if (resId == R.drawable.abc_ratingbar_material
337                             || resId == R.drawable.abc_ratingbar_indicator_material
338                             || resId == R.drawable.abc_ratingbar_small_material) {
339                         LayerDrawable ld = (LayerDrawable) drawable;
340                         setPorterDuffColorFilter(
341                                 ld.findDrawableByLayerId(android.R.id.background),
342                                 getDisabledThemeAttrColor(context, R.attr.colorControlNormal),
343                                 DEFAULT_MODE);
344                         setPorterDuffColorFilter(
345                                 ld.findDrawableByLayerId(android.R.id.secondaryProgress),
346                                 getThemeAttrColor(context, R.attr.colorControlActivated),
347                                 DEFAULT_MODE);
348                         setPorterDuffColorFilter(
349                                 ld.findDrawableByLayerId(android.R.id.progress),
350                                 getThemeAttrColor(context, R.attr.colorControlActivated),
351                                 DEFAULT_MODE);
352                         return true;
353                     }
354                     return false;
355                 }
356 
357                 private boolean arrayContains(int[] array, int value) {
358                     for (int id : array) {
359                         if (id == value) {
360                             return true;
361                         }
362                     }
363                     return false;
364                 }
365 
366                 @Override
367                 public ColorStateList getTintListForDrawableRes(@NonNull Context context,
368                         int resId) {
369                     // ...if the cache did not contain a color state list, try and create one
370                     if (resId == R.drawable.abc_edit_text_material) {
371                         return getColorStateList(context, R.color.abc_tint_edittext);
372                     } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
373                         return getColorStateList(context, R.color.abc_tint_switch_track);
374                     } else if (resId == R.drawable.abc_switch_thumb_material) {
375                         return createSwitchThumbColorStateList(context);
376                     } else if (resId == R.drawable.abc_btn_default_mtrl_shape) {
377                         return createDefaultButtonColorStateList(context);
378                     } else if (resId == R.drawable.abc_btn_borderless_material) {
379                         return createBorderlessButtonColorStateList(context);
380                     } else if (resId == R.drawable.abc_btn_colored_material) {
381                         return createColoredButtonColorStateList(context);
382                     } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
383                             || resId == R.drawable.abc_spinner_textfield_background_material) {
384                         return getColorStateList(context, R.color.abc_tint_spinner);
385                     } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
386                         return getThemeAttrColorStateList(context, R.attr.colorControlNormal);
387                     } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
388                         return getColorStateList(context, R.color.abc_tint_default);
389                     } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) {
390                         return getColorStateList(context, R.color.abc_tint_btn_checkable);
391                     } else if (resId == R.drawable.abc_seekbar_thumb_material) {
392                         return getColorStateList(context, R.color.abc_tint_seek_thumb);
393                     }
394                     return null;
395                 }
396 
397                 @Override
398                 public boolean tintDrawableUsingColorFilter(@NonNull Context context,
399                         int resId, @NonNull Drawable drawable) {
400                     PorterDuff.Mode tintMode = DEFAULT_MODE;
401                     boolean colorAttrSet = false;
402                     int colorAttr = 0;
403                     int alpha = -1;
404 
405                     if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) {
406                         colorAttr = R.attr.colorControlNormal;
407                         colorAttrSet = true;
408                     } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) {
409                         colorAttr = R.attr.colorControlActivated;
410                         colorAttrSet = true;
411                     } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) {
412                         colorAttr = android.R.attr.colorBackground;
413                         colorAttrSet = true;
414                         tintMode = PorterDuff.Mode.MULTIPLY;
415                     } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
416                         colorAttr = android.R.attr.colorForeground;
417                         colorAttrSet = true;
418                         alpha = Math.round(0.16f * 255);
419                     } else if (resId == R.drawable.abc_dialog_material_background) {
420                         colorAttr = android.R.attr.colorBackground;
421                         colorAttrSet = true;
422                     }
423 
424                     if (colorAttrSet) {
425                         drawable = drawable.mutate();
426 
427                         final int color = getThemeAttrColor(context, colorAttr);
428                         drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
429 
430                         if (alpha != -1) {
431                             drawable.setAlpha(alpha);
432                         }
433 
434                         if (DEBUG) {
435                             Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted "
436                                     + context.getResources().getResourceName(resId)
437                                     + " with color: #" + Integer.toHexString(color));
438                         }
439                         return true;
440                     }
441                     return false;
442                 }
443 
444                 @Override
445                 public PorterDuff.Mode getTintModeForDrawableRes(int resId) {
446                     PorterDuff.Mode mode = null;
447 
448                     if (resId == R.drawable.abc_switch_thumb_material) {
449                         mode = PorterDuff.Mode.MULTIPLY;
450                     }
451 
452                     return mode;
453                 }
454             });
455         }
456     }
457 
458     /**
459      * Returns the singleton instance of this class.
460      */
get()461     public static synchronized AppCompatDrawableManager get() {
462         if (INSTANCE == null) {
463             preload();
464         }
465         return INSTANCE;
466     }
467 
468     private ResourceManagerInternal mResourceManager;
469 
getDrawable(@onNull Context context, @DrawableRes int resId)470     public synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
471         return mResourceManager.getDrawable(context, resId);
472     }
473 
getDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown)474     synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
475             boolean failIfNotKnown) {
476         return mResourceManager.getDrawable(context, resId, failIfNotKnown);
477     }
478 
onConfigurationChanged(@onNull Context context)479     public synchronized void onConfigurationChanged(@NonNull Context context) {
480         mResourceManager.onConfigurationChanged(context);
481     }
482 
onDrawableLoadedFromResources(@onNull Context context, @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId)483     synchronized Drawable onDrawableLoadedFromResources(@NonNull Context context,
484             @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) {
485         return mResourceManager.onDrawableLoadedFromResources(context, resources, resId);
486     }
487 
tintDrawableUsingColorFilter(@onNull Context context, @DrawableRes final int resId, @NonNull Drawable drawable)488     boolean tintDrawableUsingColorFilter(@NonNull Context context,
489             @DrawableRes final int resId, @NonNull Drawable drawable) {
490         return mResourceManager.tintDrawableUsingColorFilter(context, resId, drawable);
491     }
492 
getTintList(@onNull Context context, @DrawableRes int resId)493     synchronized ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) {
494         return mResourceManager.getTintList(context, resId);
495     }
496 
tintDrawable(Drawable drawable, TintInfo tint, int[] state)497     static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
498         ResourceManagerInternal.tintDrawable(drawable, tint, state);
499     }
500 
getPorterDuffColorFilter( int color, PorterDuff.Mode mode)501     public static synchronized PorterDuffColorFilter getPorterDuffColorFilter(
502             int color, PorterDuff.Mode mode) {
503         return ResourceManagerInternal.getPorterDuffColorFilter(color, mode);
504     }
505 }
506