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
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -47,6 +47,7 @@ import android.widget.SeekBar;
|
||||
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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
// 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 @@ public class MediaControlPanel {
|
||||
|
||||
// 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 @@ public class MediaControlPanel {
|
||||
}
|
||||
|
||||
// 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 @@ public class MediaControlPanel {
|
||||
// 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 @@ public class MediaControlPanel {
|
||||
*/
|
||||
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 @@ public class MediaControlPanel {
|
||||
mMediaNotifView.updateState(R.id.collapsed, collapsedSet);
|
||||
mMediaNotifView.updateState(R.id.expanded, expandedSet);
|
||||
}
|
||||
|
||||
public void animatePendingSizeChange(long duration, long startDelay) {
|
||||
mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ class MediaHierarchyManager @Inject constructor(
|
||||
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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
}
|
||||
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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
// 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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
}
|
||||
|
||||
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 @@ class MediaHierarchyManager @Inject constructor(
|
||||
rootOverlay!!.add(mediaCarousel)
|
||||
} else {
|
||||
targetHost.addView(mediaCarousel)
|
||||
mediaViewManager.onViewReattached()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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(
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.android.systemui.media
|
||||
|
||||
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 @@ class MediaViewManager @Inject constructor(
|
||||
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 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
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 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
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 @@ class MediaViewManager @Inject constructor(
|
||||
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 @@ class MediaViewManager @Inject constructor(
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 @@ class MediaViewManager @Inject constructor(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,20 @@ import android.view.View
|
||||
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 @@ class UniqueObjectHostView(
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user