Measuring the Media Views now properly the first time its created
Instead of only knowing the measurement when its attached, we need
to know before otherwise the layout will be broken
Bug: 154137987
Test: atest SystemUITests
Change-Id: I8a4d90a7d933f477df65a5f26942059b0f0920da
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 0b0bfc6..3d638dd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -112,6 +112,7 @@
private int mAlbumArtSize;
private int mAlbumArtRadius;
private int mViewWidth;
+ private MediaMeasurementInput mLastMeasureInput;
public static final String MEDIA_PREFERENCES = "media_control_prefs";
public static final String MEDIA_PREFERENCE_KEY = "browser_components";
@@ -737,31 +738,31 @@
*/
protected void removePlayer() { }
- public void setDimension(int newWidth, int newHeight, boolean animate, long duration,
+ public void remeasure(@Nullable MediaMeasurementInput input, boolean animate, long duration,
long startDelay) {
// Let's remeasure if our width changed. Our height is dependent on the expansion, so we
// won't animate if it changed
- if (newWidth != mViewWidth) {
+ if (input != null && !input.sameAs(mLastMeasureInput)) {
+ mLastMeasureInput = input;
if (animate) {
mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
}
- setViewWidth(newWidth);
- mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight());
+ remeasureInternal(input);
+ mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(),
+ mMediaNotifView.getMeasuredHeight());
}
}
- protected void setViewWidth(int newWidth) {
+ public MediaMeasurementInput getLastMeasureInput() {
+ return mLastMeasureInput;
+ }
+
+ private void remeasureInternal(MediaMeasurementInput input) {
ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
- collapsedSet.setGuidelineBegin(R.id.view_width, newWidth);
- expandedSet.setGuidelineBegin(R.id.view_width, newWidth);
- DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
- int widthSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
- View.MeasureSpec.AT_MOST);
- int heightSpec = View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
- View.MeasureSpec.AT_MOST);
- mMediaNotifView.setMinimumWidth(displayMetrics.widthPixels);
- mMediaNotifView.measure(widthSpec, heightSpec);
- mViewWidth = newWidth;
+ int width = input.getWidth();
+ collapsedSet.setGuidelineBegin(R.id.view_width, width);
+ expandedSet.setGuidelineBegin(R.id.view_width, width);
+ mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index fc141bf..26a6104 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -31,7 +31,7 @@
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.animation.UniqueObjectHost
+import com.android.systemui.util.animation.UniqueObjectHostView
import javax.inject.Inject
import javax.inject.Singleton
@@ -45,7 +45,8 @@
private val statusBarStateController: SysuiStatusBarStateController,
private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController,
- private val mediaViewManager: MediaViewManager
+ private val mediaViewManager: MediaViewManager,
+ private val mediaMeasurementProvider: MediaMeasurementManager
) {
private var rootOverlay: ViewGroupOverlay? = null
private lateinit var currentState: MediaState
@@ -108,7 +109,7 @@
* @return the hostView associated with this location
*/
fun register(mediaObject: MediaHost) : ViewGroup {
- val viewHost = createUniqueObjectHost()
+ val viewHost = createUniqueObjectHost(mediaObject)
mediaObject.hostView = viewHost;
mediaHosts[mediaObject.location] = mediaObject
if (mediaObject.location == desiredLocation) {
@@ -123,8 +124,10 @@
return viewHost
}
- private fun createUniqueObjectHost(): UniqueObjectHost {
- val viewHost = UniqueObjectHost(context)
+ private fun createUniqueObjectHost(host: MediaState): UniqueObjectHostView {
+ val viewHost = UniqueObjectHostView(context)
+ viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
+
viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View?) {
if (rootOverlay == null) {
@@ -148,12 +151,16 @@
val isNewView = this.desiredLocation == -1
this.desiredLocation = desiredLocation
// Let's perform a transition
- performTransition(applyImmediately = isNewView)
+ val animate = shouldAnimateTransition(desiredLocation, previousLocation)
+ val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+ mediaViewManager.onDesiredStateChanged(getHost(desiredLocation)?.currentState,
+ animate, animDuration, delay)
+ performTransitionToNewLocation(isNewView, animate)
}
}
- private fun performTransition(applyImmediately: Boolean) {
- if (previousLocation < 0 || applyImmediately) {
+ private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
+ if (previousLocation < 0 || isNewView) {
cancelAnimationAndApplyDesiredState()
return
}
@@ -161,29 +168,27 @@
val previousHost = getHost(previousLocation)
if (currentHost == null || previousHost == null) {
cancelAnimationAndApplyDesiredState()
- return;
}
updateTargetState()
- var animate = false
if (isCurrentlyInGuidedTransformation()) {
applyTargetStateIfNotAnimating()
- } else if (shouldAnimateTransition(currentHost, previousHost)) {
+ } else if (animate) {
animator.cancel()
// Let's animate to the new position, starting from the current position
animationStartState = currentState.copy()
- adjustAnimatorForTransition(previousLocation, desiredLocation)
+ adjustAnimatorForTransition(desiredLocation, previousLocation)
animator.start()
- animate = true
} else {
cancelAnimationAndApplyDesiredState()
}
- mediaViewManager.performTransition(targetState, animate, animator.duration,
- animator.startDelay)
}
- private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean {
- if (currentHost.location == LOCATION_QQS
- && previousHost.location == LOCATION_LOCKSCREEN
+ private fun shouldAnimateTransition(
+ @MediaLocation currentLocation: Int,
+ @MediaLocation previousLocation: Int
+ ): Boolean {
+ if (currentLocation == LOCATION_QQS
+ && previousLocation == LOCATION_LOCKSCREEN
&& (statusBarStateController.leaveOpenOnKeyguardHide()
|| statusBarStateController.state == StatusBarState.SHADE_LOCKED)) {
// Usually listening to the isShown is enough to determine this, but there is some
@@ -193,7 +198,16 @@
return mediaCarousel.isShown || animator.isRunning
}
- private fun adjustAnimatorForTransition(previousLocation: Int, desiredLocation: Int) {
+ private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
+ val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+ animator.apply {
+ duration = animDuration
+ startDelay = delay
+ }
+
+ }
+
+ private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
var animDuration = 200L
var delay = 0L
if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
@@ -206,11 +220,7 @@
} else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
}
- animator.apply {
- duration = animDuration
- startDelay = delay
- }
-
+ return animDuration to delay
}
private fun applyTargetStateIfNotAnimating() {
@@ -290,7 +300,7 @@
boundsOnScreen.bottom)
}
currentState = state.copy()
- mediaViewManager.applyState(currentState)
+ mediaViewManager.setCurrentState(currentState)
}
private fun updateHostAttachment() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 12e2743..80d00e2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -6,6 +6,8 @@
import android.view.View.OnAttachStateChangeListener
import android.view.ViewGroup
import com.android.systemui.media.MediaHierarchyManager.MediaLocation
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementInputData
import javax.inject.Inject
class MediaHost @Inject constructor(
@@ -86,6 +88,7 @@
}
class MediaHostState @Inject constructor() : MediaState {
+ var measurementInput: MediaMeasurementInput? = null
override var expansion: Float = 0.0f
override var showsOnlyActiveMedia: Boolean = false
override val boundsOnScreen: Rect = Rect()
@@ -95,6 +98,7 @@
mediaHostState.expansion = expansion
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.boundsOnScreen.set(boundsOnScreen)
+ mediaHostState.measurementInput = measurementInput
return mediaHostState
}
@@ -111,8 +115,20 @@
other.boundsOnScreen.bottom.toFloat(), amount).toInt()
result.boundsOnScreen.set(left, top, right, bottom)
result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
+ if (amount > 0.0f) {
+ if (other is MediaHostState) {
+ result.measurementInput = other.measurementInput
+ }
+ } else {
+ result.measurementInput
+ }
return result
}
+
+ override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput {
+ measurementInput = MediaMeasurementInput(input, expansion)
+ return measurementInput as MediaMeasurementInput
+ }
}
}
@@ -122,4 +138,20 @@
val boundsOnScreen: Rect
fun copy() : MediaState
fun interpolate(other: MediaState, amount: Float) : MediaState
-}
\ No newline at end of file
+ fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput
+}
+/**
+ * The measurement input for a Media View
+ */
+data class MediaMeasurementInput(
+ private val viewInput: MeasurementInput,
+ val expansion: Float) : MeasurementInput by viewInput {
+
+ override fun sameAs(input: MeasurementInput?): Boolean {
+ if (!(input is MediaMeasurementInput)) {
+ return false
+ }
+ return width == input.width && expansion == input.expansion
+ }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
new file mode 100644
index 0000000..4bbf5eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 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 com.android.systemui.util.animation.BaseMeasurementCache
+import com.android.systemui.util.animation.GuaranteedMeasurementCache
+import com.android.systemui.util.animation.MeasurementCache
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementOutput
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * A class responsible creating measurement caches for media hosts which also coordinates with
+ * the view manager to obtain sizes for unknown measurement inputs.
+ */
+@Singleton
+class MediaMeasurementManager @Inject constructor(
+ private val mediaViewManager: MediaViewManager
+) {
+ private val baseCache: MeasurementCache
+
+ init {
+ baseCache = BaseMeasurementCache()
+ }
+
+ private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
+ return mediaViewManager.obtainMeasurement(input)
+ }
+
+ /**
+ * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that
+ * requesting any size from the cache will always return the correct value.
+ */
+ fun obtainCache(host: MediaState): GuaranteedMeasurementCache {
+ val remapper = { input: MeasurementInput ->
+ host.getMeasuringInput(input)
+ }
+ val provider = { input: MeasurementInput ->
+ provideMeasurement(input as MediaMeasurementInput)
+ }
+ return GuaranteedMeasurementCache(baseCache, remapper, provider)
+ }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 636e4e4..6dd9787 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -4,7 +4,6 @@
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
-
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager
import com.android.settingslib.media.LocalMediaManager
@@ -13,7 +12,8 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.VisualStabilityManager
-import com.android.systemui.util.animation.UniqueObjectHost
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -33,12 +33,14 @@
private val activityStarter: ActivityStarter,
mediaManager: MediaDataManager
) {
+ private var desiredState: MediaHost.MediaHostState? = null
+ private var currentState: MediaState? = null
val mediaCarousel: ViewGroup
private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val visualStabilityCallback = ::reorderAllPlayers
- private var viewsExpanded = true
+ private var currentlyExpanded = true
set(value) {
if (field != value) {
field = value
@@ -68,7 +70,7 @@
private fun inflateMediaCarousel(): ViewGroup {
return LayoutInflater.from(context).inflate(
- R.layout.media_carousel, UniqueObjectHost(context), false) as ViewGroup
+ R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
}
private fun reorderAllPlayers() {
@@ -98,7 +100,7 @@
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
existingPlayer.view.setLayoutParams(lp)
- existingPlayer.setListening(viewsExpanded)
+ existingPlayer.setListening(currentlyExpanded)
if (existingPlayer.isPlaying) {
mediaContent.addView(existingPlayer.view, 0)
} else {
@@ -132,32 +134,48 @@
}
- fun applyState(state: MediaState) {
+ fun setCurrentState(state: MediaState) {
+ currentState = state
+ currentlyExpanded = state.expansion > 0
for (mediaPlayer in mediaPlayers.values) {
val view = mediaPlayer.view
view.progress = state.expansion
}
- viewsExpanded = state.expansion > 0;
}
/**
* @param targetState the target state we're transitioning to
* @param animate should this be animated
*/
- fun performTransition(targetState: MediaState?, animate: Boolean, duration: Long,
- startDelay: Long) {
- if (targetState == null) {
- return
+ fun onDesiredStateChanged(targetState: MediaState?, animate: Boolean, duration: Long,
+ startDelay: Long) {
+ if (targetState is MediaHost.MediaHostState) {
+ // This is a hosting view, let's remeasure our players
+ desiredState = targetState
+ val measurementInput = targetState.measurementInput
+ for (mediaPlayer in mediaPlayers.values) {
+ mediaPlayer.remeasure(measurementInput, animate, duration, startDelay)
+ }
}
- val newWidth = targetState.boundsOnScreen.width()
- val newHeight = targetState.boundsOnScreen.height()
- remeasureViews(newWidth, newHeight, animate, duration, startDelay)
}
- private fun remeasureViews(newWidth: Int, newHeight: Int, animate: Boolean, duration: Long,
- startDelay: Long) {
- for (mediaPlayer in mediaPlayers.values) {
- mediaPlayer.setDimension(newWidth, newHeight, animate, duration, startDelay)
+ /**
+ * Get a measurement for the given input state. This measures the first player and returns
+ * its bounds as if it were measured with the given measurement dimensions
+ */
+ fun obtainMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
+ val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
+ // Let's measure the size of the first player and return its height
+ val previousProgress = firstPlayer.view.progress
+ firstPlayer.view.progress = input.expansion
+ firstPlayer.remeasure(input, false /* animate */, 0, 0)
+ val result = MeasurementOutput(firstPlayer.view.measuredWidth,
+ firstPlayer.view.measuredHeight)
+ firstPlayer.view.progress = previousProgress
+ if (desiredState != null) {
+ // remeasure it to the old size again!
+ firstPlayer.remeasure(desiredState!!.measurementInput, false, 0, 0)
}
+ return result
}
}
\ No newline at end of file