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