/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.customization.picker.theme; import static android.view.View.MeasureSpec.EXACTLY; import static android.view.View.MeasureSpec.makeMeasureSpec; import android.app.WallpaperColors; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.text.format.DateFormat; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.cardview.widget.CardView; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; import com.android.customization.model.theme.ThemeBundle; import com.android.customization.model.theme.ThemeBundle.PreviewInfo; import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon; import com.android.wallpaper.R; import com.android.wallpaper.util.ResourceUtils; import com.android.wallpaper.util.ScreenSizeCalculator; import com.android.wallpaper.util.TimeUtils; import com.android.wallpaper.util.TimeUtils.TimeTicker; import java.util.Calendar; import java.util.List; import java.util.Locale; import java.util.TimeZone; /** A class to load the {@link ThemeBundle} preview to the view. */ class ThemeOptionPreviewer implements LifecycleObserver { private static final String DATE_FORMAT = "EEEE, MMM d"; // Maps which icon from ResourceConstants#ICONS_FOR_PREVIEW. private static final int ICON_WIFI = 0; private static final int ICON_BLUETOOTH = 1; private static final int ICON_FLASHLIGHT = 3; private static final int ICON_AUTO_ROTATE = 4; private static final int ICON_CELLULAR_SIGNAL = 6; private static final int ICON_BATTERY = 7; // Icons in the top bar (fake "status bar") with the particular order. private static final int [] sTopBarIconToPreviewIcon = new int [] { ICON_WIFI, ICON_CELLULAR_SIGNAL, ICON_BATTERY }; // Ids of app icon shape preview. private int[] mShapeAppIconIds = { R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, R.id.shape_preview_icon_2, R.id.shape_preview_icon_3 }; private int[] mShapeIconAppNameIds = { R.id.shape_preview_icon_app_name_0, R.id.shape_preview_icon_app_name_1, R.id.shape_preview_icon_app_name_2, R.id.shape_preview_icon_app_name_3 }; // Ids of color/icons section. private int[][] mColorTileIconIds = { new int[] { R.id.preview_color_qs_0_icon, ICON_WIFI}, new int[] { R.id.preview_color_qs_1_icon, ICON_BLUETOOTH}, new int[] { R.id.preview_color_qs_2_icon, ICON_FLASHLIGHT}, new int[] { R.id.preview_color_qs_3_icon, ICON_AUTO_ROTATE}, }; private int[] mColorTileIds = { R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, R.id.preview_color_qs_2_bg, R.id.preview_color_qs_3_bg }; private int[] mColorButtonIds = { R.id.preview_check_selected, R.id.preview_radio_selected, R.id.preview_toggle_selected }; private final Context mContext; private View mContentView; private TextView mStatusBarClock; private TextView mSmartSpaceDate; private TimeTicker mTicker; private boolean mHasPreviewInfoSet; private boolean mHasWallpaperColorSet; ThemeOptionPreviewer(Lifecycle lifecycle, Context context, ViewGroup previewContainer) { lifecycle.addObserver(this); mContext = context; mContentView = LayoutInflater.from(context).inflate( R.layout.theme_preview_content, /* root= */ null); mContentView.setVisibility(View.INVISIBLE); mStatusBarClock = mContentView.findViewById(R.id.theme_preview_clock); mSmartSpaceDate = mContentView.findViewById(R.id.smart_space_date); updateTime(); final float screenAspectRatio = ScreenSizeCalculator.getInstance().getScreenAspectRatio(mContext); Configuration config = mContext.getResources().getConfiguration(); final boolean directionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; previewContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { // Calculate the full preview card height and width. final int fullPreviewCardHeight = getFullPreviewCardHeight(); final int fullPreviewCardWidth = (int) (fullPreviewCardHeight / screenAspectRatio); // Relayout the content view to match full preview card size. mContentView.measure( makeMeasureSpec(fullPreviewCardWidth, EXACTLY), makeMeasureSpec(fullPreviewCardHeight, EXACTLY)); mContentView.layout(0, 0, fullPreviewCardWidth, fullPreviewCardHeight); // Scale the content view from full preview size to the container size. For full // preview, the scale value is 1. float scale = (float) previewContainer.getMeasuredHeight() / fullPreviewCardHeight; mContentView.setScaleX(scale); mContentView.setScaleY(scale); // The pivot point is centered by default, set to (0, 0). mContentView.setPivotX(directionLTR ? 0f : mContentView.getMeasuredWidth()); mContentView.setPivotY(0f); // Ensure there will be only one content view in the container. previewContainer.removeAllViews(); // Finally, add the content view to the container. previewContainer.addView( mContentView, mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight()); previewContainer.removeOnLayoutChangeListener(this); } }); } /** Loads the Theme option preview into the container view. */ public void setPreviewInfo(PreviewInfo previewInfo) { setHeadlineFont(previewInfo.headlineFontFamily); setBodyFont(previewInfo.bodyFontFamily); setTopBarIcons(previewInfo.icons); setAppIconShape(previewInfo.shapeAppIcons); setColorAndIconsSection(previewInfo.icons, previewInfo.shapeDrawable, previewInfo.resolveAccentColor(mContext.getResources())); setColorAndIconsBoxRadius(previewInfo.bottomSheeetCornerRadius); setQsbRadius(previewInfo.bottomSheeetCornerRadius); mHasPreviewInfoSet = true; showPreviewIfHasAllConfigSet(); } /** * Updates the color of widgets in launcher (like top status bar, smart space, and app name * text) which will change its content color according to different wallpapers. * * @param colors the {@link WallpaperColors} of the wallpaper, or {@code null} to use light * color as default */ public void updateColorForLauncherWidgets(@Nullable WallpaperColors colors) { boolean useLightTextColor = colors == null || (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0; int textColor = mContext.getColor(useLightTextColor ? android.R.color.white : android.R.color.black); int textShadowColor = mContext.getColor(useLightTextColor ? android.R.color.tertiary_text_dark : android.R.color.transparent); // Update the top status bar clock text color. mStatusBarClock.setTextColor(textColor); // Update the top status bar icon color. ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); for (int i = 0; i < iconsContainer.getChildCount(); i++) { ((ImageView) iconsContainer.getChildAt(i)) .setImageTintList(ColorStateList.valueOf(textColor)); } // Update smart space date color. mSmartSpaceDate.setTextColor(textColor); mSmartSpaceDate.setShadowLayer( mContext.getResources().getDimension( R.dimen.smartspace_preview_key_ambient_shadow_blur), /* dx = */ 0, /* dy = */ 0, textShadowColor); // Update shape app icon name text color. for (int id : mShapeIconAppNameIds) { TextView appName = mContentView.findViewById(id); appName.setTextColor(textColor); appName.setShadowLayer( mContext.getResources().getDimension( R.dimen.preview_theme_app_name_key_ambient_shadow_blur), /* dx = */ 0, /* dy = */ 0, textShadowColor); } mHasWallpaperColorSet = true; showPreviewIfHasAllConfigSet(); } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @MainThread public void onResume() { mTicker = TimeTicker.registerNewReceiver(mContext, this::updateTime); updateTime(); } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @MainThread public void onPause() { if (mContext != null) { mContext.unregisterReceiver(mTicker); } } private void showPreviewIfHasAllConfigSet() { if (mHasPreviewInfoSet && mHasWallpaperColorSet && mContentView.getVisibility() != View.VISIBLE) { mContentView.setAlpha(0f); mContentView.setVisibility(View.VISIBLE); mContentView.animate().alpha(1f) .setStartDelay(50) .setDuration(200) .setInterpolator(AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in)) .start(); } } private void setHeadlineFont(Typeface headlineFont) { mStatusBarClock.setTypeface(headlineFont); mSmartSpaceDate.setTypeface(headlineFont); // Update font of color/icons section title. TextView colorIconsSectionTitle = mContentView.findViewById(R.id.color_icons_section_title); colorIconsSectionTitle.setTypeface(headlineFont); } private void setBodyFont(Typeface bodyFont) { // Update font of app names. for (int id : mShapeIconAppNameIds) { TextView appName = mContentView.findViewById(id); appName.setTypeface(bodyFont); } } private void setTopBarIcons(List icons) { ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons); for (int i = 0; i < iconsContainer.getChildCount(); i++) { int iconIndex = sTopBarIconToPreviewIcon[i]; if (iconIndex < icons.size()) { ((ImageView) iconsContainer.getChildAt(i)) .setImageDrawable(icons.get(iconIndex).getConstantState() .newDrawable().mutate()); } else { iconsContainer.getChildAt(i).setVisibility(View.GONE); } } } private void setAppIconShape(List appIcons) { for (int i = 0; i < mShapeAppIconIds.length && i < mShapeIconAppNameIds.length && i < appIcons.size(); i++) { ShapeAppIcon icon = appIcons.get(i); // Set app icon. ImageView iconView = mContentView.findViewById(mShapeAppIconIds[i]); iconView.setBackground(icon.getDrawableCopy()); // Set app name. TextView appName = mContentView.findViewById(mShapeIconAppNameIds[i]); appName.setText(icon.getAppName()); } } private void setColorAndIconsSection(List icons, Drawable shapeDrawable, int accentColor) { // Set QS icons and background. for (int i = 0; i < mColorTileIconIds.length && i < icons.size(); i++) { Drawable icon = icons.get(mColorTileIconIds[i][1]).getConstantState() .newDrawable().mutate(); Drawable bgShape = shapeDrawable.getConstantState().newDrawable(); bgShape.setTint(accentColor); ImageView bg = mContentView.findViewById(mColorTileIds[i]); bg.setImageDrawable(bgShape); ImageView fg = mContentView.findViewById(mColorTileIconIds[i][0]); fg.setImageDrawable(icon); } // Set color for Buttons (CheckBox, RadioButton, and Switch). ColorStateList tintList = getColorStateList(accentColor); for (int mColorButtonId : mColorButtonIds) { CompoundButton button = mContentView.findViewById(mColorButtonId); button.setButtonTintList(tintList); if (button instanceof Switch) { ((Switch) button).setThumbTintList(tintList); ((Switch) button).setTrackTintList(tintList); } } } private void setColorAndIconsBoxRadius(int cornerRadius) { ((CardView) mContentView.findViewById(R.id.color_icons_section)).setRadius(cornerRadius); } private void setQsbRadius(int cornerRadius) { View qsb = mContentView.findViewById(R.id.theme_qsb); if (qsb != null && qsb.getVisibility() == View.VISIBLE) { if (qsb.getBackground() instanceof GradientDrawable) { GradientDrawable bg = (GradientDrawable) qsb.getBackground(); float radius = useRoundedQSB(cornerRadius) ? (float) qsb.getLayoutParams().height / 2 : cornerRadius; bg.setCornerRadii(new float[]{ radius, radius, radius, radius, radius, radius, radius, radius}); } } } private void updateTime() { Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); if (mStatusBarClock != null) { mStatusBarClock.setText(TimeUtils.getFormattedTime(mContext, calendar)); } if (mSmartSpaceDate != null) { String datePattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), DATE_FORMAT); mSmartSpaceDate.setText(DateFormat.format(datePattern, calendar)); } } private boolean useRoundedQSB(int cornerRadius) { return cornerRadius >= mContext.getResources().getDimensionPixelSize( R.dimen.roundCornerThreshold); } private ColorStateList getColorStateList(int accentColor) { int controlGreyColor = ResourceUtils.getColorAttr(mContext, android.R.attr.textColorTertiary); return new ColorStateList( new int[][]{ new int[]{android.R.attr.state_selected}, new int[]{android.R.attr.state_checked}, new int[]{-android.R.attr.state_enabled}, }, new int[] { accentColor, accentColor, controlGreyColor } ); } /** * Gets the screen height which does not include the system status bar and bottom navigation * bar. */ private int getDisplayHeight() { final DisplayMetrics dm = mContext.getResources().getDisplayMetrics(); return dm.heightPixels; } // The height of top tool bar (R.layout.section_header). private int getTopToolBarHeight() { final TypedValue typedValue = new TypedValue(); return mContext.getTheme().resolveAttribute( android.R.attr.actionBarSize, typedValue, true) ? TypedValue.complexToDimensionPixelSize( typedValue.data, mContext.getResources().getDisplayMetrics()) : 0; } private int getFullPreviewCardHeight() { final Resources res = mContext.getResources(); return getDisplayHeight() - getTopToolBarHeight() - res.getDimensionPixelSize(R.dimen.bottom_actions_height) - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_top) - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_bottom); } }