• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 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.keyguard
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.graphics.Rect
23 import android.transition.Transition
24 import android.transition.TransitionValues
25 import android.util.MathUtils
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.animation.AnimationUtils
29 import com.android.app.animation.Interpolators
30 import com.android.internal.R.interpolator.fast_out_extra_slow_in
31 import com.android.systemui.res.R
32 
33 /** Animates constraint layout changes for the security view. */
34 class KeyguardSecurityViewTransition : Transition() {
35 
36     companion object {
37         const val PROP_BOUNDS = "securityViewLocation:bounds"
38 
39         // The duration of the animation to switch security sides.
40         const val SECURITY_SHIFT_ANIMATION_DURATION_MS = 500L
41 
42         // How much of the switch sides animation should be dedicated to fading the security out.
43         // The remainder will fade it back in again.
44         const val SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f
45     }
46 
47     private fun captureValues(values: TransitionValues) {
48         val boundsRect = Rect()
49         boundsRect.left = values.view.left
50         boundsRect.top = values.view.top
51         boundsRect.right = values.view.right
52         boundsRect.bottom = values.view.bottom
53         values.values[PROP_BOUNDS] = boundsRect
54     }
55 
56     override fun getTransitionProperties(): Array<String>? {
57         return arrayOf(PROP_BOUNDS)
58     }
59 
60     override fun captureEndValues(transitionValues: TransitionValues?) {
61         transitionValues?.let { captureValues(it) }
62     }
63 
64     override fun captureStartValues(transitionValues: TransitionValues?) {
65         transitionValues?.let { captureValues(it) }
66     }
67 
68     override fun createAnimator(
69         sceneRoot: ViewGroup,
70         startValues: TransitionValues?,
71         endValues: TransitionValues?
72     ): Animator? {
73         if (startValues == null || endValues == null) {
74             return null
75         }
76 
77         // This animation is a bit fun to implement. The bouncer needs to move, and fade
78         // in/out at the same time. The issue is, the bouncer should only move a short
79         // amount (120dp or so), but obviously needs to go from one side of the screen to
80         // the other. This needs a pretty custom animation.
81         //
82         // This works as follows. It uses a ValueAnimation to simply drive the animation
83         // progress. This animator is responsible for both the translation of the bouncer,
84         // and the current fade. It will fade the bouncer out while also moving it along the
85         // 120dp path. Once the bouncer is fully faded out though, it will "snap" the
86         // bouncer closer to its destination, then fade it back in again. The effect is that
87         // the bouncer will move from 0 -> X while fading out, then
88         // (destination - X) -> destination while fading back in again.
89         // TODO(b/208250221): Make this animation properly abortable.
90         val positionInterpolator =
91             AnimationUtils.loadInterpolator(sceneRoot.context, fast_out_extra_slow_in)
92         val fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN
93         val fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN
94         var runningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
95         runningSecurityShiftAnimator.duration = SECURITY_SHIFT_ANIMATION_DURATION_MS
96         runningSecurityShiftAnimator.interpolator = Interpolators.LINEAR
97         val startRect = startValues.values[PROP_BOUNDS] as Rect
98         val endRect = endValues.values[PROP_BOUNDS] as Rect
99         val v = startValues.view
100         val totalTranslation: Int =
101             sceneRoot.resources.getDimension(R.dimen.security_shift_animation_translation).toInt()
102         val shouldRestoreLayerType =
103             (v.hasOverlappingRendering() && v.layerType != View.LAYER_TYPE_HARDWARE)
104         if (shouldRestoreLayerType) {
105             v.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */ null)
106         }
107         val initialAlpha: Float = v.alpha
108         runningSecurityShiftAnimator.addListener(
109             object : AnimatorListenerAdapter() {
110                 override fun onAnimationEnd(animation: Animator) {
111                     runningSecurityShiftAnimator = null
112                     if (shouldRestoreLayerType) {
113                         v.setLayerType(View.LAYER_TYPE_NONE, /* paint= */ null)
114                     }
115                 }
116             }
117         )
118 
119         runningSecurityShiftAnimator.addUpdateListener { animation: ValueAnimator ->
120             val switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION
121             val isFadingOut = animation.animatedFraction < switchPoint
122             val opacity: Float
123             var currentTranslation =
124                 (positionInterpolator.getInterpolation(animation.animatedFraction) *
125                         totalTranslation)
126                     .toInt()
127             var translationRemaining = totalTranslation - currentTranslation
128             val leftAlign = endRect.left < startRect.left
129             if (leftAlign) {
130                 currentTranslation = -currentTranslation
131                 translationRemaining = -translationRemaining
132             }
133 
134             if (isFadingOut) {
135                 // The bouncer fades out over the first X%.
136                 val fadeOutFraction =
137                     MathUtils.constrainedMap(
138                         /* rangeMin= */ 1.0f,
139                         /* rangeMax= */ 0.0f,
140                         /* valueMin= */ 0.0f,
141                         /* valueMax= */ switchPoint,
142                         animation.animatedFraction
143                     )
144                 opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction)
145 
146                 // When fading out, the alpha needs to start from the initial opacity of the
147                 // view flipper, otherwise we get a weird bit of jank as it ramps back to
148                 // 100%.
149                 v.alpha = opacity * initialAlpha
150                 if (v is KeyguardSecurityViewFlipper) {
151                     v.setLeftTopRightBottom(
152                         startRect.left + currentTranslation,
153                         startRect.top,
154                         startRect.right + currentTranslation,
155                         startRect.bottom
156                     )
157                 } else {
158                     v.setLeftTopRightBottom(
159                         startRect.left,
160                         startRect.top,
161                         startRect.right,
162                         startRect.bottom
163                     )
164                 }
165             } else {
166                 // And in again over the remaining (100-X)%.
167                 val fadeInFraction =
168                     MathUtils.constrainedMap(
169                         /* rangeMin= */ 0.0f,
170                         /* rangeMax= */ 1.0f,
171                         /* valueMin= */ switchPoint,
172                         /* valueMax= */ 1.0f,
173                         animation.animatedFraction
174                     )
175                 opacity = fadeInInterpolator.getInterpolation(fadeInFraction)
176                 v.alpha = opacity
177 
178                 // Fading back in, animate towards the destination.
179                 if (v is KeyguardSecurityViewFlipper) {
180                     v.setLeftTopRightBottom(
181                         endRect.left - translationRemaining,
182                         endRect.top,
183                         endRect.right - translationRemaining,
184                         endRect.bottom
185                     )
186                 } else {
187                     v.setLeftTopRightBottom(
188                         endRect.left,
189                         endRect.top,
190                         endRect.right,
191                         endRect.bottom
192                     )
193                 }
194             }
195         }
196         runningSecurityShiftAnimator.start()
197         return runningSecurityShiftAnimator
198     }
199 }
200