1 /* 2 * Copyright (C) 2021 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 com.google.android.setupcompat.template; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.TypedArray; 23 import android.graphics.Color; 24 import android.graphics.PorterDuff.Mode; 25 import android.graphics.Typeface; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.GradientDrawable; 28 import android.graphics.drawable.InsetDrawable; 29 import android.graphics.drawable.LayerDrawable; 30 import android.graphics.drawable.RippleDrawable; 31 import android.os.Build; 32 import android.os.Build.VERSION_CODES; 33 import android.util.StateSet; 34 import android.util.TypedValue; 35 import android.widget.Button; 36 import androidx.annotation.ColorInt; 37 import androidx.annotation.VisibleForTesting; 38 import com.google.android.setupcompat.R; 39 import com.google.android.setupcompat.internal.FooterButtonPartnerConfig; 40 import com.google.android.setupcompat.internal.Preconditions; 41 import com.google.android.setupcompat.partnerconfig.PartnerConfig; 42 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; 43 44 /** Utils for updating the button style. */ 45 public class FooterButtonStyleUtils { 46 private static final float DEFAULT_DISABLED_ALPHA = 0.26f; 47 48 /** Apply the partner primary button style to given {@code button}. */ applyPrimaryButtonPartnerResource( Context context, Button button, boolean applyDynamicColor)49 public static void applyPrimaryButtonPartnerResource( 50 Context context, Button button, boolean applyDynamicColor) { 51 52 FooterButtonPartnerConfig footerButtonPartnerConfig = 53 new FooterButtonPartnerConfig.Builder(null) 54 .setPartnerTheme(R.style.SucPartnerCustomizationButton_Primary) 55 .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR) 56 .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) 57 .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) 58 .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) 59 .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) 60 .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR) 61 .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) 62 .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) 63 .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) 64 .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) 65 .build(); 66 applyButtonPartnerResources( 67 context, 68 button, 69 applyDynamicColor, 70 /* isButtonIconAtEnd= */ true, 71 footerButtonPartnerConfig); 72 } 73 74 /** Apply the partner secondary button style to given {@code button}. */ applySecondaryButtonPartnerResource( Context context, Button button, boolean applyDynamicColor)75 public static void applySecondaryButtonPartnerResource( 76 Context context, Button button, boolean applyDynamicColor) { 77 78 int defaultTheme = R.style.SucPartnerCustomizationButton_Secondary; 79 int color = 80 PartnerConfigHelper.get(context) 81 .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR); 82 if (color != Color.TRANSPARENT) { 83 defaultTheme = R.style.SucPartnerCustomizationButton_Primary; 84 } 85 // Setup button partner config 86 FooterButtonPartnerConfig footerButtonPartnerConfig = 87 new FooterButtonPartnerConfig.Builder(null) 88 .setPartnerTheme(defaultTheme) 89 .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR) 90 .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) 91 .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) 92 .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) 93 .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) 94 .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) 95 .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) 96 .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) 97 .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) 98 .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) 99 .build(); 100 applyButtonPartnerResources( 101 context, 102 button, 103 applyDynamicColor, 104 /* isButtonIconAtEnd= */ false, 105 footerButtonPartnerConfig); 106 } 107 applyButtonPartnerResources( Context context, Button button, boolean applyDynamicColor, boolean isButtonIconAtEnd, FooterButtonPartnerConfig footerButtonPartnerConfig)108 static void applyButtonPartnerResources( 109 Context context, 110 Button button, 111 boolean applyDynamicColor, 112 boolean isButtonIconAtEnd, 113 FooterButtonPartnerConfig footerButtonPartnerConfig) { 114 115 // If dynamic color enabled, these colors won't be overrode by partner config. 116 // Instead, these colors align with the current theme colors. 117 if (!applyDynamicColor) { 118 // use default disable color util we support the partner disable text color 119 if (button.isEnabled()) { 120 FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig( 121 context, button, footerButtonPartnerConfig.getButtonTextColorConfig()); 122 } 123 FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig( 124 context, 125 button, 126 footerButtonPartnerConfig.getButtonBackgroundConfig(), 127 footerButtonPartnerConfig.getButtonDisableAlphaConfig(), 128 footerButtonPartnerConfig.getButtonDisableBackgroundConfig()); 129 } 130 FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig( 131 context, 132 button, 133 applyDynamicColor, 134 footerButtonPartnerConfig.getButtonTextColorConfig(), 135 footerButtonPartnerConfig.getButtonRippleColorAlphaConfig()); 136 FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig( 137 context, button, footerButtonPartnerConfig.getButtonTextSizeConfig()); 138 FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig( 139 context, button, footerButtonPartnerConfig.getButtonMinHeightConfig()); 140 FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig( 141 context, 142 button, 143 footerButtonPartnerConfig.getButtonTextTypeFaceConfig(), 144 footerButtonPartnerConfig.getButtonTextStyleConfig()); 145 FooterButtonStyleUtils.updateButtonRadiusWithPartnerConfig( 146 context, button, footerButtonPartnerConfig.getButtonRadiusConfig()); 147 FooterButtonStyleUtils.updateButtonIconWithPartnerConfig( 148 context, button, footerButtonPartnerConfig.getButtonIconConfig(), isButtonIconAtEnd); 149 } 150 updateButtonTextEnabledColorWithPartnerConfig( Context context, Button button, PartnerConfig buttonEnableTextColorConfig)151 static void updateButtonTextEnabledColorWithPartnerConfig( 152 Context context, Button button, PartnerConfig buttonEnableTextColorConfig) { 153 @ColorInt 154 int color = PartnerConfigHelper.get(context).getColor(context, buttonEnableTextColorConfig); 155 updateButtonTextEnabledColor(button, color); 156 } 157 updateButtonTextEnabledColor(Button button, @ColorInt int textColor)158 static void updateButtonTextEnabledColor(Button button, @ColorInt int textColor) { 159 if (textColor != Color.TRANSPARENT) { 160 button.setTextColor(ColorStateList.valueOf(textColor)); 161 } 162 } 163 updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor)164 static void updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor) { 165 // TODO : add disable footer button text color partner config 166 167 // disable state will use the default disable state color 168 button.setTextColor(disabledTextColor); 169 } 170 171 @TargetApi(VERSION_CODES.Q) updateButtonBackgroundWithPartnerConfig( Context context, Button button, PartnerConfig buttonBackgroundConfig, PartnerConfig buttonDisableAlphaConfig, PartnerConfig buttonDisableBackgroundConfig)172 static void updateButtonBackgroundWithPartnerConfig( 173 Context context, 174 Button button, 175 PartnerConfig buttonBackgroundConfig, 176 PartnerConfig buttonDisableAlphaConfig, 177 PartnerConfig buttonDisableBackgroundConfig) { 178 Preconditions.checkArgument( 179 Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q, 180 "Update button background only support on sdk Q or higher"); 181 @ColorInt 182 int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig); 183 float disabledAlpha = 184 PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f); 185 @ColorInt 186 int disabledColor = 187 PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig); 188 189 updateButtonBackgroundTintList(context, button, color, disabledAlpha, disabledColor); 190 } 191 192 @TargetApi(VERSION_CODES.Q) updateButtonBackgroundTintList( Context context, Button button, @ColorInt int color, float disabledAlpha, @ColorInt int disabledColor)193 static void updateButtonBackgroundTintList( 194 Context context, 195 Button button, 196 @ColorInt int color, 197 float disabledAlpha, 198 @ColorInt int disabledColor) { 199 int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; 200 int[] ENABLED_STATE_SET = {}; 201 202 if (color != Color.TRANSPARENT) { 203 if (disabledAlpha <= 0f) { 204 // if no partner resource, fallback to theme disable alpha 205 TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha}); 206 float alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA); 207 a.recycle(); 208 disabledAlpha = alpha; 209 } 210 if (disabledColor == Color.TRANSPARENT) { 211 // if no partner resource, fallback to button background color 212 disabledColor = color; 213 } 214 215 // Set text color for ripple. 216 ColorStateList colorStateList = 217 new ColorStateList( 218 new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET}, 219 new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color}); 220 221 // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are 222 // created, but without copying the state from the parent drawable. So even though the 223 // parent is getting the correct drawable state from the view, the children won't get those 224 // states until a state change happens. 225 // As a workaround, we mutate the drawable and forcibly set the state to empty, and then 226 // refresh the state so the children will have the updated states. 227 button.getBackground().mutate().setState(new int[0]); 228 button.refreshDrawableState(); 229 button.setBackgroundTintList(colorStateList); 230 } 231 } 232 233 @TargetApi(VERSION_CODES.Q) updateButtonRippleColorWithPartnerConfig( Context context, Button button, boolean applyDynamicColor, PartnerConfig buttonTextColorConfig, PartnerConfig buttonRippleColorAlphaConfig)234 static void updateButtonRippleColorWithPartnerConfig( 235 Context context, 236 Button button, 237 boolean applyDynamicColor, 238 PartnerConfig buttonTextColorConfig, 239 PartnerConfig buttonRippleColorAlphaConfig) { 240 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 241 242 @ColorInt int textDefaultColor; 243 if (applyDynamicColor) { 244 // Get dynamic text color 245 textDefaultColor = button.getTextColors().getDefaultColor(); 246 } else { 247 // Get partner text color. 248 textDefaultColor = 249 PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig); 250 } 251 float alpha = 252 PartnerConfigHelper.get(context).getFraction(context, buttonRippleColorAlphaConfig); 253 updateButtonRippleColor(button, textDefaultColor, alpha); 254 } 255 } 256 updateButtonRippleColor( Button button, @ColorInt int textColor, float rippleAlpha)257 private static void updateButtonRippleColor( 258 Button button, @ColorInt int textColor, float rippleAlpha) { 259 // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is 260 // unavailable. Since Stencil customization provider only works on Q+, there is no need to 261 // perform any customization for versions 21. 262 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 263 RippleDrawable rippleDrawable = getRippleDrawable(button); 264 if (rippleDrawable == null) { 265 return; 266 } 267 268 int[] pressedState = {android.R.attr.state_pressed}; 269 270 // Set text color for ripple. 271 ColorStateList colorStateList = 272 new ColorStateList( 273 new int[][] {pressedState, StateSet.NOTHING}, 274 new int[] {convertRgbToArgb(textColor, rippleAlpha), Color.TRANSPARENT}); 275 rippleDrawable.setColor(colorStateList); 276 } 277 } 278 updateButtonTextSizeWithPartnerConfig( Context context, Button button, PartnerConfig buttonTextSizeConfig)279 static void updateButtonTextSizeWithPartnerConfig( 280 Context context, Button button, PartnerConfig buttonTextSizeConfig) { 281 float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig); 282 if (size > 0) { 283 button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); 284 } 285 } 286 updateButtonMinHeightWithPartnerConfig( Context context, Button button, PartnerConfig buttonMinHeightConfig)287 static void updateButtonMinHeightWithPartnerConfig( 288 Context context, Button button, PartnerConfig buttonMinHeightConfig) { 289 if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMinHeightConfig)) { 290 float size = PartnerConfigHelper.get(context).getDimension(context, buttonMinHeightConfig); 291 if (size > 0) { 292 button.setMinHeight((int) size); 293 } 294 } 295 } 296 updateButtonTypeFaceWithPartnerConfig( Context context, Button button, PartnerConfig buttonTextTypeFaceConfig, PartnerConfig buttonTextStyleConfig)297 static void updateButtonTypeFaceWithPartnerConfig( 298 Context context, 299 Button button, 300 PartnerConfig buttonTextTypeFaceConfig, 301 PartnerConfig buttonTextStyleConfig) { 302 String fontFamilyName = 303 PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig); 304 305 int textStyleValue = Typeface.NORMAL; 306 if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextStyleConfig)) { 307 textStyleValue = 308 PartnerConfigHelper.get(context) 309 .getInteger(context, buttonTextStyleConfig, Typeface.NORMAL); 310 } 311 Typeface font = Typeface.create(fontFamilyName, textStyleValue); 312 if (font != null) { 313 button.setTypeface(font); 314 } 315 } 316 updateButtonRadiusWithPartnerConfig( Context context, Button button, PartnerConfig buttonRadiusConfig)317 static void updateButtonRadiusWithPartnerConfig( 318 Context context, Button button, PartnerConfig buttonRadiusConfig) { 319 if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { 320 float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig); 321 GradientDrawable gradientDrawable = getGradientDrawable(button); 322 if (gradientDrawable != null) { 323 gradientDrawable.setCornerRadius(radius); 324 } 325 } 326 } 327 updateButtonIconWithPartnerConfig( Context context, Button button, PartnerConfig buttonIconConfig, boolean isButtonIconAtEnd)328 static void updateButtonIconWithPartnerConfig( 329 Context context, Button button, PartnerConfig buttonIconConfig, boolean isButtonIconAtEnd) { 330 if (button == null) { 331 return; 332 } 333 Drawable icon = null; 334 if (buttonIconConfig != null) { 335 icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig); 336 } 337 setButtonIcon(button, icon, isButtonIconAtEnd); 338 } 339 setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd)340 private static void setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd) { 341 if (button == null) { 342 return; 343 } 344 345 if (icon != null) { 346 // TODO: restrict the icons to a reasonable size 347 int h = icon.getIntrinsicHeight(); 348 int w = icon.getIntrinsicWidth(); 349 icon.setBounds(0, 0, w, h); 350 } 351 352 Drawable iconStart = null; 353 Drawable iconEnd = null; 354 if (isButtonIconAtEnd) { 355 iconEnd = icon; 356 } else { 357 iconStart = icon; 358 } 359 if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 360 button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null); 361 } else { 362 button.setCompoundDrawables(iconStart, null, iconEnd, null); 363 } 364 } 365 updateButtonBackground(Button button, @ColorInt int color)366 static void updateButtonBackground(Button button, @ColorInt int color) { 367 button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP); 368 } 369 370 @VisibleForTesting getGradientDrawable(Button button)371 public static GradientDrawable getGradientDrawable(Button button) { 372 // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after 373 // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only 374 // works on Q+, there is no need to perform any customization for versions 21. 375 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 376 Drawable drawable = button.getBackground(); 377 if (drawable instanceof InsetDrawable) { 378 LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable(); 379 return (GradientDrawable) layerDrawable.getDrawable(0); 380 } else if (drawable instanceof RippleDrawable) { 381 if (((RippleDrawable) drawable).getDrawable(0) instanceof GradientDrawable) { 382 return (GradientDrawable) ((RippleDrawable) drawable).getDrawable(0); 383 } 384 InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0); 385 return (GradientDrawable) insetDrawable.getDrawable(); 386 } 387 } 388 return null; 389 } 390 getRippleDrawable(Button button)391 static RippleDrawable getRippleDrawable(Button button) { 392 // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is 393 // unavailable. Since Stencil customization provider only works on Q+, there is no need to 394 // perform any customization for versions 21. 395 if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 396 Drawable drawable = button.getBackground(); 397 if (drawable instanceof InsetDrawable) { 398 return (RippleDrawable) ((InsetDrawable) drawable).getDrawable(); 399 } else if (drawable instanceof RippleDrawable) { 400 return (RippleDrawable) drawable; 401 } 402 } 403 return null; 404 } 405 406 @ColorInt convertRgbToArgb(@olorInt int color, float alpha)407 private static int convertRgbToArgb(@ColorInt int color, float alpha) { 408 return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color)); 409 } 410 FooterButtonStyleUtils()411 private FooterButtonStyleUtils() {} 412 } 413