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