summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/bg_smartspace_media_item.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHost.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSFragment.java9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt25
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt188
8 files changed, 387 insertions, 34 deletions
diff --git a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
index 69390848245d..33c68bf1f6ac 100644
--- a/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
+++ b/packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
@@ -16,6 +16,6 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
- <solid android:color="@android:color/white" />
+ <solid android:color="@android:color/transparent" />
<corners android:radius="@dimen/qs_media_album_radius" />
</shape> \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 8368792b8ae3..b1f794c8905e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -11,6 +11,7 @@ import android.util.MathUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.PathInterpolator
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.InstanceId
@@ -95,7 +96,8 @@ class MediaCarouselController @Inject constructor(
* finished
*/
@MediaLocation
- private var currentEndLocation: Int = -1
+ @VisibleForTesting
+ var currentEndLocation: Int = -1
/**
* The ending location of the view where it ends when all animations and transitions have
@@ -126,7 +128,8 @@ class MediaCarouselController @Inject constructor(
lateinit var settingsButton: View
private set
private val mediaContent: ViewGroup
- private val pageIndicator: PageIndicator
+ @VisibleForTesting
+ val pageIndicator: PageIndicator
private val visualStabilityCallback: OnReorderingAllowedListener
private var needsReordering: Boolean = false
private var keysNeedRemoval = mutableSetOf<String>()
@@ -149,6 +152,27 @@ class MediaCarouselController @Inject constructor(
}
}
}
+
+ companion object {
+ const val ANIMATION_BASE_DURATION = 2200f
+ const val DURATION = 167f
+ const val DETAILS_DELAY = 1067f
+ const val CONTROLS_DELAY = 1400f
+ const val PAGINATION_DELAY = 1900f
+ const val MEDIATITLES_DELAY = 1000f
+ const val MEDIACONTAINERS_DELAY = 967f
+ val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F)
+ val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F)
+
+ fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
+ val transformStartFraction = delay / ANIMATION_BASE_DURATION
+ val transformDurationFraction = duration / ANIMATION_BASE_DURATION
+ val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
+ return MathUtils.constrain((squishinessToTime - transformStartFraction) /
+ transformDurationFraction, 0F, 1F)
+ }
+ }
+
private val configListener = object : ConfigurationController.ConfigurationListener {
override fun onDensityOrFontScaleChanged() {
// System font changes should only happen when UMO is offscreen or a flicker may occur
@@ -633,12 +657,17 @@ class MediaCarouselController @Inject constructor(
}
}
- private fun updatePageIndicatorAlpha() {
+ @VisibleForTesting
+ fun updatePageIndicatorAlpha() {
val hostStates = mediaHostStatesManager.mediaHostStates
val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
val startAlpha = if (startIsVisible) 1.0f else 0.0f
- val endAlpha = if (endIsVisible) 1.0f else 0.0f
+ // when squishing in split shade, only use endState, which keeps changing
+ // to provide squishFraction
+ val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
+ val endAlpha = (if (endIsVisible) 1.0f else 0.0f) *
+ calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
var alpha = 1.0f
if (!endIsVisible || !startIsVisible) {
var progress = currentTransitionProgress
@@ -687,6 +716,7 @@ class MediaCarouselController @Inject constructor(
mediaCarouselScrollHandler.setCarouselBounds(
currentCarouselWidth, currentCarouselHeight)
updatePageIndicatorLocation()
+ updatePageIndicatorAlpha()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index bffb0fdec707..864592238b73 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -203,6 +203,14 @@ class MediaHost constructor(
}
}
+ override var squishFraction: Float = 1.0f
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
override var showsOnlyActiveMedia: Boolean = false
set(value) {
if (!value.equals(field)) {
@@ -253,6 +261,7 @@ class MediaHost constructor(
override fun copy(): MediaHostState {
val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion
+ mediaHostState.squishFraction = squishFraction
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible
@@ -271,6 +280,9 @@ class MediaHost constructor(
if (expansion != other.expansion) {
return false
}
+ if (squishFraction != other.squishFraction) {
+ return false
+ }
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
return false
}
@@ -289,6 +301,7 @@ class MediaHost constructor(
override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode()
+ result = 31 * result + squishFraction.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2
@@ -329,6 +342,11 @@ interface MediaHostState {
var expansion: Float
/**
+ * Fraction of the height animation.
+ */
+ var squishFraction: Float
+
+ /**
* Is this host only showing active media or is it showing all of them including resumption?
*/
var showsOnlyActiveMedia: Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index ac59175d4646..faa7aaee3c9a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -18,8 +18,15 @@ package com.android.systemui.media
import android.content.Context
import android.content.res.Configuration
+import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.TransitionLayout
@@ -50,6 +57,24 @@ class MediaViewController @Inject constructor(
companion object {
@JvmField
val GUTS_ANIMATION_DURATION = 500L
+ val controlIds = setOf(
+ R.id.media_progress_bar,
+ R.id.actionNext,
+ R.id.actionPrev,
+ R.id.action0,
+ R.id.action1,
+ R.id.action2,
+ R.id.action3,
+ R.id.action4,
+ R.id.media_scrubbing_elapsed_time,
+ R.id.media_scrubbing_total_time
+ )
+
+ val detailIds = setOf(
+ R.id.header_title,
+ R.id.header_artist,
+ R.id.actionPlayPause,
+ )
}
/**
@@ -57,6 +82,7 @@ class MediaViewController @Inject constructor(
*/
lateinit var sizeChangedListener: () -> Unit
private var firstRefresh: Boolean = true
+ @VisibleForTesting
private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController()
private var animationDelay: Long = 0
@@ -279,10 +305,47 @@ class MediaViewController @Inject constructor(
}
/**
+ * Apply squishFraction to a copy of viewState such that the cached version is untouched.
+ */
+ internal fun squishViewState(
+ viewState: TransitionViewState,
+ squishFraction: Float
+ ): TransitionViewState {
+ val squishedViewState = viewState.copy()
+ squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+ controlIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+ }
+ }
+
+ detailIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaContainersIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+ }
+ }
+
+ RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+ squishedViewState.widgetStates.get(id)?.let { state ->
+ state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+ }
+ }
+
+ return squishedViewState
+ }
+
+ /**
* Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive.
*/
- private fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+ @VisibleForTesting
+ fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) {
return null
}
@@ -291,41 +354,46 @@ class MediaViewController @Inject constructor(
val viewState = viewStates[cacheKey]
if (viewState != null) {
// we already have cached this measurement, let's continue
+ if (state.squishFraction <= 1f) {
+ return squishViewState(viewState, state.squishFraction)
+ }
return viewState
}
// Copy the key since this might call recursively into it and we're using tmpKey
cacheKey = cacheKey.copy()
val result: TransitionViewState?
- if (transitionLayout != null) {
- // Let's create a new measurement
- if (state.expansion == 0.0f || state.expansion == 1.0f) {
- result = transitionLayout!!.calculateViewState(
- state.measurementInput!!,
- constraintSetForExpansion(state.expansion),
- TransitionViewState())
-
- setGutsViewState(result)
- // We don't want to cache interpolated or null states as this could quickly fill up
- // our cache. We only cache the start and the end states since the interpolation
- // is cheap
- viewStates[cacheKey] = result
- } else {
- // This is an interpolated state
- val startState = state.copy().also { it.expansion = 0.0f }
-
- // Given that we have a measurement and a view, let's get (guaranteed) viewstates
- // from the start and end state and interpolate them
- val startViewState = obtainViewState(startState) as TransitionViewState
- val endState = state.copy().also { it.expansion = 1.0f }
- val endViewState = obtainViewState(endState) as TransitionViewState
- result = layoutController.getInterpolatedState(
- startViewState,
- endViewState,
- state.expansion)
- }
+ if (transitionLayout == null) {
+ return null
+ }
+ // Let's create a new measurement
+ if (state.expansion == 0.0f || state.expansion == 1.0f) {
+ result = transitionLayout!!.calculateViewState(
+ state.measurementInput!!,
+ constraintSetForExpansion(state.expansion),
+ TransitionViewState())
+
+ setGutsViewState(result)
+ // We don't want to cache interpolated or null states as this could quickly fill up
+ // our cache. We only cache the start and the end states since the interpolation
+ // is cheap
+ viewStates[cacheKey] = result
} else {
- result = null
+ // This is an interpolated state
+ val startState = state.copy().also { it.expansion = 0.0f }
+
+ // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+ // from the start and end state and interpolate them
+ val startViewState = obtainViewState(startState) as TransitionViewState
+ val endState = state.copy().also { it.expansion = 1.0f }
+ val endViewState = obtainViewState(endState) as TransitionViewState
+ result = layoutController.getInterpolatedState(
+ startViewState,
+ endViewState,
+ state.expansion)
+ }
+ if (state.squishFraction <= 1f) {
+ return squishViewState(result, state.squishFraction)
}
return result
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
index 52ac4e0682a3..8ae75fc34acb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
@@ -106,5 +106,20 @@ class RecommendationViewHolder private constructor(itemView: View) {
R.id.media_subtitle2,
R.id.media_subtitle3
)
+
+ val mediaTitlesAndSubtitlesIds = setOf(
+ R.id.media_title1,
+ R.id.media_title2,
+ R.id.media_title3,
+ R.id.media_subtitle1,
+ R.id.media_subtitle2,
+ R.id.media_subtitle3
+ )
+
+ val mediaContainersIds = setOf(
+ R.id.media_cover1_container,
+ R.id.media_cover2_container,
+ R.id.media_cover3_container
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 7b27cf45979f..9f4c707866a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -691,6 +691,15 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (mQSAnimator != null) {
mQSAnimator.setPosition(expansion);
}
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+ || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
+ // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
+ // and media player expect no change by squishiness in lock screen shade
+ mQsMediaHost.setSquishFraction(1.0F);
+ } else {
+ mQsMediaHost.setSquishFraction(mSquishinessFraction);
+ }
+
}
private void setAlphaAnimationProgress(float progress) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 5ad354247a04..f34c2ac57a5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -25,6 +25,11 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
@@ -398,4 +403,24 @@ class MediaCarouselControllerTest : SysuiTestCase() {
// added to the end because it was active less recently.
assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
}
+
+ @Test
+ fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
+ val delta = 0.0001F
+ val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
+ (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
+ val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
+ (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ whenever(mediaHostStatesManager.mediaHostStates)
+ .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
+ whenever(mediaHostState.visible).thenReturn(true)
+ mediaCarouselController.currentEndLocation = LOCATION_QS
+ whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+ mediaCarouselController.updatePageIndicatorAlpha()
+ assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+
+ whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+ mediaCarouselController.updatePageIndicatorAlpha()
+ assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
new file mode 100644
index 000000000000..622a512720d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.WidgetState
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.floatThat
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaViewControllerTest : SysuiTestCase() {
+ private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
+ private val mediaHostStatesManager = MediaHostStatesManager()
+ private val configurationController =
+ com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
+ private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+ @Mock lateinit var logger: MediaViewLogger
+ @Mock private lateinit var mockViewState: TransitionViewState
+ @Mock private lateinit var mockCopiedState: TransitionViewState
+ @Mock private lateinit var detailWidgetState: WidgetState
+ @Mock private lateinit var controlWidgetState: WidgetState
+ @Mock private lateinit var mediaTitleWidgetState: WidgetState
+ @Mock private lateinit var mediaContainerWidgetState: WidgetState
+
+ val delta = 0.0001F
+
+ private lateinit var mediaViewController: MediaViewController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mediaViewController =
+ MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
+ mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+ player.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
+ mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
+ recommendation.measureState = TransitionViewState().apply { this.height = 100 }
+ mediaHostStateHolder.expansion = 1f
+ val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ mediaHostStateHolder.measurementInput =
+ MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+ // Test no squish
+ mediaHostStateHolder.squishFraction = 1f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+ // Test half squish
+ mediaHostStateHolder.squishFraction = 0.5f
+ assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_progress_bar to controlWidgetState,
+ R.id.header_artist to detailWidgetState
+ )
+ )
+
+ val detailSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val detailSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+ verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val controlSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val controlSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+ mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+ verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+
+ @Test
+ fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
+ whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+ whenever(mockCopiedState.widgetStates)
+ .thenReturn(
+ mutableMapOf(
+ R.id.media_title1 to mediaTitleWidgetState,
+ R.id.media_cover1_container to mediaContainerWidgetState
+ )
+ )
+
+ val containerSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val containerSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+ verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+ val titleSquishMiddle =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+ val titleSquishEnd =
+ TRANSFORM_BEZIER.getInterpolation(
+ (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
+ )
+ mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+ verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+ }
+}