• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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