• 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 
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