• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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