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 
131     /**
132      * Returns the height of status bar.
133      *
134      * @return height of status bar.
135      */
getStatusBarHeight()136     public int getStatusBarHeight() {
137         return mStatusBarHeight;
138     }
139 
getNavigationBarHeight()140     private int getNavigationBarHeight() {
141         return mNavigationBarHeight;
142     }
143 
getAttributeDimension(int resId)144     private int getAttributeDimension(int resId) {
145         final TypedArray attributes = mView.getContext().getTheme().obtainStyledAttributes(
146                 new int[]{resId});
147         int dimension = attributes.getDimensionPixelSize(0, 0);
148         attributes.recycle();
149         return dimension;
150     }
151 
setViewMargins(int viewId, float marginTop, float marginBottom, boolean separatedTabs)152     private void setViewMargins(int viewId, float marginTop, float marginBottom,
153             boolean separatedTabs) {
154         FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
155                 FrameLayout.LayoutParams.MATCH_PARENT,
156                 separatedTabs ? FrameLayout.LayoutParams.WRAP_CONTENT
157                         : FrameLayout.LayoutParams.MATCH_PARENT);
158 
159         layoutParams.setMargins(0, Math.round(marginTop), 0, Math.round(marginBottom));
160 
161         if (separatedTabs) {
162             layoutParams.gravity = Gravity.BOTTOM;
163         }
164 
165         mView.findViewById(viewId).setLayoutParams(layoutParams);
166     }
167 
168     /** Sets a {@param listener} to listen full screen state changes. */
setFullScreenStatusListener(FullScreenStatusListener listener)169     public void setFullScreenStatusListener(FullScreenStatusListener listener) {
170         mFullScreenStatusListener = listener;
171     }
172 
173     /**
174      * Informs the {@code FullScreenAnimation} object about the window insets of the current
175      * window.
176      *
177      * Called by a {@code View.OnApplyWindowInsetsListener} defined in {@code PreviewFragment}.
178      *
179      * @param windowInsets the window insets of the current window.
180      */
setWindowInsets(WindowInsets windowInsets)181     public void setWindowInsets(WindowInsets windowInsets) {
182         Insets insets = windowInsets.getInsetsIgnoringVisibility(
183                 WindowInsets.Type.systemBars()
184         );
185 
186         mStatusBarHeight = insets.top;
187         mNavigationBarHeight = insets.bottom;
188     }
189 
190     /**
191      * Place UI elements in the correct locations.
192      *
193      * Takes status bar and navigation bar into account.
194      */
placeViews()195     public void placeViews() {
196         setViewMargins(R.id.screen_preview_layout,
197                 getStatusBarHeight() + getAttributeDimension(R.attr.actionBarSize),
198                 getNavigationBarHeight()
199                         + mView.getResources().getDimension(R.dimen.bottom_actions_height)
200                         + mView.getResources().getDimension(R.dimen.separated_tabs_height),
201                 false);
202         setViewMargins(R.id.bottom_action_bar_container,
203                 0,
204                 getNavigationBarHeight(),
205                 false);
206         setViewMargins(R.id.separated_tabs_container,
207                 0,
208                 getNavigationBarHeight()
209                         + mView.getResources().getDimension(R.dimen.bottom_actions_height),
210                 true);
211         ensureToolbarIsCorrectlyLocated();
212     }
213 
214     /**
215      * Ensures that the bottom action bar is in the correct location.
216      *
217      * Called by {@code onBottomActionBarReady}, so that the bottom action bar is correctly located
218      * when it is redrawn.
219      */
ensureBottomActionBarIsCorrectlyLocated()220     public void ensureBottomActionBarIsCorrectlyLocated() {
221         float targetTranslation = mIsFullScreen ? mBottomActionBarTranslation : 0;
222         mView.findViewById(R.id.bottom_actionbar).setTranslationY(targetTranslation);
223     }
224 
225     /**
226      * Ensures that the toolbar is in the correct location.
227      *
228      * Called by {@code placeViews}, {@code ImageWallpaperColorThemePreviewFragment#updateToolBar},
229      * and @{code LiveWallpaperColorThemePreviewFragment#updateToolBar}, so that the toolbar is
230      * correctly located when it is redrawn.
231      */
ensureToolbarIsCorrectlyLocated()232     public void ensureToolbarIsCorrectlyLocated() {
233         AppBarLayout.LayoutParams layoutParams = new AppBarLayout.LayoutParams(
234                 AppBarLayout.LayoutParams.MATCH_PARENT,
235                 AppBarLayout.LayoutParams.MATCH_PARENT);
236 
237         layoutParams.setMargins(0, getStatusBarHeight(), 0, 0);
238 
239         mView.findViewById(R.id.toolbar).setLayoutParams(layoutParams);
240     }
241 
242     /**
243      * Ensures that the text and the navigation button on the toolbar is given the correct color.
244      *
245      * Called by {@code updateToolBar}.
246      */
ensureToolbarIsCorrectlyColored()247     public void ensureToolbarIsCorrectlyColored() {
248         TextView textView = mView.findViewById(R.id.custom_toolbar_title);
249         textView.setTextColor(mCurrentTextColor);
250 
251         Toolbar toolbar = mView.findViewById(R.id.toolbar);
252         // It may be null because there's no back arrow in some cases. For example: no back arrow
253         // for Photos launching case.
254         ImageButton button = (ImageButton) toolbar.getNavigationView();
255         if (button != null) {
256             button.setColorFilter(mCurrentTextColor);
257         }
258     }
259 
260     /**
261      * Sets the text color used for the "Preview" caption in full screen mode.
262      *
263      * @param fullScreenTextColor The desired color for the "Preview" caption in full screen mode.
264      */
setFullScreenTextColor(FullScreenTextColor fullScreenTextColor)265     public void setFullScreenTextColor(FullScreenTextColor fullScreenTextColor) {
266         mFullScreenTextColor = fullScreenTextColor;
267 
268         animateColor(mIsFullScreen);
269     }
270 
271     /**
272      * Sets the visibility of the workspace surface (containing icons from the home screen) and
273      * the elements unique to the lock screen (date and time).
274      *
275      * Called when the "Hide UI Preview" button is clicked.
276      *
277      * @param visible {@code true} if the icons should be shown;
278      *                {@code false} if they should be hidden.
279      */
setWorkspaceVisibility(boolean visible)280     public void setWorkspaceVisibility(boolean visible) {
281         // Not using [setVisibility], because it creates a "jump".
282         if (visible) {
283             mWorkspaceSurface.setClipBounds(new Rect(
284                     0,
285                     Math.round(mWorkspaceHeight * HIDE_ICONS_TOP_RATIO),
286                     mWorkspaceWidth,
287                     mWorkspaceHeight + Math.round(mFullScreenButtonsTranslation / mScale)));
288             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE);
289         } else {
290             mWorkspaceSurface.setClipBounds(new Rect(
291                     mWorkspaceWidth - 1,
292                     mWorkspaceHeight - 1,
293                     mWorkspaceWidth,
294                     mWorkspaceHeight));
295             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE);
296         }
297         if (mIsHomeSelected) {
298             mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE);
299         }
300         mWorkspaceVisibility = visible;
301     }
302 
303     /**
304      * Returns the visibility of the workspace surface (containing icons from the home screen).
305      *
306      * @return the visibility of the workspace surface.
307      */
getWorkspaceVisibility()308     public boolean getWorkspaceVisibility() {
309         return mWorkspaceVisibility;
310     }
311 
animateColor(boolean toFullScreen)312     private void animateColor(boolean toFullScreen) {
313         TextView textView = mView.findViewById(R.id.custom_toolbar_title);
314 
315         int targetColor;
316         if (!toFullScreen || mFullScreenTextColor == FullScreenTextColor.DEFAULT) {
317             targetColor = ResourceUtils.getColorAttr(
318                     mView.getContext(),
319                     android.R.attr.textColorPrimary);
320         } else if (mFullScreenTextColor == FullScreenTextColor.DARK) {
321             targetColor = mView.getContext().getColor(android.R.color.black);
322         } else {
323             targetColor = mView.getContext().getColor(android.R.color.white);
324         }
325 
326         if (targetColor == mCurrentTextColor) {
327             return;
328         }
329 
330         Toolbar toolbar = mView.findViewById(R.id.toolbar);
331         ImageButton button = (ImageButton) toolbar.getNavigationView();
332 
333         ValueAnimator colorAnimator = ValueAnimator.ofArgb(mCurrentTextColor, targetColor);
334         colorAnimator.addUpdateListener(animation -> {
335             int color = (int) animation.getAnimatedValue();
336             textView.setTextColor(color);
337             // It may be null because there's no back arrow in some cases. For example: no back
338             // arrow for Photos launching case.
339             if (button != null) {
340                 button.setColorFilter(color);
341             }
342         });
343         colorAnimator.start();
344 
345         mCurrentTextColor = targetColor;
346     }
347 
348     /**
349      * Animates the layout to or from fullscreen.
350      *
351      * @param toFullScreen {@code true} if animating into the full screen layout;
352      *                     {@code false} if animating out of the full screen layout.
353      */
startAnimation(boolean toFullScreen)354     public void startAnimation(boolean toFullScreen) {
355         // If there is no need to animate, return.
356         if (toFullScreen == mIsFullScreen) {
357             return;
358         }
359 
360         // If the scale is not set, compute the location and size of frame layout.
361         if (!mScaleIsSet) {
362             int[] loc = new int[2];
363             mTouchForwardingLayout.getLocationInWindow(loc);
364 
365             ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance();
366             Point screenSize = screenSizeCalculator.getScreenSize(mView.getDisplay());
367             int screenWidth = screenSize.x;
368             int screenHeight = screenSize.y;
369 
370             mOffsetY = (float) (screenHeight / 2.0
371                     - (loc[1] + mTouchForwardingLayout.getHeight() / 2.0));
372 
373             mScale = Math.max(
374                     screenWidth / (float) mTouchForwardingLayout.getWidth(),
375                     screenHeight / (float) mTouchForwardingLayout.getHeight());
376 
377             mDefaultRadius = ((CardView) mWorkspaceSurface.getParent()).getRadius();
378 
379             mWorkspaceSurface.setEnableSurfaceClipping(true);
380 
381             mWorkspaceWidth = mWorkspaceSurface.getWidth();
382             mWorkspaceHeight = mWorkspaceSurface.getHeight();
383 
384             mBottomActionBarTranslation = getNavigationBarHeight()
385                     + mView.getResources().getDimension(R.dimen.bottom_actions_height)
386                     + mView.getResources().getDimension(R.dimen.separated_tabs_height);
387 
388             mFullScreenButtonsTranslation = -(getNavigationBarHeight()
389                     + mView.getResources().getDimension(
390                             R.dimen.fullscreen_preview_button_margin_bottom)
391                     + mView.getResources().getDimension(R.dimen.separated_tabs_height));
392 
393             mScaleIsSet = true;
394         }
395 
396         // Perform animations.
397 
398         // Rounding animation.
399         // Animated version of ((CardView) mWorkspaceSurface.getParent()).setRadius(0);
400         float fromRadius = toFullScreen ? mDefaultRadius : 0f;
401         float toRadius = toFullScreen ? 0f : mDefaultRadius;
402 
403         ValueAnimator animationRounding = ValueAnimator.ofFloat(fromRadius, toRadius);
404         animationRounding.addUpdateListener(animation -> {
405             ((CardView) mWorkspaceSurface.getParent()).setRadius(
406                     (float) animation.getAnimatedValue());
407         });
408 
409         // Animation to hide some of the home screen icons.
410         float fromTop = toFullScreen ? 0f : HIDE_ICONS_TOP_RATIO;
411         float toTop = toFullScreen ? HIDE_ICONS_TOP_RATIO : 0f;
412         float fromBottom = toFullScreen ? 0 : mFullScreenButtonsTranslation / mScale;
413         float toBottom = toFullScreen ? mFullScreenButtonsTranslation / mScale : 0;
414 
415         ValueAnimator animationHide = ValueAnimator.ofFloat(0f, 1f);
416         animationHide.addUpdateListener(animation -> {
417             float t = (float) animation.getAnimatedValue();
418             float top = fromTop + t * (toTop - fromTop);
419             float bottom = fromBottom + t * (toBottom - fromBottom);
420             mWorkspaceSurface.setClipBounds(new Rect(
421                     0,
422                     Math.round(mWorkspaceHeight * top),
423                     mWorkspaceWidth,
424                     mWorkspaceHeight + Math.round(bottom)));
425         });
426 
427         // Other animations.
428         float scale = toFullScreen ? mScale : 1f;
429         float offsetY = toFullScreen ? mOffsetY : 0f;
430         float bottomActionBarTranslation = toFullScreen ? mBottomActionBarTranslation : 0;
431         float fullScreenButtonsTranslation = toFullScreen ? mFullScreenButtonsTranslation : 0;
432         View frameLayout = mView.findViewById(R.id.screen_preview_layout);
433 
434         AnimatorSet animatorSet = new AnimatorSet();
435         animatorSet.playTogether(
436                 ObjectAnimator.ofFloat(frameLayout, "scaleX", scale),
437                 ObjectAnimator.ofFloat(frameLayout, "scaleY", scale),
438                 ObjectAnimator.ofFloat(frameLayout, "translationY", offsetY),
439                 ObjectAnimator.ofFloat(mView.findViewById(R.id.bottom_actionbar),
440                         "translationY", bottomActionBarTranslation),
441                 ObjectAnimator.ofFloat(mView.findViewById(R.id.separated_tabs_container),
442                         "translationY", bottomActionBarTranslation),
443                 ObjectAnimator.ofFloat(mView.findViewById(R.id.fullscreen_buttons_container),
444                         "translationY", fullScreenButtonsTranslation),
445                 animationRounding,
446                 animationHide
447         );
448         animatorSet.addListener(new Animator.AnimatorListener() {
449             @Override
450             public void onAnimationCancel(Animator animator) {}
451 
452             @Override
453             public void onAnimationEnd(Animator animator) {
454                 if (mFullScreenStatusListener != null) {
455                     mFullScreenStatusListener.onFullScreenStatusChange(toFullScreen);
456                 }
457             }
458 
459             @Override
460             public void onAnimationRepeat(Animator animator) {}
461 
462             @Override
463             public void onAnimationStart(Animator animator) {}
464         });
465         animatorSet.start();
466 
467         animateColor(toFullScreen);
468 
469         // Changes appearances of some elements.
470         mWorkspaceVisibility = true;
471 
472         if (toFullScreen) {
473             ((Button) mView.findViewById(R.id.hide_ui_preview_button)).setText(
474                     R.string.hide_ui_preview_text
475             );
476         }
477 
478         mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE);
479         if (mIsHomeSelected) {
480             mView.findViewById(R.id.lock_screen_preview_container)
481                     .setVisibility(View.INVISIBLE);
482         }
483 
484         mIsFullScreen = toFullScreen;
485     }
486 }
487