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