Make media squishy for portrait shade open and close

Propagate squishFraction from QSFragment to MediaHostState.

In MediaViewController, apply squishFraction to both
cached and uncached UMO viewStates.

Remove mTopPadding (qsHeight / space above lockscreen notifs)
from expansionFraction (aka squishFraction) calculations to avoid
the following recursion:
=> squishFraction changes UMO height
=> new UMO height changes mTopPadding
=> mTopPadding changes total stack height
=> new stack height changes squishFraction

Use expandedFraction calculated in PVC instead of recalculating it
in NSSL.

Bug: 217583474
Test: MediaViewControllerTest
Test: open/close portrait shade => squishy UMO

Change-Id: Ifdada015563394a96c3d80ae527c68ac7323d4c2
This commit is contained in:
Lyn Han
2022-02-25 05:48:45 -06:00
parent 26b0f4c136
commit a24659d135
7 changed files with 128 additions and 43 deletions

View File

@@ -192,6 +192,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 override var showsOnlyActiveMedia: Boolean = false
set(value) { set(value) {
if (!value.equals(field)) { if (!value.equals(field)) {
@@ -242,6 +250,7 @@ class MediaHost constructor(
override fun copy(): MediaHostState { override fun copy(): MediaHostState {
val mediaHostState = MediaHostStateHolder() val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion mediaHostState.expansion = expansion
mediaHostState.squishFraction = squishFraction
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.measurementInput = measurementInput?.copy() mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible mediaHostState.visible = visible
@@ -260,6 +269,9 @@ class MediaHost constructor(
if (expansion != other.expansion) { if (expansion != other.expansion) {
return false return false
} }
if (squishFraction != other.squishFraction) {
return false
}
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) { if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
return false return false
} }
@@ -278,6 +290,7 @@ class MediaHost constructor(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0 var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode() result = 31 * result + expansion.hashCode()
result = 31 * result + squishFraction.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode() result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2 result = 31 * result + if (visible) 1 else 2
@@ -317,6 +330,11 @@ interface MediaHostState {
*/ */
var expansion: Float 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? * Is this host only showing active media or is it showing all of them including resumption?
*/ */

View File

@@ -18,13 +18,11 @@ package com.android.systemui.media
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.*
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionLayoutController
import com.android.systemui.util.animation.TransitionViewState
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -270,7 +268,6 @@ class MediaViewController @Inject constructor(
TYPE.PLAYER_SESSION -> PlayerSessionViewHolder.gutsIds TYPE.PLAYER_SESSION -> PlayerSessionViewHolder.gutsIds
TYPE.RECOMMENDATION -> RecommendationViewHolder.gutsIds TYPE.RECOMMENDATION -> RecommendationViewHolder.gutsIds
} }
controlsIds.forEach { id -> controlsIds.forEach { id ->
viewState.widgetStates.get(id)?.let { state -> viewState.widgetStates.get(id)?.let { state ->
// Make sure to use the unmodified state if guts are not visible. // Make sure to use the unmodified state if guts are not visible.
@@ -282,59 +279,79 @@ class MediaViewController @Inject constructor(
viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f
viewState.widgetStates.get(id)?.gone = !isGutsVisible viewState.widgetStates.get(id)?.gone = !isGutsVisible
} }
if (shouldHideGutsSettings) { if (shouldHideGutsSettings) {
viewState.widgetStates.get(R.id.settings)?.gone = true viewState.widgetStates.get(R.id.settings)?.gone = true
} }
} }
/**
* Apply squishFraction to a copy of viewState such that the cached version is untouched.
*/
private fun squishViewState(viewState: TransitionViewState,
squishFraction: Float): TransitionViewState {
val squishedViewState = viewState.copy()
squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
val albumArtViewState = viewState.widgetStates.get(R.id.album_art)
if (albumArtViewState != null) {
albumArtViewState.height = squishedViewState.height
}
return squishedViewState;
}
/** /**
* Obtain a new viewState for a given media state. This usually returns a cached state, but if * 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. * it's not available, it will recreate one by measuring, which may be expensive.
*/ */
private fun obtainViewState(state: MediaHostState?): TransitionViewState? { @VisibleForTesting
public fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) { if (state == null || state.measurementInput == null) {
return null return null
} }
// Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
var cacheKey = getKey(state, isGutsVisible, tmpKey) var cacheKey = getKey(state, isGutsVisible, tmpKey)
val viewState = viewStates[cacheKey] val viewState = viewStates[cacheKey]
if (viewState != null) { if (viewState != null) {
// we already have cached this measurement, let's continue // we already have cached this measurement, let's continue
if (state.squishFraction < 1f) {
return squishViewState(viewState, state.squishFraction);
}
return viewState return viewState
} }
// Copy the key since this might call recursively into it and we're using tmpKey // Copy the key since this might call recursively into it and we're using tmpKey
cacheKey = cacheKey.copy() cacheKey = cacheKey.copy()
val result: TransitionViewState? val result: TransitionViewState?
if (transitionLayout != null) { if (transitionLayout == null) {
// Let's create a new measurement return null
if (state.expansion == 0.0f || state.expansion == 1.0f) { }
result = transitionLayout!!.calculateViewState( // Not cached. Let's create a new measurement
state.measurementInput!!, if (state.expansion == 0.0f || state.expansion == 1.0f) {
constraintSetForExpansion(state.expansion), result = transitionLayout!!.calculateViewState(
TransitionViewState()) state.measurementInput!!,
constraintSetForExpansion(state.expansion),
setGutsViewState(result) TransitionViewState())
// We don't want to cache interpolated or null states as this could quickly fill up // 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 // our cache. We only cache the start and the end states since the interpolation
// is cheap // is cheap
viewStates[cacheKey] = result setGutsViewState(result)
} else { viewStates[cacheKey] = result
// 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)
}
} else { } 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 return result
} }

View File

@@ -568,6 +568,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (mQSAnimator != null) { if (mQSAnimator != null) {
mQSAnimator.setPosition(expansion); mQSAnimator.setPosition(expansion);
} }
mQqsMediaHost.setSquishFraction(mSquishinessFraction);
updateMediaPositions(); updateMediaPositions();
} }

View File

@@ -1337,12 +1337,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
*/ */
@ShadeViewRefactor(RefactorComponent.COORDINATOR) @ShadeViewRefactor(RefactorComponent.COORDINATOR)
public void setExpandedHeight(float height) { public void setExpandedHeight(float height) {
final float shadeBottom = getHeight() - getEmptyBottomMargin();
final boolean skipHeightUpdate = shouldSkipHeightUpdate(); final boolean skipHeightUpdate = shouldSkipHeightUpdate();
if (!skipHeightUpdate) {
final float expansionFraction = MathUtils.saturate(height / shadeBottom);
mAmbientState.setExpansionFraction(expansionFraction);
}
updateStackPosition(); updateStackPosition();
if (!skipHeightUpdate) { if (!skipHeightUpdate) {

View File

@@ -3140,9 +3140,7 @@ public class NotificationPanelViewController extends PanelViewController
} }
private int calculatePanelHeightShade() { private int calculatePanelHeightShade() {
int emptyBottomMargin = mNotificationStackScrollLayoutController.getEmptyBottomMargin(); final int maxHeight = mNotificationStackScrollLayoutController.getHeight();
int maxHeight = mNotificationStackScrollLayoutController.getHeight() - emptyBottomMargin;
if (mBarState == KEYGUARD) { if (mBarState == KEYGUARD) {
int minKeyguardPanelBottom = mClockPositionAlgorithm.getLockscreenStatusViewHeight() int minKeyguardPanelBottom = mClockPositionAlgorithm.getLockscreenStatusViewHeight()
+ mNotificationStackScrollLayoutController.getIntrinsicContentHeight(); + mNotificationStackScrollLayoutController.getIntrinsicContentHeight();

View File

@@ -793,6 +793,7 @@ public abstract class PanelViewController {
} }
mExpandedFraction = Math.min(1f, mExpandedFraction = Math.min(1f,
maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight); maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
mAmbientState.setExpansionFraction(mExpandedFraction);
onHeightUpdated(mExpandedHeight); onHeightUpdated(mExpandedHeight);
updatePanelExpansionAndVisibility(); updatePanelExpansionAndVisibility();
}); });

View File

@@ -0,0 +1,55 @@
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.SysuiTestCase
import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.animation.TransitionViewState
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* Tests for {@link MediaViewController}.
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class MediaViewControllerTest : SysuiTestCase() {
private val configurationController =
com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
private val mediaHostStatesManager = MediaHostStatesManager()
private val mediaViewController =
MediaViewController(context, configurationController, mediaHostStatesManager)
private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
private var transitionLayout = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
@Before
fun setUp() {
mediaViewController.attach(transitionLayout, MediaViewController.TYPE.PLAYER)
}
@Test
fun testObtainViewState_applySquishFraction_toTransitionViewState_height() {
transitionLayout.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)
}
}