| /* |
| * Copyright (C) 2021 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.wallpaper.util; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.content.res.TypedArray; |
| import android.graphics.Insets; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.view.Gravity; |
| import android.view.SurfaceView; |
| import android.view.View; |
| import android.view.WindowInsets; |
| import android.widget.Button; |
| import android.widget.FrameLayout; |
| import android.widget.ImageButton; |
| import android.widget.TextView; |
| import android.widget.Toolbar; |
| |
| import androidx.cardview.widget.CardView; |
| |
| import com.android.wallpaper.R; |
| import com.android.wallpaper.picker.TouchForwardingLayout; |
| |
| /** |
| * A class storing information about a preview fragment's full-screen layout. |
| * |
| * Used for {@code ImagePreviewFragment} and {@code LivePreviewFragment}. |
| */ |
| public class FullScreenAnimation { |
| |
| private final View mView; |
| private final TouchForwardingLayout mTouchForwardingLayout; |
| private final SurfaceView mWorkspaceSurface; |
| private boolean mIsFullScreen = false; |
| private boolean mShowInFullScreen = false; |
| |
| private boolean mScaleIsSet = false; |
| private boolean mWorkspaceVisibility = true; |
| private float mOffsetY; |
| private float mScale; |
| private float mDefaultRadius; |
| private int mWorkspaceWidth; |
| private int mWorkspaceHeight; |
| private float mBottomActionBarTranslation; |
| private float mFullScreenButtonsTranslation; |
| private int mStatusBarHeight; |
| private int mNavigationBarHeight; |
| private FullScreenStatusListener mFullScreenStatusListener; |
| |
| private static final float HIDE_ICONS_TOP_RATIO = 0.2f; |
| |
| private boolean mIsHomeSelected = true; |
| |
| /** |
| * Options for the full-screen text color. |
| * |
| * {@code DEFAULT} represents the default text color. |
| * {@code DARK} represents a text color that is dark, and should be used when the wallpaper |
| * supports dark text. |
| * {@code LIGHT} represents a text color that is light, and should be used when the wallpaper |
| * does not support dark text. |
| */ |
| public enum FullScreenTextColor { |
| DEFAULT, |
| DARK, |
| LIGHT |
| } |
| |
| FullScreenTextColor mFullScreenTextColor = FullScreenTextColor.DEFAULT; |
| private int mCurrentTextColor; |
| |
| /** Callback for full screen status. */ |
| public interface FullScreenStatusListener { |
| /** Gets called at animation end when full screen status gets changed. */ |
| void onFullScreenStatusChange(boolean isFullScreen); |
| } |
| |
| /** |
| * Constructor. |
| * |
| * @param view The view containing all relevant UI elements. Equal to {@code mRootView}. |
| */ |
| public FullScreenAnimation(View view) { |
| mView = view; |
| mTouchForwardingLayout = view.findViewById(R.id.touch_forwarding_layout); |
| mWorkspaceSurface = view.findViewById(R.id.workspace_surface); |
| mCurrentTextColor = ResourceUtils.getColorAttr( |
| view.getContext(), |
| android.R.attr.textColorPrimary); |
| } |
| |
| /** |
| * Returns if the preview layout is currently in full screen. |
| * |
| * @return whether the preview layout is currently in full screen. |
| */ |
| public boolean isFullScreen() { |
| return mIsFullScreen; |
| } |
| |
| /** |
| * Informs this object whether the home tab is selected. |
| * |
| * Used to determine the visibility of {@code lock_screen_preview_container}. |
| * |
| * @param isHomeSelected whether the home tab is selected. |
| */ |
| public void setIsHomeSelected(boolean isHomeSelected) { |
| mIsHomeSelected = isHomeSelected; |
| } |
| |
| /** |
| * Informs this object whether the full screen is separate activity. |
| * |
| * Used to determine the height of workspace. |
| * |
| * @param isShowInFullScreen whether the full screen is separate activity. |
| */ |
| public void setShowInFullScreen(boolean isShowInFullScreen) { |
| mShowInFullScreen = isShowInFullScreen; |
| } |
| |
| /** |
| * Returns the height of status bar. |
| * |
| * @return height of status bar. |
| */ |
| public int getStatusBarHeight() { |
| return mStatusBarHeight; |
| } |
| |
| private int getNavigationBarHeight() { |
| return mNavigationBarHeight; |
| } |
| |
| private int getAttributeDimension(int resId) { |
| final TypedArray attributes = mView.getContext().getTheme().obtainStyledAttributes( |
| new int[]{resId}); |
| int dimension = attributes.getDimensionPixelSize(0, 0); |
| attributes.recycle(); |
| return dimension; |
| } |
| |
| private void setViewMargins(int viewId, float marginTop, float marginBottom, |
| boolean separatedTabs) { |
| FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( |
| FrameLayout.LayoutParams.MATCH_PARENT, |
| separatedTabs ? FrameLayout.LayoutParams.WRAP_CONTENT |
| : FrameLayout.LayoutParams.MATCH_PARENT); |
| |
| layoutParams.setMargins(0, Math.round(marginTop), 0, Math.round(marginBottom)); |
| |
| if (separatedTabs) { |
| layoutParams.gravity = Gravity.BOTTOM; |
| } |
| |
| mView.findViewById(viewId).setLayoutParams(layoutParams); |
| } |
| |
| /** Sets a {@param listener} to listen full screen state changes. */ |
| public void setFullScreenStatusListener(FullScreenStatusListener listener) { |
| mFullScreenStatusListener = listener; |
| } |
| |
| /** |
| * Informs the {@code FullScreenAnimation} object about the window insets of the current |
| * window. |
| * |
| * Called by a {@code View.OnApplyWindowInsetsListener} defined in {@code PreviewFragment}. |
| * |
| * @param windowInsets the window insets of the current window. |
| */ |
| public void setWindowInsets(WindowInsets windowInsets) { |
| Insets insets = windowInsets.getInsetsIgnoringVisibility( |
| WindowInsets.Type.systemBars() |
| ); |
| |
| mStatusBarHeight = insets.top; |
| mNavigationBarHeight = insets.bottom; |
| } |
| |
| /** |
| * Place UI elements in the correct locations. |
| * |
| * Takes status bar and navigation bar into account. |
| * @param view view is used to show preview fragment. |
| */ |
| public void placeViews(View view) { |
| // If is already full screen we do not do anything here. |
| if (mIsFullScreen) { |
| return; |
| } |
| if (mShowInFullScreen) { |
| View container = view.findViewById(R.id.container); |
| container.setPadding(0, 0, 0, 0); |
| setViewMargins(R.id.screen_preview_layout, 0, 0, false); |
| } else { |
| setViewMargins(R.id.screen_preview_layout, |
| (float) getStatusBarHeight() + mView.findViewById( |
| R.id.preview_header).getPaddingBottom(), |
| getNavigationBarHeight() |
| + mView.getResources().getDimension(R.dimen.bottom_actions_height) |
| + mView.getResources().getDimension(R.dimen.separated_tabs_height), |
| false); |
| } |
| setViewMargins(R.id.bottom_action_bar_container, |
| 0, |
| getNavigationBarHeight(), |
| false); |
| setViewMargins(R.id.separated_tabs_container, |
| 0, |
| getNavigationBarHeight() |
| + mView.getResources().getDimension(R.dimen.bottom_actions_height), |
| true); |
| ensureToolbarIsCorrectlyLocated(); |
| } |
| |
| /** |
| * Ensures that the bottom action bar is in the correct location. |
| * |
| * Called by {@code onBottomActionBarReady}, so that the bottom action bar is correctly located |
| * when it is redrawn. |
| */ |
| public void ensureBottomActionBarIsCorrectlyLocated() { |
| float targetTranslation = mIsFullScreen ? mBottomActionBarTranslation : 0; |
| mView.findViewById(R.id.bottom_actionbar).setTranslationY(targetTranslation); |
| } |
| |
| /** |
| * Ensures that the toolbar is in the correct location. |
| * |
| * Called by {@code placeViews}, {@code ImageWallpaperColorThemePreviewFragment#updateToolBar}, |
| * and @{code LiveWallpaperColorThemePreviewFragment#updateToolBar}, so that the toolbar is |
| * correctly located when it is redrawn. |
| */ |
| public void ensureToolbarIsCorrectlyLocated() { |
| FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( |
| FrameLayout.LayoutParams.MATCH_PARENT, |
| FrameLayout.LayoutParams.MATCH_PARENT); |
| |
| layoutParams.setMargins(0, getStatusBarHeight(), 0, 0); |
| mView.findViewById(R.id.section_header_container).setLayoutParams(layoutParams); |
| } |
| |
| /** |
| * Ensures that the text and the navigation button on the toolbar is given the correct color. |
| * |
| * Called by {@code updateToolBar}. |
| */ |
| public void ensureToolbarIsCorrectlyColored() { |
| TextView textView = mView.findViewById(R.id.custom_toolbar_title); |
| if (textView != null) { |
| textView.setTextColor(mCurrentTextColor); |
| } |
| |
| Toolbar toolbar = mView.findViewById(R.id.toolbar); |
| // It may be null because there's no back arrow in some cases. For example: no back arrow |
| // for Photos launching case. |
| ImageButton button = (ImageButton) toolbar.getNavigationView(); |
| if (button != null) { |
| button.setColorFilter(mCurrentTextColor); |
| } |
| } |
| |
| /** |
| * Sets the text color used for the "Preview" caption in full screen mode. |
| * |
| * @param fullScreenTextColor The desired color for the "Preview" caption in full screen mode. |
| */ |
| public void setFullScreenTextColor(FullScreenTextColor fullScreenTextColor) { |
| mFullScreenTextColor = fullScreenTextColor; |
| |
| animateColor(mIsFullScreen); |
| } |
| |
| /** |
| * Sets the visibility of the workspace surface (containing icons from the home screen) and |
| * the elements unique to the lock screen (date and time). |
| * |
| * Called when the "Hide UI Preview" button is clicked. |
| * |
| * @param visible {@code true} if the icons should be shown; |
| * {@code false} if they should be hidden. |
| */ |
| public void setWorkspaceVisibility(boolean visible) { |
| // Not using [setVisibility], because it creates a "jump". |
| if (visible) { |
| mWorkspaceSurface.setClipBounds(new Rect( |
| 0, |
| Math.round(mWorkspaceHeight * HIDE_ICONS_TOP_RATIO), |
| mWorkspaceWidth, |
| mShowInFullScreen ? mWorkspaceHeight : mWorkspaceHeight + Math.round( |
| mFullScreenButtonsTranslation / mScale))); |
| mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE); |
| } else { |
| mWorkspaceSurface.setClipBounds(new Rect( |
| mWorkspaceWidth - 1, |
| mWorkspaceHeight - 1, |
| mWorkspaceWidth, |
| mWorkspaceHeight)); |
| mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE); |
| } |
| if (mIsHomeSelected) { |
| mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.INVISIBLE); |
| } |
| mWorkspaceVisibility = visible; |
| } |
| |
| /** |
| * Returns the visibility of the workspace surface (containing icons from the home screen). |
| * |
| * @return the visibility of the workspace surface. |
| */ |
| public boolean getWorkspaceVisibility() { |
| return mWorkspaceVisibility; |
| } |
| |
| private void animateColor(boolean toFullScreen) { |
| TextView textView = mView.findViewById(R.id.custom_toolbar_title); |
| |
| int targetColor; |
| if (!toFullScreen || mFullScreenTextColor == FullScreenTextColor.DEFAULT) { |
| targetColor = ResourceUtils.getColorAttr( |
| mView.getContext(), |
| android.R.attr.textColorPrimary); |
| } else if (mFullScreenTextColor == FullScreenTextColor.DARK) { |
| targetColor = mView.getContext().getColor(android.R.color.black); |
| } else { |
| targetColor = mView.getContext().getColor(android.R.color.white); |
| } |
| |
| if (targetColor == mCurrentTextColor) { |
| return; |
| } |
| |
| Toolbar toolbar = mView.findViewById(R.id.toolbar); |
| ImageButton button = (ImageButton) toolbar.getNavigationView(); |
| |
| ValueAnimator colorAnimator = ValueAnimator.ofArgb(mCurrentTextColor, targetColor); |
| colorAnimator.addUpdateListener(animation -> { |
| int color = (int) animation.getAnimatedValue(); |
| if (textView != null) { |
| textView.setTextColor(color); |
| } |
| // It may be null because there's no back arrow in some cases. For example: no back |
| // arrow for Photos launching case. |
| if (button != null) { |
| button.setColorFilter(color); |
| } |
| }); |
| colorAnimator.start(); |
| |
| mCurrentTextColor = targetColor; |
| } |
| |
| /** |
| * Animates the layout to or from fullscreen. |
| * |
| * @param toFullScreen {@code true} if animating into the full screen layout; |
| * {@code false} if animating out of the full screen layout. |
| */ |
| public void startAnimation(boolean toFullScreen) { |
| // If there is no need to animate, return. |
| if (toFullScreen == mIsFullScreen) { |
| return; |
| } |
| |
| // If the scale is not set, compute the location and size of frame layout. |
| if (!mScaleIsSet) { |
| int[] loc = new int[2]; |
| mTouchForwardingLayout.getLocationInWindow(loc); |
| |
| ScreenSizeCalculator screenSizeCalculator = ScreenSizeCalculator.getInstance(); |
| Point screenSize = screenSizeCalculator.getScreenSize(mView.getDisplay()); |
| int screenWidth = screenSize.x; |
| int screenHeight = screenSize.y; |
| |
| mOffsetY = (float) (screenHeight / 2.0 |
| - (loc[1] + mTouchForwardingLayout.getHeight() / 2.0)); |
| |
| mScale = Math.max( |
| screenWidth / (float) mTouchForwardingLayout.getWidth(), |
| screenHeight / (float) mTouchForwardingLayout.getHeight()); |
| |
| mDefaultRadius = ((CardView) mWorkspaceSurface.getParent()).getRadius(); |
| |
| mWorkspaceSurface.setEnableSurfaceClipping(true); |
| |
| mWorkspaceWidth = mWorkspaceSurface.getWidth(); |
| mWorkspaceHeight = mWorkspaceSurface.getHeight(); |
| |
| mBottomActionBarTranslation = getNavigationBarHeight() |
| + mView.getResources().getDimension(R.dimen.bottom_actions_height) |
| + mView.getResources().getDimension(R.dimen.separated_tabs_height); |
| |
| mFullScreenButtonsTranslation = -(getNavigationBarHeight() |
| + mView.getResources().getDimension( |
| R.dimen.fullscreen_preview_button_margin_bottom) |
| + mView.getResources().getDimension(R.dimen.separated_tabs_height)); |
| |
| mScaleIsSet = true; |
| } |
| |
| // Perform animations. |
| |
| // Rounding animation. |
| // Animated version of ((CardView) mWorkspaceSurface.getParent()).setRadius(0); |
| float fromRadius = toFullScreen ? mDefaultRadius : 0f; |
| float toRadius = toFullScreen ? 0f : mDefaultRadius; |
| |
| ValueAnimator animationRounding = ValueAnimator.ofFloat(fromRadius, toRadius); |
| animationRounding.addUpdateListener(animation -> { |
| ((CardView) mWorkspaceSurface.getParent()).setRadius( |
| (float) animation.getAnimatedValue()); |
| }); |
| |
| // Animation to hide some of the home screen icons. |
| float fromTop = toFullScreen ? 0f : HIDE_ICONS_TOP_RATIO; |
| float toTop = toFullScreen ? HIDE_ICONS_TOP_RATIO : 0f; |
| float fromBottom = toFullScreen ? 0 : mFullScreenButtonsTranslation / mScale; |
| float toBottom = toFullScreen ? mFullScreenButtonsTranslation / mScale : 0; |
| |
| ValueAnimator animationHide = ValueAnimator.ofFloat(0f, 1f); |
| animationHide.addUpdateListener(animation -> { |
| float t = (float) animation.getAnimatedValue(); |
| float top = fromTop + t * (toTop - fromTop); |
| float bottom = fromBottom + t * (toBottom - fromBottom); |
| mWorkspaceSurface.setClipBounds(new Rect( |
| 0, |
| Math.round(mWorkspaceHeight * top), |
| mWorkspaceWidth, |
| mShowInFullScreen ? mWorkspaceHeight : mWorkspaceHeight + Math.round(bottom))); |
| }); |
| |
| // Other animations. |
| float scale = toFullScreen ? mScale : 1f; |
| float offsetY = toFullScreen ? mOffsetY : 0f; |
| float bottomActionBarTranslation = toFullScreen ? mBottomActionBarTranslation : 0; |
| float fullScreenButtonsTranslation = toFullScreen ? mFullScreenButtonsTranslation : 0; |
| View frameLayout = mView.findViewById(R.id.screen_preview_layout); |
| |
| AnimatorSet animatorSet = new AnimatorSet(); |
| animatorSet.playTogether( |
| ObjectAnimator.ofFloat(frameLayout, "scaleX", scale), |
| ObjectAnimator.ofFloat(frameLayout, "scaleY", scale), |
| ObjectAnimator.ofFloat(frameLayout, "translationY", offsetY), |
| ObjectAnimator.ofFloat(mView.findViewById(R.id.bottom_actionbar), |
| "translationY", bottomActionBarTranslation), |
| ObjectAnimator.ofFloat(mView.findViewById(R.id.separated_tabs_container), |
| "translationY", bottomActionBarTranslation), |
| ObjectAnimator.ofFloat(mView.findViewById(R.id.fullscreen_buttons_container), |
| "translationY", fullScreenButtonsTranslation), |
| animationRounding, |
| animationHide |
| ); |
| animatorSet.addListener(new Animator.AnimatorListener() { |
| @Override |
| public void onAnimationCancel(Animator animator) {} |
| |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| if (mFullScreenStatusListener != null) { |
| mFullScreenStatusListener.onFullScreenStatusChange(toFullScreen); |
| } |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animator) {} |
| |
| @Override |
| public void onAnimationStart(Animator animator) {} |
| }); |
| animatorSet.start(); |
| |
| animateColor(toFullScreen); |
| |
| // Changes appearances of some elements. |
| mWorkspaceVisibility = true; |
| |
| if (toFullScreen) { |
| ((Button) mView.findViewById(R.id.hide_ui_preview_button)).setText( |
| R.string.hide_ui_preview_text |
| ); |
| } |
| |
| mView.findViewById(R.id.lock_screen_preview_container).setVisibility(View.VISIBLE); |
| if (mIsHomeSelected) { |
| mView.findViewById(R.id.lock_screen_preview_container) |
| .setVisibility(View.INVISIBLE); |
| } |
| |
| mIsFullScreen = toFullScreen; |
| } |
| } |