1 /* <lambda>null2 * Copyright (C) 2024 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.systemui.screenshot.ui 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.ColorStateList 24 import android.graphics.BlendMode 25 import android.graphics.Color 26 import android.graphics.Matrix 27 import android.graphics.PointF 28 import android.graphics.Rect 29 import android.util.MathUtils 30 import android.view.View 31 import android.view.animation.AnimationUtils 32 import android.widget.ImageView 33 import androidx.core.animation.doOnEnd 34 import androidx.core.animation.doOnStart 35 import com.android.systemui.res.R 36 import com.android.systemui.screenshot.scroll.ScrollCaptureController 37 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel 38 import kotlin.math.abs 39 import kotlin.math.max 40 import kotlin.math.sign 41 42 class ScreenshotAnimationController( 43 private val view: ScreenshotShelfView, 44 private val viewModel: ScreenshotViewModel, 45 ) { 46 private var animator: Animator? = null 47 private val screenshotPreview = view.requireViewById<ImageView>(R.id.screenshot_preview) 48 private val scrollingScrim = view.requireViewById<ImageView>((R.id.screenshot_scrolling_scrim)) 49 private val scrollTransitionPreview = 50 view.requireViewById<ImageView>(R.id.screenshot_scrollable_preview) 51 private val flashView = view.requireViewById<View>(R.id.screenshot_flash) 52 private val actionContainer = view.requireViewById<View>(R.id.actions_container_background) 53 private val fastOutSlowIn = 54 AnimationUtils.loadInterpolator(view.context, android.R.interpolator.fast_out_slow_in) 55 private val staticUI = 56 listOf<View>( 57 view.requireViewById(R.id.screenshot_preview_border), 58 view.requireViewById(R.id.screenshot_badge), 59 view.requireViewById(R.id.screenshot_dismiss_button), 60 ) 61 private val fadeUI = 62 listOf<View>( 63 view.requireViewById(R.id.screenshot_preview_border), 64 view.requireViewById(R.id.actions_container_background), 65 view.requireViewById(R.id.screenshot_badge), 66 view.requireViewById(R.id.screenshot_dismiss_button), 67 view.requireViewById(R.id.screenshot_message_container), 68 ) 69 70 fun getEntranceAnimation( 71 bounds: Rect, 72 showFlash: Boolean, 73 onRevealMilestone: () -> Unit, 74 ): Animator { 75 val entranceAnimation = AnimatorSet() 76 view.alpha = 1f 77 view.translationX = 0f 78 79 val previewAnimator = getPreviewAnimator(bounds) 80 81 if (showFlash) { 82 val flashInAnimator = 83 ObjectAnimator.ofFloat(flashView, "alpha", 0f, 1f).apply { 84 duration = FLASH_IN_DURATION_MS 85 interpolator = fastOutSlowIn 86 } 87 val flashOutAnimator = 88 ObjectAnimator.ofFloat(flashView, "alpha", 1f, 0f).apply { 89 duration = FLASH_OUT_DURATION_MS 90 interpolator = fastOutSlowIn 91 } 92 flashInAnimator.doOnStart { flashView.visibility = View.VISIBLE } 93 flashOutAnimator.doOnEnd { flashView.visibility = View.GONE } 94 entranceAnimation.play(flashOutAnimator).after(flashInAnimator) 95 entranceAnimation.play(previewAnimator).with(flashOutAnimator) 96 entranceAnimation.doOnStart { screenshotPreview.visibility = View.INVISIBLE } 97 } 98 99 val actionsAnimator = getActionsAnimator() 100 entranceAnimation.play(actionsAnimator).with(previewAnimator) 101 102 // This isn't actually animating anything but is basically a timer for the first 200ms of 103 // the entrance animation. Using an animator here ensures that this is scaled if we change 104 // animator duration scales. 105 val revealMilestoneAnimator = 106 ValueAnimator.ofFloat(0f).apply { 107 duration = 0 108 startDelay = ACTION_REVEAL_DELAY_MS 109 doOnEnd { onRevealMilestone() } 110 } 111 entranceAnimation.play(revealMilestoneAnimator).with(actionsAnimator) 112 113 val fadeInAnimator = ValueAnimator.ofFloat(0f, 1f) 114 fadeInAnimator.addUpdateListener { 115 for (child in staticUI) { 116 child.alpha = it.animatedValue as Float 117 } 118 } 119 entranceAnimation.play(fadeInAnimator).after(previewAnimator) 120 entranceAnimation.doOnStart { 121 viewModel.setIsAnimating(true) 122 for (child in staticUI) { 123 child.alpha = 0f 124 } 125 } 126 entranceAnimation.doOnEnd { viewModel.setIsAnimating(false) } 127 128 this.animator = entranceAnimation 129 return entranceAnimation 130 } 131 132 fun fadeForSharedTransition() { 133 animator?.cancel() 134 val fadeAnimator = ValueAnimator.ofFloat(1f, 0f) 135 fadeAnimator.addUpdateListener { 136 for (view in fadeUI) { 137 view.alpha = it.animatedValue as Float 138 } 139 } 140 animator = fadeAnimator 141 fadeAnimator.start() 142 } 143 144 fun runLongScreenshotTransition( 145 destRect: Rect, 146 longScreenshot: ScrollCaptureController.LongScreenshot, 147 onTransitionEnd: Runnable, 148 ): Animator { 149 val animSet = AnimatorSet() 150 151 val scrimAnim = ValueAnimator.ofFloat(0f, 1f) 152 scrimAnim.addUpdateListener { animation: ValueAnimator -> 153 scrollingScrim.setAlpha(1 - animation.animatedFraction) 154 } 155 scrollTransitionPreview.visibility = View.VISIBLE 156 if (true) { 157 scrollTransitionPreview.setImageBitmap(longScreenshot.toBitmap()) 158 val startX: Float = scrollTransitionPreview.x 159 val startY: Float = scrollTransitionPreview.y 160 val locInScreen: IntArray = scrollTransitionPreview.getLocationOnScreen() 161 destRect.offset(startX.toInt() - locInScreen[0], startY.toInt() - locInScreen[1]) 162 scrollTransitionPreview.pivotX = 0f 163 scrollTransitionPreview.pivotY = 0f 164 scrollTransitionPreview.setAlpha(1f) 165 val currentScale: Float = scrollTransitionPreview.width / longScreenshot.width.toFloat() 166 val matrix = Matrix() 167 matrix.setScale(currentScale, currentScale) 168 matrix.postTranslate( 169 longScreenshot.left * currentScale, 170 longScreenshot.top * currentScale, 171 ) 172 scrollTransitionPreview.setImageMatrix(matrix) 173 val destinationScale: Float = destRect.width() / scrollTransitionPreview.width.toFloat() 174 val previewAnim = ValueAnimator.ofFloat(0f, 1f) 175 previewAnim.addUpdateListener { animation: ValueAnimator -> 176 val t = animation.animatedFraction 177 val currScale = MathUtils.lerp(1f, destinationScale, t) 178 scrollTransitionPreview.scaleX = currScale 179 scrollTransitionPreview.scaleY = currScale 180 scrollTransitionPreview.x = MathUtils.lerp(startX, destRect.left.toFloat(), t) 181 scrollTransitionPreview.y = MathUtils.lerp(startY, destRect.top.toFloat(), t) 182 } 183 val previewFadeAnim = ValueAnimator.ofFloat(1f, 0f) 184 previewFadeAnim.addUpdateListener { animation: ValueAnimator -> 185 scrollTransitionPreview.setAlpha(1 - animation.animatedFraction) 186 } 187 previewAnim.doOnEnd { onTransitionEnd.run() } 188 animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim) 189 } else { 190 // if we switched orientations between the original screenshot and the long screenshot 191 // capture, just fade out the scrim instead of running the preview animation 192 scrimAnim.doOnEnd { onTransitionEnd.run() } 193 animSet.play(scrimAnim) 194 } 195 animator = animSet 196 return animSet 197 } 198 199 fun fadeForLongScreenshotTransition() { 200 scrollingScrim.imageTintBlendMode = BlendMode.SRC_ATOP 201 val anim = ValueAnimator.ofFloat(0f, .3f) 202 anim.addUpdateListener { 203 scrollingScrim.setImageTintList( 204 ColorStateList.valueOf(Color.argb(it.animatedValue as Float, 0f, 0f, 0f)) 205 ) 206 } 207 for (view in fadeUI) { 208 view.alpha = 0f 209 } 210 screenshotPreview.alpha = 0f 211 anim.setDuration(200) 212 anim.start() 213 } 214 215 fun restoreUI() { 216 animator?.cancel() 217 for (view in fadeUI) { 218 view.alpha = 1f 219 } 220 screenshotPreview.alpha = 1f 221 } 222 223 fun getSwipeReturnAnimation(): Animator { 224 animator?.cancel() 225 val animator = ValueAnimator.ofFloat(view.translationX, 0f) 226 animator.addUpdateListener { view.translationX = it.animatedValue as Float } 227 this.animator = animator 228 return animator 229 } 230 231 fun getSwipeDismissAnimation(requestedVelocity: Float?): Animator { 232 animator?.cancel() 233 val velocity = getAdjustedVelocity(requestedVelocity) 234 val screenWidth = view.resources.displayMetrics.widthPixels 235 // translation at which point the visible UI is fully off the screen (in the direction 236 // according to velocity) 237 val endX = 238 if (velocity < 0) { 239 -1f * actionContainer.right 240 } else { 241 (screenWidth - actionContainer.left).toFloat() 242 } 243 val distance = endX - view.translationX 244 val animator = ValueAnimator.ofFloat(view.translationX, endX) 245 animator.addUpdateListener { 246 view.translationX = it.animatedValue as Float 247 view.alpha = 1f - it.animatedFraction 248 } 249 animator.duration = ((abs(distance / velocity))).toLong() 250 animator.doOnStart { viewModel.setIsAnimating(true) } 251 animator.doOnEnd { viewModel.setIsAnimating(false) } 252 253 this.animator = animator 254 return animator 255 } 256 257 fun cancel() { 258 animator?.cancel() 259 } 260 261 private fun getActionsAnimator(): Animator { 262 val startingOffset = view.height - actionContainer.top 263 val actionsYAnimator = 264 ValueAnimator.ofFloat(startingOffset.toFloat(), 0f).apply { 265 duration = PREVIEW_Y_ANIMATION_DURATION_MS 266 interpolator = fastOutSlowIn 267 } 268 actionsYAnimator.addUpdateListener { 269 actionContainer.translationY = it.animatedValue as Float 270 } 271 actionContainer.translationY = startingOffset.toFloat() 272 return actionsYAnimator 273 } 274 275 private fun getPreviewAnimator(bounds: Rect): Animator { 276 val targetPosition = Rect() 277 screenshotPreview.getHitRect(targetPosition) 278 val startXScale = bounds.width() / targetPosition.width().toFloat() 279 val startYScale = bounds.height() / targetPosition.height().toFloat() 280 val startPos = PointF(bounds.exactCenterX(), bounds.exactCenterY()) 281 val endPos = PointF(targetPosition.exactCenterX(), targetPosition.exactCenterY()) 282 283 val previewYAnimator = 284 ValueAnimator.ofFloat(startPos.y, endPos.y).apply { 285 duration = PREVIEW_Y_ANIMATION_DURATION_MS 286 interpolator = fastOutSlowIn 287 } 288 previewYAnimator.addUpdateListener { 289 val progress = it.animatedValue as Float 290 screenshotPreview.y = progress - screenshotPreview.height / 2f 291 } 292 // scale animation starts/finishes at the same time as x placement 293 val previewXAndScaleAnimator = 294 ValueAnimator.ofFloat(0f, 1f).apply { 295 duration = PREVIEW_X_ANIMATION_DURATION_MS 296 interpolator = fastOutSlowIn 297 } 298 previewXAndScaleAnimator.addUpdateListener { 299 val t = it.animatedFraction 300 screenshotPreview.scaleX = MathUtils.lerp(startXScale, 1f, t) 301 screenshotPreview.scaleY = MathUtils.lerp(startYScale, 1f, t) 302 screenshotPreview.x = 303 MathUtils.lerp(startPos.x, endPos.x, t) - screenshotPreview.width / 2f 304 } 305 306 val previewAnimator = AnimatorSet() 307 previewAnimator.play(previewXAndScaleAnimator).with(previewYAnimator) 308 previewAnimator.doOnEnd { 309 screenshotPreview.scaleX = 1f 310 screenshotPreview.scaleY = 1f 311 screenshotPreview.x = endPos.x - screenshotPreview.width / 2f 312 screenshotPreview.y = endPos.y - screenshotPreview.height / 2f 313 } 314 315 previewAnimator.doOnStart { screenshotPreview.visibility = View.VISIBLE } 316 return previewAnimator 317 } 318 319 private fun getAdjustedVelocity(requestedVelocity: Float?): Float { 320 return if (requestedVelocity == null || abs(requestedVelocity) < .005f) { 321 val isLTR = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR 322 // dismiss to the left in LTR locales, to the right in RTL 323 if (isLTR) -MINIMUM_VELOCITY else MINIMUM_VELOCITY 324 } else { 325 sign(requestedVelocity) * max(MINIMUM_VELOCITY, abs(requestedVelocity)) 326 } 327 } 328 329 companion object { 330 private const val MINIMUM_VELOCITY = 1.5f // pixels per second 331 private const val FLASH_IN_DURATION_MS: Long = 133 332 private const val FLASH_OUT_DURATION_MS: Long = 217 333 private const val PREVIEW_X_ANIMATION_DURATION_MS: Long = 234 334 private const val PREVIEW_Y_ANIMATION_DURATION_MS: Long = 500 335 private const val ACTION_REVEAL_DELAY_MS: Long = 200 336 } 337 } 338