diff options
10 files changed, 325 insertions, 21 deletions
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index fe8f2bb76f55..f888ca11b5fb 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 36ae2251d963..9ad380d260c0 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 04640f418e97..fef8300395b3 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 ab9b7d9a284d..6f99d9a675f1 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 000000000000..a366725a4398 --- /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 5a5cd6dc3a50..0b0bfc62ed74 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.MediaController.PlaybackInfo; 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { 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 @@ public class MediaControlPanel { * 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 673a543d8ac0..fc141bf256f0 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.content.Context 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 @@ class MediaHierarchyManager @Inject constructor( return; } updateTargetState() + var animate = false if (isCurrentlyInGuidedTransformation()) { applyTargetStateIfNotAnimating() } else if (shouldAnimateTransition(currentHost, previousHost)) { @@ -175,9 +173,12 @@ class MediaHierarchyManager @Inject constructor( 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 eeecf2bb6ef4..636e4e46a1b0 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 @@ class MediaViewManager @Inject constructor( 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 @@ class MediaViewManager @Inject constructor( } 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 75b41ca3e162..eee9cc683e2b 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 abstract class AnimatableProperty { 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 1f9d3af70b4f..b1b6a1c12a0a 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 @@ import com.android.systemui.statusbar.notification.stack.ViewState; */ 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 @@ public class PropertyAnimator { } 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 @@ public class PropertyAnimator { } 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 @@ public class PropertyAnimator { || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } - AnimatorListenerAdapter listener = properties.getAnimationFinishListener(property); if (listener != null) { animator.addListener(listener); } |