diff options
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 } + } +} |