| /* |
| * 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.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 |
| ? R.color.text_color_light |
| : R.color.text_color_dark); |
| int textShadowColor = mContext.getColor(useLightTextColor |
| ? R.color.smartspace_preview_shadow_color_dark |
| : R.color.smartspace_preview_shadow_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<Drawable> 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<ShapeAppIcon> 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<Drawable> 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 = mContext.getColor(R.color.control_grey); |
| 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_navbar_height) |
| - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_top) |
| - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_bottom); |
| } |
| } |