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.keyguard.ui.view.layout.sections.transitions 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.TransitionListenerAdapter 25 import android.transition.TransitionSet 26 import android.transition.TransitionValues 27 import android.util.Log 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.ViewTreeObserver.OnPreDrawListener 31 import com.android.app.animation.Interpolators 32 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition 33 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type 34 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_DOWN_MILLIS 35 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.ClockSizeTransition.SmartspaceMoveTransition.Companion.STATUS_AREA_MOVE_UP_MILLIS 36 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel 37 import com.android.systemui.res.R 38 import com.android.systemui.shared.R as sharedR 39 import com.google.android.material.math.MathUtils 40 import kotlin.math.abs 41 42 internal fun View.getRect(): Rect = Rect(this.left, this.top, this.right, this.bottom) 43 44 internal fun View.setRect(rect: Rect) = 45 this.setLeftTopRightBottom(rect.left, rect.top, rect.right, rect.bottom) 46 47 class ClockSizeTransition( 48 config: IntraBlueprintTransition.Config, 49 clockViewModel: KeyguardClockViewModel, 50 ) : TransitionSet() { 51 init { 52 ordering = ORDERING_TOGETHER 53 if (config.type != Type.SmartspaceVisibility) { 54 addTransition(ClockFaceOutTransition(config, clockViewModel)) 55 addTransition(ClockFaceInTransition(config, clockViewModel)) 56 } 57 addTransition(SmartspaceMoveTransition(config, clockViewModel)) 58 } 59 60 abstract class VisibilityBoundsTransition() : Transition() { 61 abstract val captureSmartspace: Boolean 62 protected val TAG = this::class.simpleName!! 63 64 override fun captureEndValues(transition: TransitionValues) = captureValues(transition) 65 66 override fun captureStartValues(transition: TransitionValues) = captureValues(transition) 67 68 override fun getTransitionProperties(): Array<String> = TRANSITION_PROPERTIES 69 70 private fun captureValues(transition: TransitionValues) { 71 val view = transition.view 72 transition.values[PROP_VISIBILITY] = view.visibility 73 transition.values[PROP_ALPHA] = view.alpha 74 transition.values[PROP_BOUNDS] = view.getRect() 75 76 if (!captureSmartspace) return 77 val parent = view.parent as View 78 val targetSSView = 79 parent.findViewById<View>(sharedR.id.bc_smartspace_view) 80 ?: parent.findViewById<View>(R.id.keyguard_slice_view) 81 if (targetSSView == null) { 82 Log.e(TAG, "Failed to find smartspace equivalent target under $parent") 83 return 84 } 85 transition.values[SMARTSPACE_BOUNDS] = targetSSView.getRect() 86 } 87 88 open fun mutateBounds( 89 view: View, 90 fromIsVis: Boolean, 91 toIsVis: Boolean, 92 fromBounds: Rect, 93 toBounds: Rect, 94 fromSSBounds: Rect?, 95 toSSBounds: Rect? 96 ) {} 97 98 override fun createAnimator( 99 sceenRoot: ViewGroup, 100 startValues: TransitionValues?, 101 endValues: TransitionValues? 102 ): Animator? { 103 if (startValues == null || endValues == null) { 104 Log.w( 105 TAG, 106 "Couldn't create animator: startValues=$startValues; endValues=$endValues" 107 ) 108 return null 109 } 110 111 var fromVis = startValues.values[PROP_VISIBILITY] as Int 112 var fromIsVis = fromVis == View.VISIBLE 113 var fromAlpha = startValues.values[PROP_ALPHA] as Float 114 val fromBounds = startValues.values[PROP_BOUNDS] as Rect 115 val fromSSBounds = startValues.values[SMARTSPACE_BOUNDS] as Rect? 116 117 val toView = endValues.view 118 val toVis = endValues.values[PROP_VISIBILITY] as Int 119 val toBounds = endValues.values[PROP_BOUNDS] as Rect 120 val toSSBounds = endValues.values[SMARTSPACE_BOUNDS] as Rect? 121 val toIsVis = toVis == View.VISIBLE 122 val toAlpha = if (toIsVis) 1f else 0f 123 124 // Align starting visibility and alpha 125 if (!fromIsVis) fromAlpha = 0f 126 else if (fromAlpha <= 0f) { 127 fromIsVis = false 128 fromVis = View.INVISIBLE 129 } 130 131 mutateBounds(toView, fromIsVis, toIsVis, fromBounds, toBounds, fromSSBounds, toSSBounds) 132 if (fromIsVis == toIsVis && fromBounds.equals(toBounds)) { 133 if (DEBUG) { 134 Log.w( 135 TAG, 136 "Skipping no-op transition: $toView; " + 137 "vis: $fromVis -> $toVis; " + 138 "alpha: $fromAlpha -> $toAlpha; " + 139 "bounds: $fromBounds -> $toBounds; " 140 ) 141 } 142 return null 143 } 144 145 val sendToBack = fromIsVis && !toIsVis 146 fun lerp(start: Int, end: Int, fract: Float): Int = 147 MathUtils.lerp(start.toFloat(), end.toFloat(), fract).toInt() 148 fun computeBounds(fract: Float): Rect = 149 Rect( 150 lerp(fromBounds.left, toBounds.left, fract), 151 lerp(fromBounds.top, toBounds.top, fract), 152 lerp(fromBounds.right, toBounds.right, fract), 153 lerp(fromBounds.bottom, toBounds.bottom, fract) 154 ) 155 156 fun assignAnimValues(src: String, fract: Float, vis: Int? = null) { 157 val bounds = computeBounds(fract) 158 val alpha = MathUtils.lerp(fromAlpha, toAlpha, fract) 159 if (DEBUG) { 160 Log.i( 161 TAG, 162 "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;" 163 ) 164 } 165 toView.setVisibility(vis ?: View.VISIBLE) 166 toView.setAlpha(alpha) 167 toView.setRect(bounds) 168 } 169 170 if (DEBUG) { 171 Log.i( 172 TAG, 173 "transitioning: $toView; " + 174 "vis: $fromVis -> $toVis; " + 175 "alpha: $fromAlpha -> $toAlpha; " + 176 "bounds: $fromBounds -> $toBounds; " 177 ) 178 } 179 180 return ValueAnimator.ofFloat(0f, 1f).also { anim -> 181 // We enforce the animation parameters on the target view every frame using a 182 // predraw listener. This is suboptimal but prevents issues with layout passes 183 // overwriting the animation for individual frames. 184 val predrawCallback = OnPreDrawListener { 185 assignAnimValues("predraw", anim.animatedFraction) 186 return@OnPreDrawListener true 187 } 188 189 this@VisibilityBoundsTransition.addListener( 190 object : TransitionListenerAdapter() { 191 override fun onTransitionStart(t: Transition) { 192 toView.viewTreeObserver.addOnPreDrawListener(predrawCallback) 193 } 194 195 override fun onTransitionEnd(t: Transition) { 196 toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback) 197 } 198 } 199 ) 200 201 val listener = 202 object : AnimatorListenerAdapter() { 203 override fun onAnimationStart(anim: Animator) { 204 assignAnimValues("start", 0f, fromVis) 205 } 206 207 override fun onAnimationEnd(anim: Animator) { 208 assignAnimValues("end", 1f, toVis) 209 if (sendToBack) toView.translationZ = 0f 210 } 211 } 212 213 anim.addListener(listener) 214 assignAnimValues("init", 0f, fromVis) 215 } 216 } 217 218 companion object { 219 private const val PROP_VISIBILITY = "ClockSizeTransition:Visibility" 220 private const val PROP_ALPHA = "ClockSizeTransition:Alpha" 221 private const val PROP_BOUNDS = "ClockSizeTransition:Bounds" 222 private const val SMARTSPACE_BOUNDS = "ClockSizeTransition:SSBounds" 223 private val TRANSITION_PROPERTIES = 224 arrayOf(PROP_VISIBILITY, PROP_ALPHA, PROP_BOUNDS, SMARTSPACE_BOUNDS) 225 } 226 } 227 228 abstract class ClockFaceTransition( 229 config: IntraBlueprintTransition.Config, 230 val viewModel: KeyguardClockViewModel, 231 ) : VisibilityBoundsTransition() { 232 protected abstract val isLargeClock: Boolean 233 protected abstract val smallClockMoveScale: Float 234 override val captureSmartspace 235 get() = !isLargeClock 236 237 protected fun addTargets() { 238 if (isLargeClock) { 239 viewModel.currentClock.value?.let { 240 if (DEBUG) Log.i(TAG, "Adding large clock views: ${it.largeClock.layout.views}") 241 it.largeClock.layout.views.forEach { addTarget(it) } 242 } 243 ?: run { 244 Log.e(TAG, "No large clock set, falling back") 245 addTarget(R.id.lockscreen_clock_view_large) 246 } 247 } else { 248 if (DEBUG) Log.i(TAG, "Adding small clock") 249 addTarget(R.id.lockscreen_clock_view) 250 } 251 } 252 253 override fun mutateBounds( 254 view: View, 255 fromIsVis: Boolean, 256 toIsVis: Boolean, 257 fromBounds: Rect, 258 toBounds: Rect, 259 fromSSBounds: Rect?, 260 toSSBounds: Rect? 261 ) { 262 // Move normally if clock is not changing visibility 263 if (fromIsVis == toIsVis) return 264 265 fromBounds.set(toBounds) 266 if (isLargeClock) { 267 // Large clock shouldn't move; fromBounds already set 268 } else if (toSSBounds != null && fromSSBounds != null) { 269 // Instead of moving the small clock the full distance, we compute the distance 270 // smartspace will move. We then scale this to match the duration of this animation 271 // so that the small clock moves at the same speed as smartspace. 272 val ssTranslation = 273 abs((toSSBounds.top - fromSSBounds.top) * smallClockMoveScale).toInt() 274 fromBounds.top = toBounds.top - ssTranslation 275 fromBounds.bottom = toBounds.bottom - ssTranslation 276 } else { 277 Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds") 278 } 279 } 280 } 281 282 class ClockFaceInTransition( 283 config: IntraBlueprintTransition.Config, 284 viewModel: KeyguardClockViewModel, 285 ) : ClockFaceTransition(config, viewModel) { 286 override val isLargeClock = viewModel.isLargeClockVisible.value 287 override val smallClockMoveScale = CLOCK_IN_MILLIS / STATUS_AREA_MOVE_DOWN_MILLIS.toFloat() 288 289 init { 290 duration = CLOCK_IN_MILLIS 291 startDelay = CLOCK_IN_START_DELAY_MILLIS 292 interpolator = CLOCK_IN_INTERPOLATOR 293 addTargets() 294 } 295 296 companion object { 297 const val CLOCK_IN_MILLIS = 167L 298 const val CLOCK_IN_START_DELAY_MILLIS = 133L 299 val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN 300 } 301 } 302 303 class ClockFaceOutTransition( 304 config: IntraBlueprintTransition.Config, 305 viewModel: KeyguardClockViewModel, 306 ) : ClockFaceTransition(config, viewModel) { 307 override val isLargeClock = !viewModel.isLargeClockVisible.value 308 override val smallClockMoveScale = CLOCK_OUT_MILLIS / STATUS_AREA_MOVE_UP_MILLIS.toFloat() 309 310 init { 311 duration = CLOCK_OUT_MILLIS 312 interpolator = CLOCK_OUT_INTERPOLATOR 313 addTargets() 314 } 315 316 companion object { 317 const val CLOCK_OUT_MILLIS = 133L 318 val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR 319 } 320 } 321 322 // TODO: Might need a mechanism to update this one while in-progress 323 class SmartspaceMoveTransition( 324 val config: IntraBlueprintTransition.Config, 325 viewModel: KeyguardClockViewModel, 326 ) : VisibilityBoundsTransition() { 327 private val isLargeClock = viewModel.isLargeClockVisible.value 328 override val captureSmartspace = false 329 330 init { 331 duration = 332 if (isLargeClock) STATUS_AREA_MOVE_UP_MILLIS else STATUS_AREA_MOVE_DOWN_MILLIS 333 interpolator = Interpolators.EMPHASIZED 334 addTarget(sharedR.id.date_smartspace_view) 335 addTarget(sharedR.id.bc_smartspace_view) 336 337 // Notifications normally and media on split shade needs to be moved 338 addTarget(R.id.aod_notification_icon_container) 339 addTarget(R.id.status_view_media_container) 340 } 341 342 override fun mutateBounds( 343 view: View, 344 fromIsVis: Boolean, 345 toIsVis: Boolean, 346 fromBounds: Rect, 347 toBounds: Rect, 348 fromSSBounds: Rect?, 349 toSSBounds: Rect? 350 ) { 351 // If view is changing visibility, hold it in place 352 if (fromIsVis == toIsVis) return 353 if (DEBUG) Log.i(TAG, "Holding position of ${view.id}") 354 toBounds.set(fromBounds) 355 } 356 357 companion object { 358 const val STATUS_AREA_MOVE_UP_MILLIS = 967L 359 const val STATUS_AREA_MOVE_DOWN_MILLIS = 467L 360 } 361 } 362 363 companion object { 364 val DEBUG = false 365 } 366 } 367