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/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index fe8f2bb..f888ca1 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -23,7 +23,6 @@
     android:scrollbars="none"
     android:clipChildren="false"
     android:clipToPadding="false"
-    android:fillViewport="true"
     >
     <LinearLayout
         android:id="@+id/media_carousel"
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml
index 36ae225..9ad380d 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/qs_media_panel.xml
@@ -31,7 +31,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:background="@drawable/qs_media_background"
-        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintEnd_toEndOf="@id/view_width"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -175,7 +175,14 @@
     <include
         layout="@layout/qs_media_panel_options"
         android:visibility="gone"
-        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintEnd_toEndOf="@id/view_width"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/view_width"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="300dp" />
 </androidx.constraintlayout.motion.widget.MotionLayout>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 04640f4..fef8300 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -70,6 +70,26 @@
     <item type="id" name="panel_alpha_animator_end_tag"/>
     <item type="id" name="cross_fade_layer_type_changed_tag"/>
 
+    <item type="id" name="absolute_x_animator_tag"/>
+    <item type="id" name="absolute_x_animator_start_tag"/>
+    <item type="id" name="absolute_x_animator_end_tag"/>
+    <item type="id" name="absolute_x_current_value"/>
+
+    <item type="id" name="absolute_y_animator_tag"/>
+    <item type="id" name="absolute_y_animator_start_tag"/>
+    <item type="id" name="absolute_y_animator_end_tag"/>
+    <item type="id" name="absolute_y_current_value"/>
+
+    <item type="id" name="view_height_animator_tag"/>
+    <item type="id" name="view_height_animator_start_tag"/>
+    <item type="id" name="view_height_animator_end_tag"/>
+    <item type="id" name="view_height_current_value"/>
+
+    <item type="id" name="view_width_animator_tag"/>
+    <item type="id" name="view_width_animator_start_tag"/>
+    <item type="id" name="view_width_animator_end_tag"/>
+    <item type="id" name="view_width_current_value"/>
+
     <!-- Whether the icon is from a notification for which targetSdk < L -->
     <item type="id" name="icon_is_pre_L"/>
 
diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml
index ab9b7d9..6f99d9a 100644
--- a/packages/SystemUI/res/xml/media_scene.xml
+++ b/packages/SystemUI/res/xml/media_scene.xml
@@ -145,7 +145,7 @@
             android:id="@+id/media_seamless"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintWidth_min="60dp"
             android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
@@ -172,7 +172,7 @@
             android:layout_marginStart="16dp"
             app:layout_constraintTop_toBottomOf="@+id/app_name"
             app:layout_constraintStart_toEndOf="@id/album_art"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             app:layout_constraintHorizontal_bias="0"/>
 
         <!-- Artist name -->
@@ -184,7 +184,7 @@
             android:layout_marginTop="3dp"
             app:layout_constraintTop_toBottomOf="@id/header_title"
             app:layout_constraintStart_toStartOf="@id/header_title"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             app:layout_constraintHorizontal_bias="0"/>
 
         <!-- Seek Bar -->
@@ -195,7 +195,7 @@
             android:layout_marginTop="3dp"
             app:layout_constraintTop_toBottomOf="@id/header_artist"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             />
 
         <Constraint
@@ -207,7 +207,7 @@
             android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
             app:layout_constraintTop_toBottomOf="@id/header_artist"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             />
 
         <Constraint
@@ -272,7 +272,7 @@
             android:layout_marginEnd="4dp"
             android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
             app:layout_constraintLeft_toRightOf="@id/action3"
-            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintRight_toRightOf="@id/view_width"
             app:layout_constraintTop_toTopOf="@id/action0"
             app:layout_constraintBottom_toBottomOf="parent">
         </Constraint>
@@ -306,7 +306,7 @@
             android:id="@+id/media_seamless"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintWidth_min="60dp"
             android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
@@ -358,7 +358,7 @@
             android:layout_height="wrap_content"
             app:layout_constraintTop_toBottomOf="@id/album_art"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             android:visibility="gone"
             />
 
@@ -371,7 +371,7 @@
             android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
             app:layout_constraintTop_toBottomOf="@id/album_art"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/view_width"
             android:visibility="gone"
             />
 
@@ -438,7 +438,7 @@
             android:layout_marginTop="18dp"
             app:layout_constraintTop_toBottomOf="@id/app_name"
             app:layout_constraintLeft_toRightOf="@id/action3"
-            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintRight_toRightOf="@id/view_width"
             >
         </Constraint>
     </ConstraintSet>
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
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java
index 75b41ca..eee9cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AnimatableProperty.java
@@ -16,7 +16,9 @@
 
 package com.android.systemui.statusbar.notification;
 
+import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.util.Property;
 import android.view.View;
 
@@ -35,6 +37,100 @@
     public static final AnimatableProperty Y = AnimatableProperty.from(View.Y,
             R.id.y_animator_tag, R.id.y_animator_tag_start_value, R.id.y_animator_tag_end_value);
 
+    /**
+     * Similar to X, however this doesn't allow for any other modifications other than from this
+     * property. When using X, it's possible that the view is laid out during the animation,
+     * which could break the continuity
+     */
+    public static final AnimatableProperty ABSOLUTE_X = AnimatableProperty.from(
+            new FloatProperty<View>("ViewAbsoluteX") {
+                @Override
+                public void setValue(View view, float value) {
+                    view.setTag(R.id.absolute_x_current_value, value);
+                    View.X.set(view, value);
+                }
+
+                @Override
+                public Float get(View view) {
+                    Object tag = view.getTag(R.id.absolute_x_current_value);
+                    if (tag instanceof Float) {
+                        return (Float) tag;
+                    }
+                    return View.X.get(view);
+                }
+            },
+            R.id.absolute_x_animator_tag,
+            R.id.absolute_x_animator_start_tag,
+            R.id.absolute_x_animator_end_tag);
+
+    /**
+     * Similar to Y, however this doesn't allow for any other modifications other than from this
+     * property. When using X, it's possible that the view is laid out during the animation,
+     * which could break the continuity
+     */
+    public static final AnimatableProperty ABSOLUTE_Y = AnimatableProperty.from(
+            new FloatProperty<View>("ViewAbsoluteY") {
+                @Override
+                public void setValue(View view, float value) {
+                    view.setTag(R.id.absolute_y_current_value, value);
+                    View.Y.set(view, value);
+                }
+
+                @Override
+                public Float get(View view) {
+                    Object tag = view.getTag(R.id.absolute_y_current_value);
+                    if (tag instanceof Float) {
+                        return (Float) tag;
+                    }
+                    return View.Y.get(view);
+                }
+            },
+            R.id.absolute_y_animator_tag,
+            R.id.absolute_y_animator_start_tag,
+            R.id.absolute_y_animator_end_tag);
+
+    public static final AnimatableProperty WIDTH = AnimatableProperty.from(
+            new FloatProperty<View>("ViewWidth") {
+                @Override
+                public void setValue(View view, float value) {
+                    view.setTag(R.id.view_width_current_value, value);
+                    view.setRight((int) (view.getLeft() + value));
+                }
+
+                @Override
+                public Float get(View view) {
+                    Object tag = view.getTag(R.id.view_width_current_value);
+                    if (tag instanceof Float) {
+                        return (Float) tag;
+                    }
+                    return (float) view.getWidth();
+                }
+            },
+            R.id.view_width_animator_tag,
+            R.id.view_width_animator_start_tag,
+            R.id.view_width_animator_end_tag);
+
+    public static final AnimatableProperty HEIGHT = AnimatableProperty.from(
+            new FloatProperty<View>("ViewHeight") {
+                @Override
+                public void setValue(View view, float value) {
+                    view.setTag(R.id.view_height_current_value, value);
+                    view.setBottom((int) (view.getTop() + value));
+                }
+
+                @Override
+                public Float get(View view) {
+                    Object tag = view.getTag(R.id.view_height_current_value);
+                    if (tag instanceof Float) {
+                        return (Float) tag;
+                    }
+                    return (float) view.getHeight();
+                }
+            },
+            R.id.view_height_animator_tag,
+            R.id.view_height_animator_start_tag,
+            R.id.view_height_animator_end_tag);
+
     public abstract int getAnimationStartTag();
 
     public abstract int getAnimationEndTag();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
index 1f9d3af..b1b6a1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java
@@ -34,13 +34,20 @@
  */
 public class PropertyAnimator {
 
+    /**
+     * Set a property on a view, updating its value, even if it's already animating.
+     * The @param animated can be used to request an animation.
+     * If the view isn't animated, this utility will update the current animation if existent,
+     * such that the end value will point to @param newEndValue or apply it directly if there's
+     * no animation.
+     */
     public static <T extends View> void setProperty(final T view,
             AnimatableProperty animatableProperty, float newEndValue,
             AnimationProperties properties, boolean animated) {
         int animatorTag = animatableProperty.getAnimatorTag();
         ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
         if (previousAnimator != null || animated) {
-            startAnimation(view, animatableProperty, newEndValue, properties);
+            startAnimation(view, animatableProperty, newEndValue, animated ? properties : null);
         } else {
             // no new animation needed, let's just apply the value
             animatableProperty.getProperty().set(view, newEndValue);
@@ -60,8 +67,8 @@
         }
         int animatorTag = animatableProperty.getAnimatorTag();
         ValueAnimator previousAnimator = ViewState.getChildTag(view, animatorTag);
-        AnimationFilter filter = properties.getAnimationFilter();
-        if (!filter.shouldAnimateProperty(property)) {
+        AnimationFilter filter = properties != null ? properties.getAnimationFilter() : null;
+        if (filter == null || !filter.shouldAnimateProperty(property)) {
             // just a local update was performed
             if (previousAnimator != null) {
                 // we need to increase all animation keyframes of the previous animator by the
@@ -82,6 +89,14 @@
         }
 
         Float currentValue = property.get(view);
+        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property);
+        if (currentValue.equals(newEndValue)) {
+            // Skip the animation!
+            if (listener != null) {
+                listener.onAnimationEnd(null);
+            }
+            return;
+        }
         ValueAnimator animator = ValueAnimator.ofFloat(currentValue, newEndValue);
         animator.addUpdateListener(
                 animation -> property.set(view, (Float) animation.getAnimatedValue()));
@@ -96,7 +111,6 @@
                 || previousAnimator.getAnimatedFraction() == 0)) {
             animator.setStartDelay(properties.delay);
         }
-        AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property);
         if (listener != null) {
             animator.addListener(listener);
         }