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