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