• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package com.android.wallpaper.util;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.content.res.TypedArray;
24 import android.graphics.Insets;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.view.Gravity;
28 import android.view.SurfaceView;
29 import android.view.View;
30 import android.view.WindowInsets;
31 import android.widget.Button;
32 import android.widget.FrameLayout;
33 import android.widget.ImageButton;
34 import android.widget.TextView;
35 import android.widget.Toolbar;
36 
37 import androidx.cardview.widget.CardView;
38 
39 import com.android.wallpaper.R;
40 import com.android.wallpaper.picker.TouchForwardingLayout;
41 
42 import com.google.android.material.appbar.AppBarLayout;
43 
44 /**
45  * A class storing information about a preview fragment's full-screen layout.
46  *
47  * Used for {@code ImagePreviewFragment} and {@code LivePreviewFragment}.
48  */
49 public class FullScreenAnimation {
50 
51     private final View mView;
52     private final TouchForwardingLayout mTouchForwardingLayout;
53     private final SurfaceView mWorkspaceSurface;
54     private boolean mIsFullScreen = false;
55 
56     private boolean mScaleIsSet = false;
57     private boolean mWorkspaceVisibility = true;
58     private float mOffsetY;
59     private float mScale;
60     private float mDefaultRadius;
61     private int mWorkspaceWidth;
62     private int mWorkspaceHeight;
63     private float mBottomActionBarTranslation;
64     private float mFullScreenButtonsTranslation;
65     private int mStatusBarHeight;
66     private int mNavigationBarHeight;
67     private FullScreenStatusListener mFullScreenStatusListener;
68 
69     private static final float HIDE_ICONS_TOP_RATIO = 0.2f;
70 
71     private boolean mIsHomeSelected = true;
72 
73     /**
74      * Options for the full-screen text color.
75      *
76      * {@code DEFAULT} represents the default text color.
77      * {@code DARK} represents a text color that is dark, and should be used when the wallpaper
78      *              supports dark text.
79      * {@code LIGHT} represents a text color that is light, and should be used when the wallpaper
80      *               does not support dark text.
81      */
82     public enum FullScreenTextColor {
83         DEFAULT,
84         DARK,
85         LIGHT
86     }
87 
88     FullScreenTextColor mFullScreenTextColor = FullScreenTextColor.DEFAULT;
89     private int mCurrentTextColor;
90 
91     /** Callback for full screen status. */
92     public interface FullScreenStatusListener {
93         /** Gets called at animation end when full screen status gets changed. */
onFullScreenStatusChange(boolean isFullScreen)94         void onFullScreenStatusChange(boolean isFullScreen);
95     }
96 
97     /**
98      * Constructor.
99      *
100      * @param view The view containing all relevant UI elements. Equal to {@code mRootView}.
101      */
FullScreenAnimation(View view)102     public FullScreenAnimation(View view) {
103         mView = view;
104         mTouchForwardingLayout = view.findViewById(R.id.touch_forwarding_layout);
105         mWorkspaceSurface = view.findViewById(R.id.workspace_surface);
106         mCurrentTextColor = ResourceUtils.getColorAttr(
107                 view.getContext(),
108                 android.R.attr.textColorPrimary);
109     }
110 
111     /**
112      * Returns if the preview layout is currently in full screen.
113      *
114      * @return whether the preview layout is currently in full screen.
115      */
isFullScreen()116     public boolean isFullScreen() {
117         return mIsFullScreen;
118     }
119 
120     /**
121      * Informs this object whether the home tab is selected.
122      *
123      * Used to determine the visibility of {@code lock_screen_preview_container}.
124      *
125      * @param isHomeSelected whether the home tab is selected.
126      */
setIsHomeSelected(boolean isHomeSelected)127     public void setIsHomeSelected(boolean isHomeSelected) {
128         mIsHomeSelected = isHomeSelected;
129     }
130 
getStatusBarHeight()131     private int getStatusBarHeight() {
132         return mStatusBarHeight;
133     }
134 
getNavigationBarHeight()135     private int getNavigationBarHeight() {
136         return mNavigationBarHeight;
137     }
138 
getAttributeDimension(int resId)139     private int getAttributeDimension(int resId) {
140         final TypedArray attributes = mView.getContext().getTheme().obtainStyledAttributes(
141                 new int[]{resId});
142         int dimension = attributes.getDimensionPixelSize(0, 0);
143         attributes.recycle();
144         return dimension;
145     }
146 
setViewMargins(int viewId, float marginTop, float marginBottom, boolean separatedTabs)147     private void setViewMargins(int viewId, float marginTop, float marginBottom,
148             boolean separatedTabs) {
149         FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
150                 FrameLayout.LayoutParams.MATCH_PARENT,
151                 separatedTabs ? FrameLayout.LayoutParams.WRAP_CONTENT
152                         : FrameLayout.LayoutParams.MATCH_PARENT);
153 
154         layoutParams.setMargins(0, Math.round(marginTop), 0, Math.round(marginBottom));
155 
156         if (separatedTabs) {
157             layoutParams.gravity = Gravity.BOTTOM;
158         }
159 
160         mView.findViewById(viewId).setLayoutParams(layoutParams);
161     }
162 
163     /** Sets a {@param listener} to listen full screen state changes. */
setFullScreenStatusListener(FullScreenStatusListener listener)164     public void setFullScreenStatusListener(FullScreenStatusListener listener) {
165         mFullScreenStatusListener = listener;
166     }
167 
168     /**
169      * Informs the {@code FullScreenAnimation} object about the window insets of the current
170      * window.
171      *
172      * Called by a {@code View.OnApplyWindowInsetsListener} defined in {@code PreviewFragment}.
173      *
174      * @param windowInsets the window insets of the current window.
175      */
setWindowInsets(WindowInsets windowInsets)176     public void setWindowInsets(WindowInsets windowInsets) {
177         Insets insets = windowInsets.getInsetsIgnoringVisibility(
178                 WindowInsets.Type.systemBars()
179         );
180 
181         mStatusBarHeight = insets.top;
182         mNavigationBarHeight = insets.bottom;
183     }
184 
185     /**
186      * Place UI elements in the correct locations.
187      *
188      * Takes status bar and navigation bar into account.
189      */
placeViews()190     public void placeViews() {
191         setViewMargins(R.id.screen_preview_layout,
192                 getStatusBarHeight() + getAttributeDimension(R.attr.actionBarSize),
193                 getNavigationBarHeight()
194                         + mView.getResources().getDimension(R.dimen.bottom_actions_height)
195                         + mView.getResources().getDimension(R.dimen.separated_tabs_height),
196                 false);
197         setViewMargins(R.id.bottom_action_bar_container,
198                 0,
199                 getNavigationBarHeight(),
200                 false);
201         setViewMargins(R.id.separated_tabs_container,
202                 0,
203                 getNavigationBarHeight()
204                         + mView.getResources().getDimension(R.dimen.bottom_actions_height),
205                 true);
206         ensureToolbarIsCorrectlyLocated();
207     }
208 
209     /**
210      * Ensures that the bottom action bar is in the correct location.
211      *
212      * Called by {@code onBottomActionBarReady}, so that the bottom action bar is correctly located
213      * when it is redrawn.
214      */
ensureBottomActionBarIsCorrectlyLocated()215     public void ensureBottomActionBarIsCorrectlyLocated() {
216         float targetTranslation = mIsFullScreen ? mBottomActionBarTranslation : 0;
217         mView.findViewById(R.id.bottom_actionbar).setTranslationY(targetTranslation);
218     }
219 
220     /**
221      * Ensures that the toolbar is in the correct location.
222      *
223      * Called by {@code placeViews}, {@code ImageWallpaperColorThemePreviewFragment#updateToolBar},
224      * and @{code LiveWallpaperColorThemePreviewFragment#updateToolBar}, so that the toolbar is
225      * correctly located when it is redrawn.
226      */
ensureToolbarIsCorrectlyLocated()227     public void ensureToolbarIsCorrectlyLocated() {
228         AppBarLayout.LayoutParams layoutParams = new AppBarLayout.LayoutParams(
229                 AppBarLayout.LayoutParams.MATCH_PARENT,
230                 AppBarLayout.LayoutParams.MATCH_PARENT);
231 
232         layoutParams.setMargins(0, getStatusBarHeight(), 0, 0);
233 
234         mView.findViewById(R.id.toolbar).setLayoutParams(layoutParams);
235     }
236 
237     /**
238      * Ensures that the text and the navigation button on the toolbar is given the correct color.
239      *
240      * Called by {@code updateToolBar}.
241      */
ensureToolbarIsCorrectlyColored()242     public void ensureToolbarIsCorrectlyColored() {
243         TextView textView = mView.findViewById(R.id.custom_toolbar_title);
244         textView.setTextColor(mCurrentTextColor);
245 
246         Toolbar toolbar = mView.findViewById(R.id.toolbar);
247         // It may be null because there's no back arrow in some cases. For example: no back arrow
248         // for Photos launching case.
249         ImageButton button = (ImageButton) toolbar.getNavigationView();
250         if (button != null) {
251             button.setColorFilter(mCurrentTextColor);
252         }
253     }
254 
255     /**
256      * Sets the text color used for the "Preview" caption in full screen mode.
257      *
258      * @param fullScreenTextColor The desired color for the "Preview" caption in full screen mode.
259      */
setFullScreenTextColor(FullScreenTextColor fullScreenTextColor)260     public void setFullScreenTextColor(FullScreenTextColor fullScreenTextColor) {
261         mFullScreenTextColor = fullScreenTextColor;
262 
263         animateColor(mIsFullScreen);
264     }
265 
266     /**
267      * Sets the visibility of the workspace surface (containing icons from the home screen) and
268      * the elements unique to the lock screen (date and time).
269      *
270      * Called when the "Hide UI Preview" button is clicked.
271      *
272      * @param visible {@code true} if the icons should be shown;
273      *                {@code false} if they should be hidden.
274      */
setWorkspaceVisibility(boolean visible)275     public void setWorkspaceVisibility(boolean visible) {
276         // Not using [setVisibility], because it creates a "jump".
277         if (visible) {
278             mWorkspaceSurface.setClipBounds(new Rect(
279                     0,
280                     Math.round(mWorkspaceHeight * HIDE_ICONS_TOP_RATIO),
281                     mWorkspaceWidth,
282                     mWorkspaceHeight + Math.round(mFullScreenButtonsTranslation / mScale)));
283             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE);
284         } else {
285             int half = mWorkspaceHeight / 2;
286             mWorkspaceSurface.setClipBounds(new Rect(
287                     0,
288                     half,
289                     mWorkspaceWidth,
290                     half + 1));
291             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE);
292         }
293         if (mIsHomeSelected) {
294             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE);
295         }
296         mWorkspaceVisibility = visible;
297     }
298 
299     /**
300      * Returns the visibility of the workspace surface (containing icons from the home screen).
301      *
302      * @return the visibility of the workspace surface.
303      */
getWorkspaceVisibility()304     public boolean getWorkspaceVisibility() {
305         return mWorkspaceVisibility;
306     }
307 
animateColor(boolean toFullScreen)308     private void animateColor(boolean toFullScreen) {
309         TextView textView = mView.findViewById(R.id.custom_toolbar_title);
310 
311         int targetColor;
312         if (!toFullScreen || mFullScreenTextColor == FullScreenTextColor.DEFAULT) {
313             targetColor = ResourceUtils.getColorAttr(
314                     mView.getContext(),
315                     android.R.attr.textColorPrimary);
316         } else if (mFullScreenTextColor == FullScreenTextColor.DARK) {
317             targetColor = mView.getContext().getColor(android.R.color.black);
318         } else {
319             targetColor = mView.getContext().getColor(android.R.color.white);
320         }
321 
322         if (targetColor == mCurrentTextColor) {
323             return;
324         }
325 
326         Toolbar toolbar = mView.findViewById(R.id.toolbar);
327         ImageButton button = (ImageButton) toolbar.getNavigationView();
328 
329         ValueAnimator colorAnimator = ValueAnimator.ofArgb(mCurrentTextColor, targetColor);
330         colorAnimator.addUpdateListener(animation -> {
331             int color = (int) animation.getAnimatedValue();
332             textView.setTextColor(color);
333             // It may be null because there's no back arrow in some cases. For example: no back
334             // arrow for Photos launching case.
335             if (button != null) {
336                 button.setColorFilter(color);
337             }
338         });
339         colorAnimator.start();
340 
341         mCurrentTextColor = targetColor;
342     }
343 
344     /**
345      * Animates the layout to or from fullscreen.
346      *
347      * @param toFullScreen {@code true} if animating into the full screen layout;
348      *                     {@code false} if animating out of the full screen layout.
349      */
startAnimation(boolean toFullScreen)350     public void startAnimation(boolean toFullScreen) {
351         // If there is no need to animate, return.
352         if (toFullScreen == mIsFullScreen) {
353             return;
354         }
355 
356         // If the scale is not set, compute the location and size of frame layout.
357         if (!mScaleIsSet) {
358             int[] loc = new int[2];
359             mTouchForwardingLayout.getLocationInWindow(loc);
360 
361             ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance();
362             Point screenSize = screenSizeCalculator.getScreenSize(mView.getDisplay());
363             int screenWidth = screenSize.x;
364             int screenHeight = screenSize.y;
365 
366             mOffsetY = (float) (screenHeight / 2.0
367                     - (loc[1] + mTouchForwardingLayout.getHeight() / 2.0));
368 
369             mScale = Math.max(
370                     screenWidth / (float) mTouchForwardingLayout.getWidth(),
371                     screenHeight / (float) mTouchForwardingLayout.getHeight());
372 
373             mDefaultRadius = ((CardView) mWorkspaceSurface.getParent()).getRadius();
374 
375             mWorkspaceSurface.setEnableSurfaceClipping(true);
376 
377             mWorkspaceWidth = mWorkspaceSurface.getWidth();
378             mWorkspaceHeight = mWorkspaceSurface.getHeight();
379 
380             mBottomActionBarTranslation = getNavigationBarHeight()
381                     + mView.getResources().getDimension(R.dimen.bottom_actions_height)
382                     + mView.getResources().getDimension(R.dimen.separated_tabs_height);
383 
384             mFullScreenButtonsTranslation = -(getNavigationBarHeight()
385                     + mView.getResources().getDimension(
386                             R.dimen.fullscreen_preview_button_margin_bottom)
387                     + mView.getResources().getDimension(R.dimen.separated_tabs_height));
388 
389             mScaleIsSet = true;
390         }
391 
392         // Perform animations.
393 
394         // Rounding animation.
395         // Animated version of ((CardView) mWorkspaceSurface.getParent()).setRadius(0);
396         float fromRadius = toFullScreen ? mDefaultRadius : 0f;
397         float toRadius = toFullScreen ? 0f : mDefaultRadius;
398 
399         ValueAnimator animationRounding = ValueAnimator.ofFloat(fromRadius, toRadius);
400         animationRounding.addUpdateListener(animation -> {
401             ((CardView) mWorkspaceSurface.getParent()).setRadius(
402                     (float) animation.getAnimatedValue());
403         });
404 
405         // Animation to hide some of the home screen icons.
406         float fromTop = toFullScreen ? 0f : HIDE_ICONS_TOP_RATIO;
407         float toTop = toFullScreen ? HIDE_ICONS_TOP_RATIO : 0f;
408         float fromBottom = toFullScreen ? 0 : mFullScreenButtonsTranslation / mScale;
409         float toBottom = toFullScreen ? mFullScreenButtonsTranslation / mScale : 0;
410 
411         ValueAnimator animationHide = ValueAnimator.ofFloat(0f, 1f);
412         animationHide.addUpdateListener(animation -> {
413             float t = (float) animation.getAnimatedValue();
414             float top = fromTop + t * (toTop - fromTop);
415             float bottom = fromBottom + t * (toBottom - fromBottom);
416             mWorkspaceSurface.setClipBounds(new Rect(
417                     0,
418                     Math.round(mWorkspaceHeight * top),
419                     mWorkspaceWidth,
420                     mWorkspaceHeight + Math.round(bottom)));
421         });
422 
423         // Other animations.
424         float scale = toFullScreen ? mScale : 1f;
425         float offsetY = toFullScreen ? mOffsetY : 0f;
426         float bottomActionBarTranslation = toFullScreen ? mBottomActionBarTranslation : 0;
427         float fullScreenButtonsTranslation = toFullScreen ? mFullScreenButtonsTranslation : 0;
428         View frameLayout = mView.findViewById(R.id.screen_preview_layout);
429 
430         AnimatorSet animatorSet = new AnimatorSet();
431         animatorSet.playTogether(
432                 ObjectAnimator.ofFloat(frameLayout, "scaleX", scale),
433                 ObjectAnimator.ofFloat(frameLayout, "scaleY", scale),
434                 ObjectAnimator.ofFloat(frameLayout, "translationY", offsetY),
435                 ObjectAnimator.ofFloat(mView.findViewById(R.id.bottom_actionbar),
436                         "translationY", bottomActionBarTranslation),
437                 ObjectAnimator.ofFloat(mView.findViewById(R.id.separated_tabs_container),
438                         "translationY", bottomActionBarTranslation),
439                 ObjectAnimator.ofFloat(mView.findViewById(R.id.fullscreen_buttons_container),
440                         "translationY", fullScreenButtonsTranslation),
441                 animationRounding,
442                 animationHide
443         );
444         animatorSet.addListener(new Animator.AnimatorListener() {
445             @Override
446             public void onAnimationCancel(Animator animator) {}
447 
448             @Override
449             public void onAnimationEnd(Animator animator) {
450                 if (mFullScreenStatusListener != null) {
451                     mFullScreenStatusListener.onFullScreenStatusChange(toFullScreen);
452                 }
453             }
454 
455             @Override
456             public void onAnimationRepeat(Animator animator) {}
457 
458             @Override
459             public void onAnimationStart(Animator animator) {}
460         });
461         animatorSet.start();
462 
463         animateColor(toFullScreen);
464 
465         // Changes appearances of some elements.
466         mWorkspaceVisibility = true;
467 
468         if (toFullScreen) {
469             ((Button) mView.findViewById(R.id.hide_ui_preview_button)).setText(
470                     R.string.hide_ui_preview_text
471             );
472         }
473 
474         mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE);
475         if (mIsHomeSelected) {
476             mView.findViewById(R.id.lock_screen_preview_container)
477                     .setVisibility(View.INVISIBLE);
478         }
479 
480         mIsFullScreen = toFullScreen;
481     }
482 }
483