1 /* 2 * Copyright (C) 2019 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 package com.android.customization.model.theme.custom; 17 18 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR; 19 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT; 20 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID; 21 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER; 22 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS; 23 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI; 24 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER; 25 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE; 26 27 import android.content.Context; 28 import android.content.res.ColorStateList; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.content.res.Resources.Theme; 32 import android.content.res.TypedArray; 33 import android.graphics.Path; 34 import android.graphics.Typeface; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.LayerDrawable; 37 import android.graphics.drawable.ShapeDrawable; 38 import android.view.Gravity; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.CompoundButton; 43 import android.widget.ImageView; 44 import android.widget.SeekBar; 45 import android.widget.Switch; 46 import android.widget.TextView; 47 48 import androidx.annotation.ColorInt; 49 import androidx.annotation.Dimension; 50 import androidx.annotation.DrawableRes; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.StringRes; 53 import androidx.core.graphics.ColorUtils; 54 55 import com.android.customization.model.CustomizationManager; 56 import com.android.customization.model.CustomizationOption; 57 import com.android.customization.model.ResourceConstants; 58 import com.android.customization.model.theme.custom.CustomTheme.Builder; 59 import com.android.wallpaper.R; 60 61 import java.util.ArrayList; 62 import java.util.HashMap; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Objects; 66 67 /** 68 * Represents an option of a component of a custom Theme (for example, a possible color, or font, 69 * shape, etc). 70 * Extending classes correspond to each component's options and provide the structure to bind 71 * preview and thumbnails. 72 * // TODO (santie): refactor the logic to bind preview cards to reuse between ThemeFragment and 73 * // here 74 */ 75 public abstract class ThemeComponentOption implements CustomizationOption<ThemeComponentOption> { 76 77 protected final Map<String, String> mOverlayPackageNames = new HashMap<>(); 78 addOverlayPackage(String category, String packageName)79 protected void addOverlayPackage(String category, String packageName) { 80 mOverlayPackageNames.put(category, packageName); 81 } 82 getOverlayPackages()83 public Map<String, String> getOverlayPackages() { 84 return mOverlayPackageNames; 85 } 86 87 @Override getTitle()88 public String getTitle() { 89 return null; 90 } 91 bindPreview(ViewGroup container)92 public abstract void bindPreview(ViewGroup container); 93 buildStep(Builder builder)94 public Builder buildStep(Builder builder) { 95 getOverlayPackages().forEach(builder::addOverlayPackage); 96 return builder; 97 } 98 99 public static class FontOption extends ThemeComponentOption { 100 101 private final String mLabel; 102 private final Typeface mHeadlineFont; 103 private final Typeface mBodyFont; 104 FontOption(String packageName, String label, Typeface headlineFont, Typeface bodyFont)105 public FontOption(String packageName, String label, Typeface headlineFont, 106 Typeface bodyFont) { 107 addOverlayPackage(OVERLAY_CATEGORY_FONT, packageName); 108 mLabel = label; 109 mHeadlineFont = headlineFont; 110 mBodyFont = bodyFont; 111 } 112 113 @Override getTitle()114 public String getTitle() { 115 return null; 116 } 117 118 @Override bindThumbnailTile(View view)119 public void bindThumbnailTile(View view) { 120 ((TextView) view.findViewById(R.id.thumbnail_text)).setTypeface( 121 mHeadlineFont); 122 view.setContentDescription(mLabel); 123 } 124 125 @Override isActive(CustomizationManager<ThemeComponentOption> manager)126 public boolean isActive(CustomizationManager<ThemeComponentOption> manager) { 127 CustomThemeManager customThemeManager = (CustomThemeManager) manager; 128 return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_FONT), 129 customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_FONT)); 130 } 131 132 @Override getLayoutResId()133 public int getLayoutResId() { 134 return R.layout.theme_font_option; 135 } 136 137 @Override bindPreview(ViewGroup container)138 public void bindPreview(ViewGroup container) { 139 bindPreviewHeader(container, R.string.preview_name_font, R.drawable.ic_font); 140 141 ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container); 142 if (cardBody.getChildCount() == 0) { 143 LayoutInflater.from(container.getContext()).inflate( 144 R.layout.preview_card_font_content, 145 cardBody, true); 146 } 147 TextView title = container.findViewById(R.id.font_card_title); 148 title.setTypeface(mHeadlineFont); 149 TextView bodyText = container.findViewById(R.id.font_card_body); 150 bodyText.setTypeface(mBodyFont); 151 container.findViewById(R.id.font_card_divider).setBackgroundColor( 152 title.getCurrentTextColor()); 153 } 154 155 @Override buildStep(Builder builder)156 public Builder buildStep(Builder builder) { 157 builder.setHeadlineFontFamily(mHeadlineFont).setBodyFontFamily(mBodyFont); 158 return super.buildStep(builder); 159 } 160 } 161 bindPreviewHeader(ViewGroup container, @StringRes int headerTextResId, @DrawableRes int headerIcon)162 void bindPreviewHeader(ViewGroup container, @StringRes int headerTextResId, 163 @DrawableRes int headerIcon) { 164 TextView header = container.findViewById(R.id.theme_preview_card_header); 165 header.setText(headerTextResId); 166 167 Context context = container.getContext(); 168 Drawable icon = context.getResources().getDrawable(headerIcon, context.getTheme()); 169 int size = context.getResources().getDimensionPixelSize(R.dimen.card_header_icon_size); 170 icon.setBounds(0, 0, size, size); 171 172 header.setCompoundDrawables(null, icon, null, null); 173 header.setCompoundDrawableTintList(ColorStateList.valueOf( 174 header.getCurrentTextColor())); 175 } 176 177 public static class IconOption extends ThemeComponentOption { 178 179 public static final int THUMBNAIL_ICON_POSITION = 0; 180 private static int[] mIconIds = { 181 R.id.preview_icon_0, R.id.preview_icon_1, R.id.preview_icon_2, R.id.preview_icon_3, 182 R.id.preview_icon_4, R.id.preview_icon_5 183 }; 184 185 private List<Drawable> mIcons = new ArrayList<>(); 186 private String mLabel; 187 188 @Override bindThumbnailTile(View view)189 public void bindThumbnailTile(View view) { 190 Resources res = view.getContext().getResources(); 191 Drawable icon = mIcons.get(THUMBNAIL_ICON_POSITION) 192 .getConstantState().newDrawable().mutate(); 193 icon.setTint(res.getColor(R.color.icon_thumbnail_color, null)); 194 ((ImageView) view.findViewById(R.id.option_icon)).setImageDrawable( 195 icon); 196 view.setContentDescription(mLabel); 197 } 198 199 @Override isActive(CustomizationManager<ThemeComponentOption> manager)200 public boolean isActive(CustomizationManager<ThemeComponentOption> manager) { 201 CustomThemeManager customThemeManager = (CustomThemeManager) manager; 202 Map<String, String> themePackages = customThemeManager.getOverlayPackages(); 203 if (getOverlayPackages().isEmpty()) { 204 return themePackages.get(OVERLAY_CATEGORY_ICON_SYSUI) == null && 205 themePackages.get(OVERLAY_CATEGORY_ICON_SETTINGS) == null && 206 themePackages.get(OVERLAY_CATEGORY_ICON_ANDROID) == null && 207 themePackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER) == null && 208 themePackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER) == null; 209 } 210 for (Map.Entry<String, String> overlayEntry : getOverlayPackages().entrySet()) { 211 if(!Objects.equals(overlayEntry.getValue(), 212 themePackages.get(overlayEntry.getKey()))) { 213 return false; 214 } 215 } 216 return true; 217 } 218 219 @Override getLayoutResId()220 public int getLayoutResId() { 221 return R.layout.theme_icon_option; 222 } 223 224 @Override bindPreview(ViewGroup container)225 public void bindPreview(ViewGroup container) { 226 bindPreviewHeader(container, R.string.preview_name_icon, R.drawable.ic_wifi_24px); 227 228 ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container); 229 if (cardBody.getChildCount() == 0) { 230 LayoutInflater.from(container.getContext()).inflate( 231 R.layout.preview_card_icon_content, cardBody, true); 232 } 233 for (int i = 0; i < mIconIds.length && i < mIcons.size(); i++) { 234 ((ImageView) container.findViewById(mIconIds[i])).setImageDrawable( 235 mIcons.get(i)); 236 } 237 } 238 addIcon(Drawable previewIcon)239 public void addIcon(Drawable previewIcon) { 240 mIcons.add(previewIcon); 241 } 242 243 /** 244 * @return whether this icon option has overlays and previews for all the required packages 245 */ isValid(Context context)246 public boolean isValid(Context context) { 247 return getOverlayPackages().keySet().size() == 248 ResourceConstants.getPackagesToOverlay(context).length; 249 } 250 setLabel(String label)251 public void setLabel(String label) { 252 mLabel = label; 253 } 254 255 @Override buildStep(Builder builder)256 public Builder buildStep(Builder builder) { 257 for (Drawable icon : mIcons) { 258 builder.addIcon(icon); 259 } 260 return super.buildStep(builder); 261 } 262 } 263 264 public static class ColorOption extends ThemeComponentOption { 265 266 /** 267 * Ids of views used to represent quick setting tiles in the color preview screen 268 */ 269 private static int[] COLOR_TILE_IDS = { 270 R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, R.id.preview_color_qs_2_bg 271 }; 272 273 /** 274 * Ids of the views for the foreground of the icon, mapping to the corresponding index of 275 * the actual icon drawable. 276 */ 277 static int[][] COLOR_TILES_ICON_IDS = { 278 new int[]{ R.id.preview_color_qs_0_icon, 0}, 279 new int[]{ R.id.preview_color_qs_1_icon, 1}, 280 new int[] { R.id.preview_color_qs_2_icon, 3} 281 }; 282 283 /** 284 * Ids of views used to represent control buttons in the color preview screen 285 */ 286 private static int[] COLOR_BUTTON_IDS = { 287 R.id.preview_check_selected, R.id.preview_radio_selected, 288 R.id.preview_toggle_selected 289 }; 290 291 @ColorInt private int mColorAccentLight; 292 @ColorInt private int mColorAccentDark; 293 /** 294 * Icons shown as example of QuickSettings tiles in the color preview screen. 295 */ 296 private List<Drawable> mIcons = new ArrayList<>(); 297 298 /** 299 * Drawable with the currently selected shape to be used as background of the sample 300 * QuickSetting icons in the color preview screen. 301 */ 302 private Drawable mShapeDrawable; 303 304 private String mLabel; 305 ColorOption(String packageName, String label, @ColorInt int lightColor, @ColorInt int darkColor)306 ColorOption(String packageName, String label, @ColorInt int lightColor, 307 @ColorInt int darkColor) { 308 addOverlayPackage(OVERLAY_CATEGORY_COLOR, packageName); 309 mLabel = label; 310 mColorAccentLight = lightColor; 311 mColorAccentDark = darkColor; 312 } 313 314 @Override bindThumbnailTile(View view)315 public void bindThumbnailTile(View view) { 316 @ColorInt int color = resolveColor(view.getResources()); 317 ((ImageView) view.findViewById(R.id.option_tile)).setImageTintList( 318 ColorStateList.valueOf(color)); 319 view.setContentDescription(mLabel); 320 } 321 322 @ColorInt resolveColor(Resources res)323 private int resolveColor(Resources res) { 324 Configuration configuration = res.getConfiguration(); 325 return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) 326 == Configuration.UI_MODE_NIGHT_YES ? mColorAccentDark : mColorAccentLight; 327 } 328 329 @Override isActive(CustomizationManager<ThemeComponentOption> manager)330 public boolean isActive(CustomizationManager<ThemeComponentOption> manager) { 331 CustomThemeManager customThemeManager = (CustomThemeManager) manager; 332 return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_COLOR), 333 customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_COLOR)); 334 } 335 336 @Override getLayoutResId()337 public int getLayoutResId() { 338 return R.layout.theme_color_option; 339 } 340 341 @Override bindPreview(ViewGroup container)342 public void bindPreview(ViewGroup container) { 343 bindPreviewHeader(container, R.string.preview_name_color, R.drawable.ic_colorize_24px); 344 345 ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container); 346 if (cardBody.getChildCount() == 0) { 347 LayoutInflater.from(container.getContext()).inflate( 348 R.layout.preview_card_color_content, cardBody, true); 349 } 350 Resources res = container.getResources(); 351 @ColorInt int accentColor = resolveColor(res); 352 @ColorInt int controlGreyColor = res.getColor(R.color.control_grey); 353 ColorStateList tintList = new ColorStateList( 354 new int[][]{ 355 new int[]{android.R.attr.state_selected}, 356 new int[]{android.R.attr.state_checked}, 357 new int[]{-android.R.attr.state_enabled} 358 }, 359 new int[] { 360 accentColor, 361 accentColor, 362 controlGreyColor 363 } 364 ); 365 366 for (int i = 0; i < COLOR_BUTTON_IDS.length; i++) { 367 CompoundButton button = container.findViewById(COLOR_BUTTON_IDS[i]); 368 button.setButtonTintList(tintList); 369 } 370 371 Switch enabledSwitch = container.findViewById(R.id.preview_toggle_selected); 372 enabledSwitch.setThumbTintList(tintList); 373 enabledSwitch.setTrackTintList(tintList); 374 375 ColorStateList seekbarTintList = ColorStateList.valueOf(accentColor); 376 SeekBar seekbar = container.findViewById(R.id.preview_seekbar); 377 seekbar.setThumbTintList(seekbarTintList); 378 seekbar.setProgressTintList(seekbarTintList); 379 seekbar.setProgressBackgroundTintList(seekbarTintList); 380 // Disable seekbar 381 seekbar.setOnTouchListener((view, motionEvent) -> true); 382 383 int iconFgColor = res.getColor(R.color.tile_enabled_icon_color, null); 384 if (!mIcons.isEmpty() && mShapeDrawable != null) { 385 for (int i = 0; i < COLOR_TILE_IDS.length; i++) { 386 Drawable icon = mIcons.get(COLOR_TILES_ICON_IDS[i][1]).getConstantState() 387 .newDrawable(); 388 icon.setTint(iconFgColor); 389 //TODO: load and set the shape. 390 Drawable bgShape = mShapeDrawable.getConstantState().newDrawable(); 391 bgShape.setTint(accentColor); 392 393 ImageView bg = container.findViewById(COLOR_TILE_IDS[i]); 394 bg.setImageDrawable(bgShape); 395 ImageView fg = container.findViewById(COLOR_TILES_ICON_IDS[i][0]); 396 fg.setImageDrawable(icon); 397 } 398 } 399 } 400 setPreviewIcons(List<Drawable> icons)401 public void setPreviewIcons(List<Drawable> icons) { 402 mIcons.addAll(icons); 403 } 404 setShapeDrawable(@ullable Drawable shapeDrawable)405 public void setShapeDrawable(@Nullable Drawable shapeDrawable) { 406 mShapeDrawable = shapeDrawable; 407 } 408 409 @Override buildStep(Builder builder)410 public Builder buildStep(Builder builder) { 411 builder.setColorAccentDark(mColorAccentDark).setColorAccentLight(mColorAccentLight); 412 return super.buildStep(builder); 413 } 414 } 415 416 public static class ShapeOption extends ThemeComponentOption { 417 418 private final LayerDrawable mShape; 419 private final List<Drawable> mAppIcons; 420 private final String mLabel; 421 private final Path mPath; 422 private final int mCornerRadius; 423 private int[] mShapeIconIds = { 424 R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, R.id.shape_preview_icon_2, 425 R.id.shape_preview_icon_3, R.id.shape_preview_icon_4, R.id.shape_preview_icon_5 426 }; 427 ShapeOption(String packageName, String label, Path path, @Dimension int cornerRadius, Drawable shapeDrawable, List<Drawable> appIcons)428 ShapeOption(String packageName, String label, Path path, 429 @Dimension int cornerRadius, Drawable shapeDrawable, 430 List<Drawable> appIcons) { 431 addOverlayPackage(OVERLAY_CATEGORY_SHAPE, packageName); 432 mLabel = label; 433 mAppIcons = appIcons; 434 mPath = path; 435 mCornerRadius = cornerRadius; 436 Drawable background = shapeDrawable.getConstantState().newDrawable(); 437 Drawable foreground = shapeDrawable.getConstantState().newDrawable(); 438 mShape = new LayerDrawable(new Drawable[]{background, foreground}); 439 mShape.setLayerGravity(0, Gravity.CENTER); 440 mShape.setLayerGravity(1, Gravity.CENTER); 441 } 442 443 @Override bindThumbnailTile(View view)444 public void bindThumbnailTile(View view) { 445 ImageView thumb = view.findViewById(R.id.shape_thumbnail); 446 Resources res = view.getResources(); 447 Theme theme = view.getContext().getTheme(); 448 int borderWidth = 2 * res.getDimensionPixelSize(R.dimen.option_border_width); 449 450 Drawable background = mShape.getDrawable(0); 451 background.setTintList(res.getColorStateList(R.color.option_border_color, theme)); 452 453 ShapeDrawable foreground = (ShapeDrawable) mShape.getDrawable(1); 454 455 foreground.setIntrinsicHeight(background.getIntrinsicHeight() - borderWidth); 456 foreground.setIntrinsicWidth(background.getIntrinsicWidth() - borderWidth); 457 TypedArray ta = view.getContext().obtainStyledAttributes( 458 new int[]{android.R.attr.colorPrimary}); 459 int primaryColor = ta.getColor(0, 0); 460 ta.recycle(); 461 int foregroundColor = res.getColor(R.color.shape_option_tile_foreground_color, theme); 462 463 foreground.setTint(ColorUtils.blendARGB(primaryColor, foregroundColor, .05f)); 464 465 thumb.setImageDrawable(mShape); 466 view.setContentDescription(mLabel); 467 } 468 469 @Override isActive(CustomizationManager<ThemeComponentOption> manager)470 public boolean isActive(CustomizationManager<ThemeComponentOption> manager) { 471 CustomThemeManager customThemeManager = (CustomThemeManager) manager; 472 return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE), 473 customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE)); 474 } 475 476 @Override getLayoutResId()477 public int getLayoutResId() { 478 return R.layout.theme_shape_option; 479 } 480 481 @Override bindPreview(ViewGroup container)482 public void bindPreview(ViewGroup container) { 483 bindPreviewHeader(container, R.string.preview_name_shape, R.drawable.ic_shapes_24px); 484 485 ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container); 486 if (cardBody.getChildCount() == 0) { 487 LayoutInflater.from(container.getContext()).inflate( 488 R.layout.preview_card_shape_content, cardBody, true); 489 } 490 for (int i = 0; i < mShapeIconIds.length && i < mAppIcons.size(); i++) { 491 ImageView iconView = cardBody.findViewById(mShapeIconIds[i]); 492 iconView.setBackground(mAppIcons.get(i)); 493 } 494 } 495 496 @Override buildStep(Builder builder)497 public Builder buildStep(Builder builder) { 498 builder.setShapePath(mPath).setBottomSheetCornerRadius(mCornerRadius); 499 for (Drawable appIcon : mAppIcons) { 500 builder.addShapePreviewIcon(appIcon); 501 } 502 return super.buildStep(builder); 503 } 504 } 505 } 506