Handling multiple players better
Previously the animation was completely broken with multiple players
since they were all laid out at 0 instead of the actual position.
Measuring the full layout is a bit too expansive, so we're
introducing some workarounds to only measure the players.
Test: add multiple players, observe transitions
Bug: 154137987
Change-Id: I1c424c980cf3b64f5a9d63ba058aa7e47f6e4156
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index f888ca1..03e7467 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -16,7 +16,7 @@
-->
<!-- Carousel for media controls -->
-<HorizontalScrollView
+<com.android.systemui.media.UnboundHorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -34,4 +34,4 @@
>
<!-- QSMediaPlayers will be added here dynamically -->
</LinearLayout>
-</HorizontalScrollView>
+</com.android.systemui.media.UnboundHorizontalScrollView>
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 7228271..60c2ed2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -47,6 +47,7 @@
import android.widget.TextView;
import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import androidx.constraintlayout.motion.widget.Key;
import androidx.constraintlayout.motion.widget.KeyAttributes;
import androidx.constraintlayout.motion.widget.KeyFrames;
@@ -111,7 +112,6 @@
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";
@@ -188,7 +188,6 @@
mActivityStarter = activityStarter;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
mSeekBarObserver = new SeekBarObserver(getView());
- // TODO: we should pause this whenever the screen is off / panel is collapsed etc.
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
SeekBar bar = getView().findViewById(R.id.media_progress_bar);
bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
@@ -261,7 +260,7 @@
// Try to find a browser service component for this app
// TODO also check for a media button receiver intended for restarting (b/154127084)
// Only check if we haven't tried yet or the session token changed
- final String pkgName = mController.getPackageName();
+ final String pkgName = data.getPackageName();
if (mServiceComponent == null && !mCheckedForResumption) {
Log.d(TAG, "Checking for service component");
PackageManager pm = mContext.getPackageManager();
@@ -301,8 +300,7 @@
// App icon
ImageView appIcon = mMediaNotifView.requireViewById(R.id.icon);
- // TODO: look at iconDrawable
- Drawable iconDrawable = data.getAppIcon();
+ Drawable iconDrawable = data.getAppIcon().mutate();
iconDrawable.setTint(mForegroundColor);
appIcon.setImageDrawable(iconDrawable);
@@ -389,7 +387,7 @@
}
// Seek Bar
- final MediaController controller = new MediaController(getContext(), data.getToken());
+ final MediaController controller = getController();
mBackgroundExecutor.execute(
() -> mSeekBarViewModel.updateController(controller, data.getForegroundColor()));
@@ -397,10 +395,13 @@
// TODO: b/156036025 bring back media guts
makeActive();
+
+ // Update both constraint sets to regenerate the animation.
mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
mMediaNotifView.updateState(R.id.expanded, expandedSet);
}
+ @UiThread
private Drawable createRoundedBitmap(Icon icon) {
if (icon == null) {
return null;
@@ -746,27 +747,14 @@
*/
protected void removePlayer() { }
- 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 (input != null && !input.sameAs(mLastMeasureInput)) {
- mLastMeasureInput = input;
- if (animate) {
- mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
- }
- remeasureInternal(input);
- mMediaNotifView.layout(0, 0, mMediaNotifView.getMeasuredWidth(),
- mMediaNotifView.getMeasuredHeight());
+ public void measure(@Nullable MediaMeasurementInput input) {
+ if (input != null) {
+ int width = input.getWidth();
+ setPlayerWidth(width);
+ mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
}
}
- private void remeasureInternal(MediaMeasurementInput input) {
- int width = input.getWidth();
- setPlayerWidth(width);
- mMediaNotifView.measure(input.getWidthMeasureSpec(), input.getHeightMeasureSpec());
- }
-
public void setPlayerWidth(int width) {
ConstraintSet expandedSet = mMediaNotifView.getConstraintSet(R.id.expanded);
ConstraintSet collapsedSet = mMediaNotifView.getConstraintSet(R.id.collapsed);
@@ -775,4 +763,8 @@
mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
mMediaNotifView.updateState(R.id.expanded, expandedSet);
}
+
+ public void animatePendingSizeChange(long duration, long startDelay) {
+ mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index b4c0c33..6b1c520 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -48,6 +48,11 @@
private val mediaViewManager: MediaViewManager,
private val mediaMeasurementProvider: MediaMeasurementManager
) {
+ /**
+ * The root overlay of the hierarchy. This is where the media notification is attached to
+ * whenever the view is transitioning from one host to another. It also make sure that the
+ * view is always in its final state when it is attached to a view host.
+ */
private var rootOverlay: ViewGroupOverlay? = null
private lateinit var currentState: MediaState
private val mediaCarousel
@@ -79,9 +84,24 @@
}
private var targetState: MediaState? = null
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
- private var previousLocation = -1
- private var desiredLocation = -1
- private var currentAttachmentLocation = -1
+
+ /**
+ * The last location where this view was at before going to the desired location. This is
+ * useful for guided transitions.
+ */
+ @MediaLocation private var previousLocation = -1
+
+ /**
+ * The desired location where the view will be at the end of the transition.
+ */
+ @MediaLocation private var desiredLocation = -1
+
+ /**
+ * The current attachment location where the view is currently attached.
+ * Usually this matches the desired location except for animations whenever a view moves
+ * to the new desired location, during which it is in [IN_OVERLAY].
+ */
+ @MediaLocation private var currentAttachmentLocation = -1
var qsExpansion: Float = 0.0f
set(value) {
@@ -135,16 +155,12 @@
private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView {
val viewHost = UniqueObjectHostView(context)
viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
- viewHost.firstMeasureListener = { input ->
- if (host.location == currentAttachmentLocation) {
- // The first measurement of the attached view is happening, Let's make
- // sure the player width is updated
+ viewHost.onMeasureListener = { input ->
+ if (host.location == desiredLocation) {
+ // Measurement of the currently active player is happening, Let's make
+ // sure the player width is up to date
val measuringInput = host.getMeasuringInput(input)
- mediaViewManager.remeasureAllPlayers(
- measuringInput,
- animate = false,
- duration = 0,
- startDelay = 0)
+ mediaViewManager.setPlayerWidth(measuringInput.width)
}
}
@@ -162,6 +178,10 @@
return viewHost
}
+ /**
+ * Updates the location that the view should be in. If it changes, an animation may be triggered
+ * going from the old desired location to the new one.
+ */
private fun updateDesiredLocation() {
val desiredLocation = calculateLocation()
if (desiredLocation != this.desiredLocation) {
@@ -173,7 +193,7 @@
// Let's perform a transition
val animate = shouldAnimateTransition(desiredLocation, previousLocation)
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
- mediaViewManager.onDesiredStateChanged(getHost(desiredLocation)?.currentState,
+ mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState,
animate, animDuration, delay)
performTransitionToNewLocation(isNewView, animate)
}
@@ -262,6 +282,9 @@
}
}
+ /**
+ * Updates the state that the view wants to be in at the end of the animation.
+ */
private fun updateTargetState() {
if (isCurrentlyInGuidedTransformation()) {
val progress = getTransformationProgress()
@@ -320,17 +343,17 @@
}
private fun applyState(state: MediaState) {
+ currentState = state.copy()
+ mediaViewManager.setCurrentState(currentState)
updateHostAttachment()
- val boundsOnScreen = state.boundsOnScreen
if (currentAttachmentLocation == IN_OVERLAY) {
+ val boundsOnScreen = state.boundsOnScreen
mediaCarousel.setLeftTopRightBottom(
boundsOnScreen.left,
boundsOnScreen.top,
boundsOnScreen.right,
boundsOnScreen.bottom)
}
- currentState = state.copy()
- mediaViewManager.setCurrentState(currentState)
}
private fun updateHostAttachment() {
@@ -348,6 +371,7 @@
rootOverlay!!.add(mediaCarousel)
} else {
targetHost.addView(mediaCarousel)
+ mediaViewManager.onViewReattached()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index e1500ac..6e7b6bc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -7,7 +7,6 @@
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(
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index a364e6b..49d2d88 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -2,7 +2,9 @@
import android.content.Context
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
+import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.InfoMediaManager
@@ -33,12 +35,16 @@
private val activityStarter: ActivityStarter,
mediaManager: MediaDataManager
) {
+ private var playerWidth: Int = 0
+ private var playerWidthPlusPadding: Int = 0
private var desiredState: MediaHost.MediaHostState? = null
private var currentState: MediaState? = null
- val mediaCarousel: ViewGroup
+ val mediaCarousel: HorizontalScrollView
private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val visualStabilityCallback = ::reorderAllPlayers
+ private var activeMediaIndex: Int = 0
+ private var scrollIntoCurrentMedia: Int = 0
private var currentlyExpanded = true
set(value) {
@@ -49,29 +55,50 @@
}
}
}
+ private val scrollChangedListener = object : View.OnScrollChangeListener {
+ override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int,
+ oldScrollY: Int) {
+ if (playerWidthPlusPadding == 0) {
+ return
+ }
+ onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
+ scrollX % playerWidthPlusPadding)
+ }
+ }
init {
mediaCarousel = inflateMediaCarousel()
+ mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
mediaManager.addListener(object : MediaDataManager.Listener {
override fun onMediaDataLoaded(key: String, data: MediaData) {
updateView(key, data)
+ updatePlayerVisibilities()
}
override fun onMediaDataRemoved(key: String) {
val removed = mediaPlayers.remove(key)
removed?.apply {
+ val beforeActive = mediaContent.indexOfChild(removed.view) <= activeMediaIndex
mediaContent.removeView(removed.view)
removed.onDestroy()
updateMediaPaddings()
+ if (beforeActive) {
+ // also update the index here since the scroll below might not always lead
+ // to a scrolling changed
+ activeMediaIndex = Math.max(0, activeMediaIndex - 1)
+ mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX
+ - playerWidthPlusPadding, 0)
+ }
+ updatePlayerVisibilities()
}
}
})
}
- private fun inflateMediaCarousel(): ViewGroup {
- return LayoutInflater.from(context).inflate(
- R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
+ private fun inflateMediaCarousel(): HorizontalScrollView {
+ return LayoutInflater.from(context).inflate(R.layout.media_carousel,
+ UniqueObjectHostView(context), false) as HorizontalScrollView
}
private fun reorderAllPlayers() {
@@ -83,6 +110,26 @@
}
}
updateMediaPaddings()
+ updatePlayerVisibilities()
+ }
+
+ private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
+ val wasScrolledIn = scrollIntoCurrentMedia != 0
+ scrollIntoCurrentMedia = scrollInAmount
+ val nowScrolledIn = scrollIntoCurrentMedia != 0
+ if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
+ activeMediaIndex = newIndex
+ updatePlayerVisibilities()
+ }
+ }
+
+ private fun updatePlayerVisibilities() {
+ val scrolledIn = scrollIntoCurrentMedia != 0
+ for (i in 0 until mediaContent.childCount) {
+ val view = mediaContent.getChildAt(i)
+ val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
+ view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
+ }
}
private fun updateView(key: String, data: MediaData) {
@@ -127,8 +174,7 @@
private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
if (desiredState != null && desiredState!!.measurementInput != null) {
// make sure the player width is set to the current state
- val measurementInput = desiredState!!.measurementInput!!
- existingPlayer.setPlayerWidth(measurementInput.width)
+ existingPlayer.setPlayerWidth(playerWidth)
}
}
@@ -147,6 +193,10 @@
}
+ /**
+ * Set the current state of a view. This is updated often during animations and we shouldn't
+ * do anything expensive.
+ */
fun setCurrentState(state: MediaState) {
currentState = state
currentlyExpanded = state.expansion > 0
@@ -157,23 +207,58 @@
}
/**
- * @param targetState the target state we're transitioning to
+ * The desired location of this view has changed. We should remeasure the view to match
+ * the new bounds and kick off bounds animations if necessary.
+ * If an animation is happening, an animation is kicked of externally, which sets a new
+ * current state until we reach the targetState.
+ *
+ * @param desiredState the target state we're transitioning to
* @param animate should this be animated
*/
- fun onDesiredStateChanged(targetState: MediaState?, animate: Boolean, duration: Long,
- startDelay: Long) {
- if (targetState is MediaHost.MediaHostState) {
+ fun onDesiredLocationChanged(desiredState: MediaState?, animate: Boolean, duration: Long,
+ startDelay: Long) {
+ if (desiredState is MediaHost.MediaHostState) {
// This is a hosting view, let's remeasure our players
- desiredState = targetState
- val measurementInput = targetState.measurementInput
- remeasureAllPlayers(measurementInput, animate, duration, startDelay)
+ this.desiredState = desiredState
+ val width = desiredState.boundsOnScreen.width()
+ if (playerWidth != width) {
+ setPlayerWidth(width)
+ for (mediaPlayer in mediaPlayers.values) {
+ if (animate && mediaPlayer.view.visibility == View.VISIBLE) {
+ mediaPlayer.animatePendingSizeChange(duration, startDelay)
+ }
+ }
+ val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
+ val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
+ var left = 0
+ for (i in 0 until mediaContent.childCount) {
+ val view = mediaContent.getChildAt(i)
+ view.measure(widthSpec, heightSpec)
+ view.layout(left, 0, left + width, view.measuredHeight)
+ left = left + playerWidthPlusPadding
+ }
+ }
}
}
- fun remeasureAllPlayers(measurementInput: MediaMeasurementInput?,
- animate: Boolean, duration: Long, startDelay: Long) {
- for (mediaPlayer in mediaPlayers.values) {
- mediaPlayer.remeasure(measurementInput, animate, duration, startDelay)
+ fun setPlayerWidth(width: Int) {
+ if (width != playerWidth) {
+ playerWidth = width
+ playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
+ R.dimen.qs_media_padding)
+ for (mediaPlayer in mediaPlayers.values) {
+ mediaPlayer.setPlayerWidth(width)
+ }
+ // The player width has changed, let's update the scroll position to make sure
+ // it's still at the same place
+ var newScroll = activeMediaIndex * playerWidthPlusPadding
+ if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
+ newScroll += playerWidthPlusPadding
+ - (scrollIntoCurrentMedia - playerWidthPlusPadding)
+ } else {
+ newScroll += scrollIntoCurrentMedia
+ }
+ mediaCarousel.scrollX = newScroll
}
}
@@ -185,15 +270,33 @@
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
+ val previousRight = firstPlayer.view.right
+ val previousBottom = firstPlayer.view.bottom
firstPlayer.view.progress = input.expansion
- firstPlayer.remeasure(input, false /* animate */, 0, 0)
+ firstPlayer.measure(input)
+ // Relayouting is necessary in motionlayout to obtain its size properly ....
+ firstPlayer.view.layout(0, 0, firstPlayer.view.measuredWidth,
+ firstPlayer.view.measuredHeight)
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)
+ firstPlayer.measure(desiredState!!.measurementInput)
+ firstPlayer.view.layout(0, 0, previousRight, previousBottom)
}
return result
}
+
+ fun onViewReattached() {
+ if (desiredState is MediaHost.MediaHostState) {
+ // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
+ // a measure to force it.
+ val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
+ val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
+ for (mediaPlayer in mediaPlayers.values) {
+ mediaPlayer.view.measure(widthSpec, heightSpec)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt
new file mode 100644
index 0000000..8efc954
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.media
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.HorizontalScrollView
+
+/**
+ * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful
+ * when only measuring children but not the parent, when trying to apply a new scroll position
+ */
+class UnboundHorizontalScrollView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
+ : HorizontalScrollView(context, attrs, defStyleAttr) {
+
+ /**
+ * Allow all scrolls to go through, use base implementation
+ */
+ override fun scrollTo(x: Int, y: Int) {
+ if (mScrollX != x || mScrollY != y) {
+ val oldX: Int = mScrollX
+ val oldY: Int = mScrollY
+ mScrollX = x
+ mScrollY = y
+ invalidateParentCaches()
+ onScrollChanged(mScrollX, mScrollY, oldX, oldY)
+ if (!awakenScrollBars()) {
+ postInvalidateOnAnimation()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
index 376d091..bf94c5d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
@@ -22,15 +22,20 @@
import android.widget.FrameLayout
/**
- * A special view that hosts a unique object, which only exists once, but can transition between
- * different hosts. If the view currently hosts the unique object, it's measuring it normally,
- * but if it's not attached, it will obtain the size by requesting a measure.
+ * A special view that is designed to host a single "unique object". The unique object is
+ * dynamically added and removed from this view and may transition to other UniqueObjectHostViews
+ * available in the system.
+ * This is useful to share a singular instance of a view that can transition between completely
+ * independent parts of the view hierarchy.
+ * If the view currently hosts the unique object, it's measuring it normally,
+ * but if it's not attached, it will obtain the size by requesting a measure, as if it were
+ * always attached.
*/
class UniqueObjectHostView(
context: Context
) : FrameLayout(context) {
lateinit var measurementCache : GuaranteedMeasurementCache
- var firstMeasureListener: ((MeasurementInput) -> Unit)? = null
+ var onMeasureListener: ((MeasurementInput) -> Unit)? = null
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -41,13 +46,18 @@
val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical
val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
val measurementInput = MeasurementInputData(widthSpec, heightSpec)
- firstMeasureListener?.apply {
+ onMeasureListener?.apply {
invoke(measurementInput)
- firstMeasureListener = null
}
if (!isCurrentHost()) {
// We're not currently the host, let's get the dimension from our cache (this might
// perform a measuring if the cache doesn't have it yet)
+ // The goal here is that the view will always have a consistent measuring, regardless
+ // if it's attached or not.
+ // The behavior is therefore very similar to the view being persistently attached to
+ // this host, which can prevent flickers. It also makes sure that we always know
+ // the size of the view during transitions even if it has never been attached here
+ // before.
val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput)
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
} else {