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