When animating between states, animate the view width
Test: add media notification observe no flickery animation
Bug: 154137987
Change-Id: I909dd4427813b34426cc7d7fc08f298761f134bc
diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
new file mode 100644
index 0000000..a366725
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
@@ -0,0 +1,115 @@
+/*
+ * 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 android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import com.android.systemui.statusbar.notification.AnimatableProperty
+import com.android.systemui.statusbar.notification.PropertyAnimator
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+
+/**
+ * A utility class that helps with animations of bound changes designed for motionlayout which
+ * doesn't work together with regular changeBounds.
+ */
+class LayoutAnimationHelper {
+
+ private val layout: ViewGroup
+ private var sizeAnimationPending = false
+ private val desiredBounds = mutableMapOf<View, Rect>()
+ private val animationProperties = AnimationProperties()
+ private val layoutListener = object : View.OnLayoutChangeListener {
+ override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
+ oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
+ v?.let {
+ if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 ||
+ oldTop - oldBottom == 0) {
+ return
+ }
+ if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) {
+ val rect = desiredBounds.getOrPut(v, { Rect() })
+ rect.set(left, top, right, bottom)
+ onDesiredLocationChanged(v, rect)
+ }
+ }
+ }
+ }
+
+ constructor(layout: ViewGroup) {
+ this.layout = layout
+ val childCount = this.layout.childCount
+ for (i in 0 until childCount) {
+ val child = this.layout.getChildAt(i)
+ child.addOnLayoutChangeListener(layoutListener)
+ }
+ }
+
+ private fun onDesiredLocationChanged(v: View, rect: Rect) {
+ if (!sizeAnimationPending) {
+ applyBounds(v, rect, animate = false)
+ }
+ // We need to reapply the current bounds in every frame since the layout may override
+ // the layout bounds making this view jump and not all calls to apply bounds actually
+ // reapply them, for example if there's already an animator to the same target
+ reapplyProperty(v, AnimatableProperty.ABSOLUTE_X);
+ reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y);
+ reapplyProperty(v, AnimatableProperty.WIDTH);
+ reapplyProperty(v, AnimatableProperty.HEIGHT);
+ }
+
+ private fun reapplyProperty(v: View, property: AnimatableProperty) {
+ property.property.set(v, property.property.get(v))
+ }
+
+ private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) {
+ PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(),
+ animationProperties, animate)
+ PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(),
+ animationProperties, animate)
+ PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(),
+ animationProperties, animate)
+ PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(),
+ animationProperties, animate)
+ }
+
+ private fun startBoundAnimation(v: View) {
+ val target = desiredBounds[v] ?: return
+ applyBounds(v, target, animate = true)
+ }
+
+ fun animatePendingSizeChange(duration: Long, delay: Long) {
+ animationProperties.duration = duration
+ animationProperties.delay = delay
+ if (!sizeAnimationPending) {
+ sizeAnimationPending = true
+ layout.viewTreeObserver.addOnPreDrawListener (
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ layout.viewTreeObserver.removeOnPreDrawListener(this)
+ sizeAnimationPending = false
+ val childCount = layout.childCount
+ for (i in 0 until childCount) {
+ val child = layout.getChildAt(i)
+ startBoundAnimation(child)
+ }
+ return true
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 5a5cd6d..0b0bfc6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -36,6 +36,7 @@
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.service.media.MediaBrowserService;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -93,6 +94,7 @@
private final Executor mForegroundExecutor;
protected final Executor mBackgroundExecutor;
private final ActivityStarter mActivityStarter;
+ private final LayoutAnimationHelper mLayoutAnimationHelper;
private Context mContext;
private MotionLayout mMediaNotifView;
@@ -109,6 +111,7 @@
private String mKey;
private int mAlbumArtSize;
private int mAlbumArtRadius;
+ private int mViewWidth;
public static final String MEDIA_PREFERENCES = "media_control_prefs";
public static final String MEDIA_PREFERENCE_KEY = "browser_components";
@@ -176,6 +179,7 @@
LayoutInflater inflater = LayoutInflater.from(mContext);
mMediaNotifView = (MotionLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
mBackground = mMediaNotifView.findViewById(R.id.media_background);
+ mLayoutAnimationHelper = new LayoutAnimationHelper(mMediaNotifView);
mKeyFrames = mMediaNotifView.getDefinedTransitions().get(0).getKeyFrameList();
mLocalMediaManager = routeManager;
mForegroundExecutor = foregroundExecutor;
@@ -732,4 +736,32 @@
* Called when a player can't be resumed to give it an opportunity to hide or remove itself
*/
protected void removePlayer() { }
+
+ public void setDimension(int newWidth, int newHeight, 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 (animate) {
+ mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
+ }
+ setViewWidth(newWidth);
+ mMediaNotifView.layout(0, 0, newWidth, mMediaNotifView.getMeasuredHeight());
+ }
+ }
+
+ protected void setViewWidth(int newWidth) {
+ 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;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 673a543..fc141bf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -24,9 +24,6 @@
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.media.InfoMediaManager
-import com.android.settingslib.media.LocalMediaManager
import com.android.systemui.Interpolators
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
@@ -167,6 +164,7 @@
return;
}
updateTargetState()
+ var animate = false
if (isCurrentlyInGuidedTransformation()) {
applyTargetStateIfNotAnimating()
} else if (shouldAnimateTransition(currentHost, previousHost)) {
@@ -175,9 +173,12 @@
animationStartState = currentState.copy()
adjustAnimatorForTransition(previousLocation, desiredLocation)
animator.start()
+ animate = true
} else {
cancelAnimationAndApplyDesiredState()
}
+ mediaViewManager.performTransition(targetState, animate, animator.duration,
+ animator.startDelay)
}
private fun shouldAnimateTransition(currentHost: MediaHost, previousHost: MediaHost): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index eeecf2b..636e4e4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -33,7 +33,6 @@
private val activityStarter: ActivityStarter,
mediaManager: MediaDataManager
) {
- private var targetState: MediaState? = null
val mediaCarousel: ViewGroup
private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
@@ -140,4 +139,25 @@
}
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
+ }
+ 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)
+ }
+ }
}
\ No newline at end of file