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/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 {