1 /* 2 * Copyright (C) 2020 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.media.controls.ui 18 19 import android.content.Context 20 import android.content.res.Configuration 21 import androidx.annotation.VisibleForTesting 22 import androidx.constraintlayout.widget.ConstraintSet 23 import com.android.systemui.R 24 import com.android.systemui.media.controls.models.GutsViewHolder 25 import com.android.systemui.media.controls.models.player.MediaViewHolder 26 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder 27 import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha 28 import com.android.systemui.media.controls.util.MediaFlags 29 import com.android.systemui.statusbar.policy.ConfigurationController 30 import com.android.systemui.util.animation.MeasurementOutput 31 import com.android.systemui.util.animation.TransitionLayout 32 import com.android.systemui.util.animation.TransitionLayoutController 33 import com.android.systemui.util.animation.TransitionViewState 34 import com.android.systemui.util.traceSection 35 import java.lang.Float.max 36 import java.lang.Float.min 37 import javax.inject.Inject 38 39 /** 40 * A class responsible for controlling a single instance of a media player handling interactions 41 * with the view instance and keeping the media view states up to date. 42 */ 43 class MediaViewController 44 @Inject 45 constructor( 46 private val context: Context, 47 private val configurationController: ConfigurationController, 48 private val mediaHostStatesManager: MediaHostStatesManager, 49 private val logger: MediaViewLogger, 50 private val mediaFlags: MediaFlags, 51 ) { 52 53 /** 54 * Indicating that the media view controller is for a notification-based player, session-based 55 * player, or recommendation 56 */ 57 enum class TYPE { 58 PLAYER, 59 RECOMMENDATION 60 } 61 62 companion object { 63 @JvmField val GUTS_ANIMATION_DURATION = 500L 64 } 65 66 /** A listener when the current dimensions of the player change */ 67 lateinit var sizeChangedListener: () -> Unit 68 lateinit var configurationChangeListener: () -> Unit 69 private var firstRefresh: Boolean = true 70 @VisibleForTesting private var transitionLayout: TransitionLayout? = null 71 private val layoutController = TransitionLayoutController() 72 private var animationDelay: Long = 0 73 private var animationDuration: Long = 0 74 private var animateNextStateChange: Boolean = false 75 private val measurement = MeasurementOutput(0, 0) 76 private var type: TYPE = TYPE.PLAYER 77 78 /** A map containing all viewStates for all locations of this mediaState */ 79 private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() 80 81 /** 82 * The ending location of the view where it ends when all animations and transitions have 83 * finished 84 */ 85 @MediaLocation var currentEndLocation: Int = -1 86 87 /** The starting location of the view where it starts for all animations and transitions */ 88 @MediaLocation private var currentStartLocation: Int = -1 89 90 /** The progress of the transition or 1.0 if there is no transition happening */ 91 private var currentTransitionProgress: Float = 1.0f 92 93 /** A temporary state used to store intermediate measurements. */ 94 private val tmpState = TransitionViewState() 95 96 /** A temporary state used to store intermediate measurements. */ 97 private val tmpState2 = TransitionViewState() 98 99 /** A temporary state used to store intermediate measurements. */ 100 private val tmpState3 = TransitionViewState() 101 102 /** A temporary cache key to be used to look up cache entries */ 103 private val tmpKey = CacheKey() 104 105 /** 106 * The current width of the player. This might not factor in case the player is animating to the 107 * current state, but represents the end state 108 */ 109 var currentWidth: Int = 0 110 /** 111 * The current height of the player. This might not factor in case the player is animating to 112 * the current state, but represents the end state 113 */ 114 var currentHeight: Int = 0 115 116 /** Get the translationX of the layout */ 117 var translationX: Float = 0.0f 118 private set 119 get() { 120 return transitionLayout?.translationX ?: 0.0f 121 } 122 123 /** Get the translationY of the layout */ 124 var translationY: Float = 0.0f 125 private set 126 get() { 127 return transitionLayout?.translationY ?: 0.0f 128 } 129 130 /** A callback for config changes */ 131 private val configurationListener = 132 object : ConfigurationController.ConfigurationListener { 133 var lastOrientation = -1 134 135 override fun onConfigChanged(newConfig: Configuration?) { 136 // Because the TransitionLayout is not always attached (and calculates/caches layout 137 // results regardless of attach state), we have to force the layoutDirection of the 138 // view 139 // to the correct value for the user's current locale to ensure correct 140 // recalculation 141 // when/after calling refreshState() 142 newConfig?.apply { 143 if (transitionLayout?.rawLayoutDirection != layoutDirection) { 144 transitionLayout?.layoutDirection = layoutDirection 145 refreshState() 146 } 147 val newOrientation = newConfig.orientation 148 if (lastOrientation != newOrientation) { 149 // Layout dimensions are possibly changing, so we need to update them. (at 150 // least on large screen devices) 151 lastOrientation = newOrientation 152 // Update the height of media controls for the expanded layout. it is needed 153 // for large screen devices. 154 val backgroundIds = 155 if (type == TYPE.PLAYER) { 156 MediaViewHolder.backgroundIds 157 } else { 158 setOf(RecommendationViewHolder.backgroundId) 159 } 160 backgroundIds.forEach { id -> 161 expandedLayout.getConstraint(id).layout.mHeight = 162 context.resources.getDimensionPixelSize( 163 R.dimen.qs_media_session_height_expanded 164 ) 165 } 166 } 167 if (this@MediaViewController::configurationChangeListener.isInitialized) { 168 configurationChangeListener.invoke() 169 refreshState() 170 } 171 } 172 } 173 } 174 175 /** A callback for media state changes */ 176 val stateCallback = 177 object : MediaHostStatesManager.Callback { 178 override fun onHostStateChanged( 179 @MediaLocation location: Int, 180 mediaHostState: MediaHostState 181 ) { 182 if (location == currentEndLocation || location == currentStartLocation) { 183 setCurrentState( 184 currentStartLocation, 185 currentEndLocation, 186 currentTransitionProgress, 187 applyImmediately = false 188 ) 189 } 190 } 191 } 192 193 /** 194 * The expanded constraint set used to render a expanded player. If it is modified, make sure to 195 * call [refreshState] 196 */ 197 var collapsedLayout = ConstraintSet() 198 @VisibleForTesting set 199 /** 200 * The expanded constraint set used to render a collapsed player. If it is modified, make sure 201 * to call [refreshState] 202 */ 203 var expandedLayout = ConstraintSet() 204 @VisibleForTesting set 205 206 /** Whether the guts are visible for the associated player. */ 207 var isGutsVisible = false 208 private set 209 210 init { 211 mediaHostStatesManager.addController(this) 212 layoutController.sizeChangedListener = { width: Int, height: Int -> 213 currentWidth = width 214 currentHeight = height 215 sizeChangedListener.invoke() 216 } 217 configurationController.addCallback(configurationListener) 218 } 219 220 /** 221 * Notify this controller that the view has been removed and all listeners should be destroyed 222 */ 223 fun onDestroy() { 224 mediaHostStatesManager.removeController(this) 225 configurationController.removeCallback(configurationListener) 226 } 227 228 /** Show guts with an animated transition. */ 229 fun openGuts() { 230 if (isGutsVisible) return 231 isGutsVisible = true 232 animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) 233 setCurrentState( 234 currentStartLocation, 235 currentEndLocation, 236 currentTransitionProgress, 237 applyImmediately = false 238 ) 239 } 240 241 /** 242 * Close the guts for the associated player. 243 * 244 * @param immediate if `false`, it will animate the transition. 245 */ 246 @JvmOverloads 247 fun closeGuts(immediate: Boolean = false) { 248 if (!isGutsVisible) return 249 isGutsVisible = false 250 if (!immediate) { 251 animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) 252 } 253 setCurrentState( 254 currentStartLocation, 255 currentEndLocation, 256 currentTransitionProgress, 257 applyImmediately = immediate 258 ) 259 } 260 261 private fun ensureAllMeasurements() { 262 val mediaStates = mediaHostStatesManager.mediaHostStates 263 for (entry in mediaStates) { 264 obtainViewState(entry.value) 265 } 266 } 267 268 /** Get the constraintSet for a given expansion */ 269 private fun constraintSetForExpansion(expansion: Float): ConstraintSet = 270 if (expansion > 0) expandedLayout else collapsedLayout 271 272 /** 273 * Set the views to be showing/hidden based on the [isGutsVisible] for a given 274 * [TransitionViewState]. 275 */ 276 private fun setGutsViewState(viewState: TransitionViewState) { 277 val controlsIds = 278 when (type) { 279 TYPE.PLAYER -> MediaViewHolder.controlsIds 280 TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds 281 } 282 val gutsIds = GutsViewHolder.ids 283 controlsIds.forEach { id -> 284 viewState.widgetStates.get(id)?.let { state -> 285 // Make sure to use the unmodified state if guts are not visible. 286 state.alpha = if (isGutsVisible) 0f else state.alpha 287 state.gone = if (isGutsVisible) true else state.gone 288 } 289 } 290 gutsIds.forEach { id -> 291 viewState.widgetStates.get(id)?.let { state -> 292 // Make sure to use the unmodified state if guts are visible 293 state.alpha = if (isGutsVisible) state.alpha else 0f 294 state.gone = if (isGutsVisible) state.gone else true 295 } 296 } 297 } 298 299 /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */ 300 internal fun squishViewState( 301 viewState: TransitionViewState, 302 squishFraction: Float 303 ): TransitionViewState { 304 val squishedViewState = viewState.copy() 305 val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt() 306 squishedViewState.height = squishedHeight 307 // We are not overriding the squishedViewStates height but only the children to avoid 308 // them remeasuring the whole view. Instead it just remains as the original size 309 MediaViewHolder.backgroundIds.forEach { id -> 310 squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } 311 } 312 313 // media player 314 calculateWidgetGroupAlphaForSquishiness( 315 MediaViewHolder.expandedBottomActionIds, 316 squishedViewState.measureHeight.toFloat(), 317 squishedViewState, 318 squishFraction 319 ) 320 calculateWidgetGroupAlphaForSquishiness( 321 MediaViewHolder.detailIds, 322 squishedViewState.measureHeight.toFloat(), 323 squishedViewState, 324 squishFraction 325 ) 326 // recommendation card 327 val titlesTop = 328 calculateWidgetGroupAlphaForSquishiness( 329 RecommendationViewHolder.mediaTitlesAndSubtitlesIds, 330 squishedViewState.measureHeight.toFloat(), 331 squishedViewState, 332 squishFraction 333 ) 334 calculateWidgetGroupAlphaForSquishiness( 335 RecommendationViewHolder.mediaContainersIds, 336 titlesTop, 337 squishedViewState, 338 squishFraction 339 ) 340 return squishedViewState 341 } 342 343 /** 344 * This function is to make each widget in UMO disappear before being clipped by squished UMO 345 * 346 * The general rule is that widgets in UMO has been divided into several groups, and widgets in 347 * one group have the same alpha during squishing It will change from alpha 0.0 when the visible 348 * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible 349 * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause 350 * button will change alpha together. 351 * 352 * ``` 353 * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls, 354 * including progress bar, next button, previous button 355 * ``` 356 * 357 * widgetGroupIds: a group of widgets have same state during UMO is squished, 358 * ``` 359 * e.g. Album title, artist title and play-pause button 360 * ``` 361 * 362 * groupEndPosition: the height of UMO, when the height reaches this value, 363 * ``` 364 * widgets in this group should have 1.0 as alpha 365 * e.g., the group of album title, artist title and play-pause button will become fully 366 * visible when the height of UMO reaches the top of controls group 367 * (progress bar, previous button and next button) 368 * ``` 369 * 370 * squishedViewState: hold the widgetState of each widget, which will be modified 371 * squishFraction: the squishFraction of UMO 372 */ 373 private fun calculateWidgetGroupAlphaForSquishiness( 374 widgetGroupIds: Set<Int>, 375 groupEndPosition: Float, 376 squishedViewState: TransitionViewState, 377 squishFraction: Float 378 ): Float { 379 val nonsquishedHeight = squishedViewState.measureHeight 380 var groupTop = squishedViewState.measureHeight.toFloat() 381 var groupBottom = 0F 382 widgetGroupIds.forEach { id -> 383 squishedViewState.widgetStates.get(id)?.let { state -> 384 groupTop = min(groupTop, state.y) 385 groupBottom = max(groupBottom, state.y + state.height) 386 } 387 } 388 // startPosition means to the height of squished UMO where the widget alpha should start 389 // changing from 0.0 390 // generally, it equals to the bottom of widgets, so that we can meet the requirement that 391 // widget should not go beyond the bounds of background 392 // endPosition means to the height of squished UMO where the widget alpha should finish 393 // changing alpha to 1.0 394 var startPosition = groupBottom 395 val endPosition = groupEndPosition 396 if (startPosition == endPosition) { 397 startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat() 398 } 399 widgetGroupIds.forEach { id -> 400 squishedViewState.widgetStates.get(id)?.let { state -> 401 state.alpha = 402 calculateAlpha( 403 squishFraction, 404 startPosition / nonsquishedHeight, 405 endPosition / nonsquishedHeight 406 ) 407 } 408 } 409 return groupTop // used for the widget group above this group 410 } 411 412 /** 413 * Obtain a new viewState for a given media state. This usually returns a cached state, but if 414 * it's not available, it will recreate one by measuring, which may be expensive. 415 */ 416 @VisibleForTesting 417 fun obtainViewState(state: MediaHostState?): TransitionViewState? { 418 if (state == null || state.measurementInput == null) { 419 return null 420 } 421 // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey 422 var cacheKey = getKey(state, isGutsVisible, tmpKey) 423 val viewState = viewStates[cacheKey] 424 if (viewState != null) { 425 // we already have cached this measurement, let's continue 426 if (state.squishFraction <= 1f) { 427 return squishViewState(viewState, state.squishFraction) 428 } 429 return viewState 430 } 431 // Copy the key since this might call recursively into it and we're using tmpKey 432 cacheKey = cacheKey.copy() 433 val result: TransitionViewState? 434 435 if (transitionLayout == null) { 436 return null 437 } 438 // Let's create a new measurement 439 if (state.expansion == 0.0f || state.expansion == 1.0f) { 440 result = 441 transitionLayout!!.calculateViewState( 442 state.measurementInput!!, 443 constraintSetForExpansion(state.expansion), 444 TransitionViewState() 445 ) 446 447 setGutsViewState(result) 448 // We don't want to cache interpolated or null states as this could quickly fill up 449 // our cache. We only cache the start and the end states since the interpolation 450 // is cheap 451 viewStates[cacheKey] = result 452 } else { 453 // This is an interpolated state 454 val startState = state.copy().also { it.expansion = 0.0f } 455 456 // Given that we have a measurement and a view, let's get (guaranteed) viewstates 457 // from the start and end state and interpolate them 458 val startViewState = obtainViewState(startState) as TransitionViewState 459 val endState = state.copy().also { it.expansion = 1.0f } 460 val endViewState = obtainViewState(endState) as TransitionViewState 461 result = 462 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion) 463 } 464 if (state.squishFraction <= 1f) { 465 return squishViewState(result, state.squishFraction) 466 } 467 return result 468 } 469 470 private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { 471 result.apply { 472 heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 473 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 474 expansion = state.expansion 475 gutsVisible = guts 476 } 477 return result 478 } 479 480 /** 481 * Attach a view to this controller. This may perform measurements if it's not available yet and 482 * should therefore be done carefully. 483 */ 484 fun attach(transitionLayout: TransitionLayout, type: TYPE) = 485 traceSection("MediaViewController#attach") { 486 loadLayoutForType(type) 487 logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) 488 this.transitionLayout = transitionLayout 489 layoutController.attach(transitionLayout) 490 if (currentEndLocation == -1) { 491 return 492 } 493 // Set the previously set state immediately to the view, now that it's finally attached 494 setCurrentState( 495 startLocation = currentStartLocation, 496 endLocation = currentEndLocation, 497 transitionProgress = currentTransitionProgress, 498 applyImmediately = true 499 ) 500 } 501 502 /** 503 * Obtain a measurement for a given location. This makes sure that the state is up to date and 504 * all widgets know their location. Calling this method may create a measurement if we don't 505 * have a cached value available already. 506 */ 507 fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? = 508 traceSection("MediaViewController#getMeasurementsForState") { 509 // measurements should never factor in the squish fraction 510 val viewState = obtainViewState(hostState) ?: return null 511 measurement.measuredWidth = viewState.measureWidth 512 measurement.measuredHeight = viewState.measureHeight 513 return measurement 514 } 515 516 /** 517 * Set a new state for the controlled view which can be an interpolation between multiple 518 * locations. 519 */ 520 fun setCurrentState( 521 @MediaLocation startLocation: Int, 522 @MediaLocation endLocation: Int, 523 transitionProgress: Float, 524 applyImmediately: Boolean 525 ) = 526 traceSection("MediaViewController#setCurrentState") { 527 currentEndLocation = endLocation 528 currentStartLocation = startLocation 529 currentTransitionProgress = transitionProgress 530 logger.logMediaLocation("setCurrentState", startLocation, endLocation) 531 532 val shouldAnimate = animateNextStateChange && !applyImmediately 533 534 val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return 535 val startHostState = mediaHostStatesManager.mediaHostStates[startLocation] 536 537 // Obtain the view state that we'd want to be at the end 538 // The view might not be bound yet or has never been measured and in that case will be 539 // reset once the state is fully available 540 var endViewState = obtainViewState(endHostState) ?: return 541 endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!! 542 layoutController.setMeasureState(endViewState) 543 544 // If the view isn't bound, we can drop the animation, otherwise we'll execute it 545 animateNextStateChange = false 546 if (transitionLayout == null) { 547 return 548 } 549 550 val result: TransitionViewState 551 var startViewState = obtainViewState(startHostState) 552 startViewState = updateViewStateSize(startViewState, startLocation, tmpState3) 553 554 if (!endHostState.visible) { 555 // Let's handle the case where the end is gone first. In this case we take the 556 // start viewState and will make it gone 557 if (startViewState == null || startHostState == null || !startHostState.visible) { 558 // the start isn't a valid state, let's use the endstate directly 559 result = endViewState 560 } else { 561 // Let's get the gone presentation from the start state 562 result = 563 layoutController.getGoneState( 564 startViewState, 565 startHostState.disappearParameters, 566 transitionProgress, 567 tmpState 568 ) 569 } 570 } else if (startHostState != null && !startHostState.visible) { 571 // We have a start state and it is gone. 572 // Let's get presentation from the endState 573 result = 574 layoutController.getGoneState( 575 endViewState, 576 endHostState.disappearParameters, 577 1.0f - transitionProgress, 578 tmpState 579 ) 580 } else if (transitionProgress == 1.0f || startViewState == null) { 581 // We're at the end. Let's use that state 582 result = endViewState 583 } else if (transitionProgress == 0.0f) { 584 // We're at the start. Let's use that state 585 result = startViewState 586 } else { 587 result = 588 layoutController.getInterpolatedState( 589 startViewState, 590 endViewState, 591 transitionProgress, 592 tmpState 593 ) 594 } 595 logger.logMediaSize( 596 "setCurrentState (progress $transitionProgress)", 597 result.width, 598 result.height 599 ) 600 layoutController.setState( 601 result, 602 applyImmediately, 603 shouldAnimate, 604 animationDuration, 605 animationDelay 606 ) 607 } 608 609 private fun updateViewStateSize( 610 viewState: TransitionViewState?, 611 location: Int, 612 outState: TransitionViewState 613 ): TransitionViewState? { 614 var result = viewState?.copy(outState) ?: return null 615 val state = mediaHostStatesManager.mediaHostStates[location] 616 val overrideSize = mediaHostStatesManager.carouselSizes[location] 617 var overridden = false 618 overrideSize?.let { 619 // To be safe we're using a maximum here. The override size should always be set 620 // properly though. 621 if ( 622 result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth 623 ) { 624 result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) 625 result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) 626 // The measureHeight and the shown height should both be set to the overridden 627 // height 628 result.height = result.measureHeight 629 result.width = result.measureWidth 630 // Make sure all background views are also resized such that their size is correct 631 MediaViewHolder.backgroundIds.forEach { id -> 632 result.widgetStates.get(id)?.let { state -> 633 state.height = result.height 634 state.width = result.width 635 } 636 } 637 overridden = true 638 } 639 } 640 if (overridden && state != null && state.squishFraction <= 1f) { 641 // Let's squish the media player if our size was overridden 642 result = squishViewState(result, state.squishFraction) 643 } 644 logger.logMediaSize("update to carousel", result.width, result.height) 645 return result 646 } 647 648 private fun loadLayoutForType(type: TYPE) { 649 this.type = type 650 651 // These XML resources contain ConstraintSets that will apply to this player type's layout 652 when (type) { 653 TYPE.PLAYER -> { 654 collapsedLayout.load(context, R.xml.media_session_collapsed) 655 expandedLayout.load(context, R.xml.media_session_expanded) 656 } 657 TYPE.RECOMMENDATION -> { 658 collapsedLayout.load(context, R.xml.media_recommendations_collapsed) 659 expandedLayout.load(context, R.xml.media_recommendations_expanded) 660 } 661 } 662 refreshState() 663 } 664 665 /** 666 * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event 667 * of [location] not being visible, [locationWhenHidden] will be used instead. 668 * 669 * @param location Target 670 * @param locationWhenHidden Location that will be used when the target is not 671 * [MediaHost.visible] 672 * @return State require for executing a transition, and also the respective [MediaHost]. 673 */ 674 private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { 675 val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null 676 val viewState = obtainViewState(mediaHostState) 677 if (viewState != null) { 678 // update the size of the viewstate for the location with the override 679 updateViewStateSize(viewState, location, tmpState) 680 return tmpState 681 } 682 return viewState 683 } 684 685 /** 686 * Notify that the location is changing right now and a [setCurrentState] change is imminent. 687 * This updates the width the view will me measured with. 688 */ 689 fun onLocationPreChange(@MediaLocation newLocation: Int) { 690 obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) } 691 } 692 693 /** Request that the next state change should be animated with the given parameters. */ 694 fun animatePendingStateChange(duration: Long, delay: Long) { 695 animateNextStateChange = true 696 animationDuration = duration 697 animationDelay = delay 698 } 699 700 /** Clear all existing measurements and refresh the state to match the view. */ 701 fun refreshState() = 702 traceSection("MediaViewController#refreshState") { 703 // Let's clear all of our measurements and recreate them! 704 viewStates.clear() 705 if (firstRefresh) { 706 // This is the first bind, let's ensure we pre-cache all measurements. Otherwise 707 // We'll just load these on demand. 708 ensureAllMeasurements() 709 firstRefresh = false 710 } 711 setCurrentState( 712 currentStartLocation, 713 currentEndLocation, 714 currentTransitionProgress, 715 applyImmediately = true 716 ) 717 } 718 } 719 720 /** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */ 721 private data class CacheKey( 722 var widthMeasureSpec: Int = -1, 723 var heightMeasureSpec: Int = -1, 724 var expansion: Float = 0.0f, 725 var gutsVisible: Boolean = false 726 ) 727