Merge "Make UMO squishy and add motion to widgets only for split shade" into tm-qpr-dev

This commit is contained in:
Sherry Zhou
2022-10-13 21:36:02 +00:00
committed by Android (Google) Code Review
8 changed files with 386 additions and 33 deletions

View File

@@ -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>

View File

@@ -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()
}
}

View File

@@ -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
@@ -328,6 +341,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?
*/

View File

@@ -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
@@ -278,11 +304,48 @@ 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())
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 {
// 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)
}
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
}

View File

@@ -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
)
}
}

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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 }
}
}