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