1 /* 2 * Copyright (C) 2020 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.picker.theme; 17 18 import static android.view.View.MeasureSpec.EXACTLY; 19 import static android.view.View.MeasureSpec.makeMeasureSpec; 20 21 import android.app.WallpaperColors; 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.graphics.Typeface; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.GradientDrawable; 29 import android.text.format.DateFormat; 30 import android.util.DisplayMetrics; 31 import android.util.TypedValue; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.animation.AnimationUtils; 36 import android.widget.CompoundButton; 37 import android.widget.ImageView; 38 import android.widget.Switch; 39 import android.widget.TextView; 40 41 import androidx.annotation.MainThread; 42 import androidx.annotation.Nullable; 43 import androidx.cardview.widget.CardView; 44 import androidx.lifecycle.Lifecycle; 45 import androidx.lifecycle.LifecycleObserver; 46 import androidx.lifecycle.OnLifecycleEvent; 47 48 import com.android.customization.model.theme.ThemeBundle; 49 import com.android.customization.model.theme.ThemeBundle.PreviewInfo; 50 import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; 51 import com.android.wallpaper.R; 52 import com.android.wallpaper.util.ResourceUtils; 53 import com.android.wallpaper.util.ScreenSizeCalculator; 54 import com.android.wallpaper.util.TimeUtils; 55 import com.android.wallpaper.util.TimeUtils.TimeTicker; 56 57 import java.util.Calendar; 58 import java.util.List; 59 import java.util.Locale; 60 import java.util.TimeZone; 61 62 /** A class to load the {@link ThemeBundle} preview to the view. */ 63 class ThemeOptionPreviewer implements LifecycleObserver { 64 private static final String DATE_FORMAT = "EEEE, MMM d"; 65 66 // Maps which icon from ResourceConstants#ICONS_FOR_PREVIEW. 67 private static final int ICON_WIFI = 0; 68 private static final int ICON_BLUETOOTH = 1; 69 private static final int ICON_FLASHLIGHT = 3; 70 private static final int ICON_AUTO_ROTATE = 4; 71 private static final int ICON_CELLULAR_SIGNAL = 6; 72 private static final int ICON_BATTERY = 7; 73 74 // Icons in the top bar (fake "status bar") with the particular order. 75 private static final int [] sTopBarIconToPreviewIcon = new int [] { 76 ICON_WIFI, ICON_CELLULAR_SIGNAL, ICON_BATTERY }; 77 78 // Ids of app icon shape preview. 79 private int[] mShapeAppIconIds = { 80 R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, 81 R.id.shape_preview_icon_2, R.id.shape_preview_icon_3 82 }; 83 private int[] mShapeIconAppNameIds = { 84 R.id.shape_preview_icon_app_name_0, R.id.shape_preview_icon_app_name_1, 85 R.id.shape_preview_icon_app_name_2, R.id.shape_preview_icon_app_name_3 86 }; 87 88 // Ids of color/icons section. 89 private int[][] mColorTileIconIds = { 90 new int[] { R.id.preview_color_qs_0_icon, ICON_WIFI}, 91 new int[] { R.id.preview_color_qs_1_icon, ICON_BLUETOOTH}, 92 new int[] { R.id.preview_color_qs_2_icon, ICON_FLASHLIGHT}, 93 new int[] { R.id.preview_color_qs_3_icon, ICON_AUTO_ROTATE}, 94 }; 95 private int[] mColorTileIds = { 96 R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, 97 R.id.preview_color_qs_2_bg, R.id.preview_color_qs_3_bg 98 }; 99 private int[] mColorButtonIds = { 100 R.id.preview_check_selected, R.id.preview_radio_selected, R.id.preview_toggle_selected 101 }; 102 103 private final Context mContext; 104 105 private View mContentView; 106 private TextView mStatusBarClock; 107 private TextView mSmartSpaceDate; 108 private TimeTicker mTicker; 109 110 private boolean mHasPreviewInfoSet; 111 private boolean mHasWallpaperColorSet; 112 ThemeOptionPreviewer(Lifecycle lifecycle, Context context, ViewGroup previewContainer)113 ThemeOptionPreviewer(Lifecycle lifecycle, Context context, ViewGroup previewContainer) { 114 lifecycle.addObserver(this); 115 116 mContext = context; 117 mContentView = LayoutInflater.from(context).inflate( 118 R.layout.theme_preview_content, /* root= */ null); 119 mContentView.setVisibility(View.INVISIBLE); 120 mStatusBarClock = mContentView.findViewById(R.id.theme_preview_clock); 121 mSmartSpaceDate = mContentView.findViewById(R.id.smart_space_date); 122 updateTime(); 123 final float screenAspectRatio = 124 ScreenSizeCalculator.getInstance().getScreenAspectRatio(mContext); 125 Configuration config = mContext.getResources().getConfiguration(); 126 final boolean directionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 127 previewContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 128 @Override 129 public void onLayoutChange(View view, int left, int top, int right, int bottom, 130 int oldLeft, int oldTop, int oldRight, int oldBottom) { 131 // Calculate the full preview card height and width. 132 final int fullPreviewCardHeight = getFullPreviewCardHeight(); 133 final int fullPreviewCardWidth = (int) (fullPreviewCardHeight / screenAspectRatio); 134 135 // Relayout the content view to match full preview card size. 136 mContentView.measure( 137 makeMeasureSpec(fullPreviewCardWidth, EXACTLY), 138 makeMeasureSpec(fullPreviewCardHeight, EXACTLY)); 139 mContentView.layout(0, 0, fullPreviewCardWidth, fullPreviewCardHeight); 140 141 // Scale the content view from full preview size to the container size. For full 142 // preview, the scale value is 1. 143 float scale = (float) previewContainer.getMeasuredHeight() / fullPreviewCardHeight; 144 mContentView.setScaleX(scale); 145 mContentView.setScaleY(scale); 146 // The pivot point is centered by default, set to (0, 0). 147 mContentView.setPivotX(directionLTR ? 0f : mContentView.getMeasuredWidth()); 148 mContentView.setPivotY(0f); 149 150 // Ensure there will be only one content view in the container. 151 previewContainer.removeAllViews(); 152 // Finally, add the content view to the container. 153 previewContainer.addView( 154 mContentView, 155 mContentView.getMeasuredWidth(), 156 mContentView.getMeasuredHeight()); 157 158 previewContainer.removeOnLayoutChangeListener(this); 159 } 160 }); 161 } 162 163 /** Loads the Theme option preview into the container view. */ setPreviewInfo(PreviewInfo previewInfo)164 public void setPreviewInfo(PreviewInfo previewInfo) { 165 setHeadlineFont(previewInfo.headlineFontFamily); 166 setBodyFont(previewInfo.bodyFontFamily); 167 setTopBarIcons(previewInfo.icons); 168 setAppIconShape(previewInfo.shapeAppIcons); 169 setColorAndIconsSection(previewInfo.icons, previewInfo.shapeDrawable, 170 previewInfo.resolveAccentColor(mContext.getResources())); 171 setColorAndIconsBoxRadius(previewInfo.bottomSheeetCornerRadius); 172 setQsbRadius(previewInfo.bottomSheeetCornerRadius); 173 mHasPreviewInfoSet = true; 174 showPreviewIfHasAllConfigSet(); 175 } 176 177 /** 178 * Updates the color of widgets in launcher (like top status bar, smart space, and app name 179 * text) which will change its content color according to different wallpapers. 180 * 181 * @param colors the {@link WallpaperColors} of the wallpaper, or {@code null} to use light 182 * color as default 183 */ updateColorForLauncherWidgets(@ullable WallpaperColors colors)184 public void updateColorForLauncherWidgets(@Nullable WallpaperColors colors) { 185 boolean useLightTextColor = colors == null 186 || (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0; 187 int textColor = mContext.getColor(useLightTextColor 188 ? android.R.color.white 189 : android.R.color.black); 190 int textShadowColor = mContext.getColor(useLightTextColor 191 ? android.R.color.tertiary_text_dark 192 : android.R.color.transparent); 193 // Update the top status bar clock text color. 194 mStatusBarClock.setTextColor(textColor); 195 // Update the top status bar icon color. 196 ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); 197 for (int i = 0; i < iconsContainer.getChildCount(); i++) { 198 ((ImageView) iconsContainer.getChildAt(i)) 199 .setImageTintList(ColorStateList.valueOf(textColor)); 200 } 201 // Update smart space date color. 202 mSmartSpaceDate.setTextColor(textColor); 203 mSmartSpaceDate.setShadowLayer( 204 mContext.getResources().getDimension( 205 R.dimen.smartspace_preview_key_ambient_shadow_blur), 206 /* dx = */ 0, 207 /* dy = */ 0, 208 textShadowColor); 209 210 // Update shape app icon name text color. 211 for (int id : mShapeIconAppNameIds) { 212 TextView appName = mContentView.findViewById(id); 213 appName.setTextColor(textColor); 214 appName.setShadowLayer( 215 mContext.getResources().getDimension( 216 R.dimen.preview_theme_app_name_key_ambient_shadow_blur), 217 /* dx = */ 0, 218 /* dy = */ 0, 219 textShadowColor); 220 } 221 222 mHasWallpaperColorSet = true; 223 showPreviewIfHasAllConfigSet(); 224 } 225 226 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) 227 @MainThread onResume()228 public void onResume() { 229 mTicker = TimeTicker.registerNewReceiver(mContext, this::updateTime); 230 updateTime(); 231 } 232 233 @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) 234 @MainThread onPause()235 public void onPause() { 236 if (mContext != null) { 237 mContext.unregisterReceiver(mTicker); 238 } 239 } 240 showPreviewIfHasAllConfigSet()241 private void showPreviewIfHasAllConfigSet() { 242 if (mHasPreviewInfoSet && mHasWallpaperColorSet 243 && mContentView.getVisibility() != View.VISIBLE) { 244 mContentView.setAlpha(0f); 245 mContentView.setVisibility(View.VISIBLE); 246 mContentView.animate().alpha(1f) 247 .setStartDelay(50) 248 .setDuration(200) 249 .setInterpolator(AnimationUtils.loadInterpolator(mContext, 250 android.R.interpolator.fast_out_linear_in)) 251 .start(); 252 } 253 } 254 setHeadlineFont(Typeface headlineFont)255 private void setHeadlineFont(Typeface headlineFont) { 256 mStatusBarClock.setTypeface(headlineFont); 257 mSmartSpaceDate.setTypeface(headlineFont); 258 259 // Update font of color/icons section title. 260 TextView colorIconsSectionTitle = mContentView.findViewById(R.id.color_icons_section_title); 261 colorIconsSectionTitle.setTypeface(headlineFont); 262 } 263 setBodyFont(Typeface bodyFont)264 private void setBodyFont(Typeface bodyFont) { 265 // Update font of app names. 266 for (int id : mShapeIconAppNameIds) { 267 TextView appName = mContentView.findViewById(id); 268 appName.setTypeface(bodyFont); 269 } 270 } 271 setTopBarIcons(List<Drawable> icons)272 private void setTopBarIcons(List<Drawable> icons) { 273 ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); 274 for (int i = 0; i < iconsContainer.getChildCount(); i++) { 275 int iconIndex = sTopBarIconToPreviewIcon[i]; 276 if (iconIndex < icons.size()) { 277 ((ImageView) iconsContainer.getChildAt(i)) 278 .setImageDrawable(icons.get(iconIndex).getConstantState() 279 .newDrawable().mutate()); 280 } else { 281 iconsContainer.getChildAt(i).setVisibility(View.GONE); 282 } 283 } 284 } 285 setAppIconShape(List<ShapeAppIcon> appIcons)286 private void setAppIconShape(List<ShapeAppIcon> appIcons) { 287 for (int i = 0; i < mShapeAppIconIds.length && i < mShapeIconAppNameIds.length 288 && i < appIcons.size(); i++) { 289 ShapeAppIcon icon = appIcons.get(i); 290 // Set app icon. 291 ImageView iconView = mContentView.findViewById(mShapeAppIconIds[i]); 292 iconView.setBackground(icon.getDrawableCopy()); 293 // Set app name. 294 TextView appName = mContentView.findViewById(mShapeIconAppNameIds[i]); 295 appName.setText(icon.getAppName()); 296 } 297 } 298 setColorAndIconsSection(List<Drawable> icons, Drawable shapeDrawable, int accentColor)299 private void setColorAndIconsSection(List<Drawable> icons, Drawable shapeDrawable, 300 int accentColor) { 301 // Set QS icons and background. 302 for (int i = 0; i < mColorTileIconIds.length && i < icons.size(); i++) { 303 Drawable icon = icons.get(mColorTileIconIds[i][1]).getConstantState() 304 .newDrawable().mutate(); 305 Drawable bgShape = shapeDrawable.getConstantState().newDrawable(); 306 bgShape.setTint(accentColor); 307 308 ImageView bg = mContentView.findViewById(mColorTileIds[i]); 309 bg.setImageDrawable(bgShape); 310 ImageView fg = mContentView.findViewById(mColorTileIconIds[i][0]); 311 fg.setImageDrawable(icon); 312 } 313 314 // Set color for Buttons (CheckBox, RadioButton, and Switch). 315 ColorStateList tintList = getColorStateList(accentColor); 316 for (int mColorButtonId : mColorButtonIds) { 317 CompoundButton button = mContentView.findViewById(mColorButtonId); 318 button.setButtonTintList(tintList); 319 if (button instanceof Switch) { 320 ((Switch) button).setThumbTintList(tintList); 321 ((Switch) button).setTrackTintList(tintList); 322 } 323 } 324 } 325 setColorAndIconsBoxRadius(int cornerRadius)326 private void setColorAndIconsBoxRadius(int cornerRadius) { 327 ((CardView) mContentView.findViewById(R.id.color_icons_section)).setRadius(cornerRadius); 328 } 329 setQsbRadius(int cornerRadius)330 private void setQsbRadius(int cornerRadius) { 331 View qsb = mContentView.findViewById(R.id.theme_qsb); 332 if (qsb != null && qsb.getVisibility() == View.VISIBLE) { 333 if (qsb.getBackground() instanceof GradientDrawable) { 334 GradientDrawable bg = (GradientDrawable) qsb.getBackground(); 335 float radius = useRoundedQSB(cornerRadius) 336 ? (float) qsb.getLayoutParams().height / 2 : cornerRadius; 337 bg.setCornerRadii(new float[]{ 338 radius, radius, radius, radius, 339 radius, radius, radius, radius}); 340 } 341 } 342 } 343 updateTime()344 private void updateTime() { 345 Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); 346 if (mStatusBarClock != null) { 347 mStatusBarClock.setText(TimeUtils.getFormattedTime(mContext, calendar)); 348 } 349 if (mSmartSpaceDate != null) { 350 String datePattern = 351 DateFormat.getBestDateTimePattern(Locale.getDefault(), DATE_FORMAT); 352 mSmartSpaceDate.setText(DateFormat.format(datePattern, calendar)); 353 } 354 } 355 useRoundedQSB(int cornerRadius)356 private boolean useRoundedQSB(int cornerRadius) { 357 return cornerRadius >= mContext.getResources().getDimensionPixelSize( 358 R.dimen.roundCornerThreshold); 359 } 360 getColorStateList(int accentColor)361 private ColorStateList getColorStateList(int accentColor) { 362 int controlGreyColor = 363 ResourceUtils.getColorAttr(mContext, android.R.attr.textColorTertiary); 364 return new ColorStateList( 365 new int[][]{ 366 new int[]{android.R.attr.state_selected}, 367 new int[]{android.R.attr.state_checked}, 368 new int[]{-android.R.attr.state_enabled}, 369 }, 370 new int[] { 371 accentColor, 372 accentColor, 373 controlGreyColor 374 } 375 ); 376 } 377 378 /** 379 * Gets the screen height which does not include the system status bar and bottom navigation 380 * bar. 381 */ getDisplayHeight()382 private int getDisplayHeight() { 383 final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); 384 return dm.heightPixels; 385 } 386 387 // The height of top tool bar (R.layout.section_header). getTopToolBarHeight()388 private int getTopToolBarHeight() { 389 final TypedValue typedValue = new TypedValue(); 390 return mContext.getTheme().resolveAttribute( 391 android.R.attr.actionBarSize, typedValue, true) 392 ? TypedValue.complexToDimensionPixelSize( 393 typedValue.data, mContext.getResources().getDisplayMetrics()) 394 : 0; 395 } 396 getFullPreviewCardHeight()397 private int getFullPreviewCardHeight() { 398 final Resources res = mContext.getResources(); 399 return getDisplayHeight() 400 - getTopToolBarHeight() 401 - res.getDimensionPixelSize(R.dimen.bottom_actions_height) 402 - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_top) 403 - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_bottom); 404 } 405 } 406