Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3db09e5b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Adding task open animation in fallback activity" into ub-launcher3-master

parents f0f00b30 68c47c56
Loading
Loading
Loading
Loading
+36 −194
Original line number Diff line number Diff line
@@ -19,6 +19,13 @@ package com.android.launcher3;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
import static com.android.launcher3.anim.Interpolators.APP_CLOSE_ALPHA;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.TaskUtils.findTaskViewToLaunch;
import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
@@ -31,7 +38,6 @@ import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -45,7 +51,6 @@ import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;

import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.InsettableFrameLayout.LayoutParams;
@@ -55,12 +60,10 @@ import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.DrawableFactory;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.quickstep.RecentsAnimationInterpolator;
import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -79,7 +82,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        implements OnDeviceProfileChangeListener {

    private static final String TAG = "LauncherTransition";
    private static final int STATUS_BAR_TRANSITION_DURATION = 120;
    public static final int STATUS_BAR_TRANSITION_DURATION = 120;

    private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
            "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
@@ -87,7 +90,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
    private static final int APP_LAUNCH_DURATION = 500;
    // Use a shorter duration for x or y translation to create a curve effect
    private static final int APP_LAUNCH_CURVED_DURATION = 233;
    private static final int RECENTS_LAUNCH_DURATION = 336;
    public static final int RECENTS_LAUNCH_DURATION = 336;
    private static final int LAUNCHER_RESUME_START_DELAY = 100;
    private static final int CLOSING_TRANSITION_DURATION_MS = 350;

@@ -184,64 +187,6 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        mRemoteAnimationProvider = animationProvider;
    }

    /**
     * Try to find a TaskView that corresponds with the component of the launched view.
     *
     * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation.
     * Otherwise, we will assume we are using a normal app transition, but it's possible that the
     * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
     */
    private TaskView findTaskViewToLaunch(
            BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) {
        if (v instanceof TaskView) {
            return (TaskView) v;
        }
        RecentsView recentsView = activity.getOverviewPanel();

        // It's possible that the launched view can still be resolved to a visible task view, check
        // the task id of the opening task and see if we can find a match.
        if (v.getTag() instanceof ItemInfo) {
            ItemInfo itemInfo = (ItemInfo) v.getTag();
            ComponentName componentName = itemInfo.getTargetComponent();
            if (componentName != null) {
                for (int i = 0; i < recentsView.getChildCount(); i++) {
                    TaskView taskView = (TaskView) recentsView.getPageAt(i);
                    if (recentsView.isTaskViewVisible(taskView)) {
                        Task task = taskView.getTask();
                        if (componentName.equals(task.key.getComponent())) {
                            return taskView;
                        }
                    }
                }
            }
        }

        if (targets == null) {
            return null;
        }
        // Resolve the opening task id
        int openingTaskId = -1;
        for (RemoteAnimationTargetCompat target : targets) {
            if (target.mode == MODE_OPENING) {
                openingTaskId = target.taskId;
                break;
            }
        }

        // If there is no opening task id, fall back to the normal app icon launch animation
        if (openingTaskId == -1) {
            return null;
        }

        // If the opening task id is not currently visible in overview, then fall back to normal app
        // icon launch animation
        TaskView taskView = recentsView.getTaskView(openingTaskId);
        if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
            return null;
        }
        return taskView;
    }

    /**
     * Composes the animations for a launch from the recents list if possible.
     */
@@ -285,7 +230,8 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
            };
        }

        target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets));
        target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets)
                .setDuration(RECENTS_LAUNCH_DURATION));
        target.play(launcherAnim);

        // Set the current animation first, before adding windowAnimEndListener. Setting current
@@ -296,83 +242,6 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        return true;
    }

    /**
     * @return Animator that controls the window of the opening targets for the recents launch
     * animation.
     */
    private ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipLauncherChanges,
            RemoteAnimationTargetCompat[] targets) {
        final RecentsAnimationInterpolator recentsInterpolator = v.getRecentsInterpolator();

        Rect crop = new Rect();
        Matrix matrix = new Matrix();

        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
        appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
        appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            boolean isFirstFrame = true;

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                final Surface surface = getSurface(v);
                final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
                if (frameNumber == -1) {
                    // Booo, not cool! Our surface got destroyed, so no reason to animate anything.
                    Log.w(TAG, "Failed to animate, surface got destroyed.");
                    return;
                }
                final float percent = animation.getAnimatedFraction();
                TaskWindowBounds tw = recentsInterpolator.interpolate(percent);

                float alphaDuration = 75;
                if (!skipLauncherChanges) {
                    v.setScaleX(tw.taskScale);
                    v.setScaleY(tw.taskScale);
                    v.setTranslationX(tw.taskX);
                    v.setTranslationY(tw.taskY);
                    // Defer fading out the view until after the app window gets faded in
                    v.setAlpha(getValue(1f, 0f, alphaDuration, alphaDuration,
                            appAnimator.getDuration() * percent, Interpolators.LINEAR));
                }

                matrix.setScale(tw.winScale, tw.winScale);
                matrix.postTranslate(tw.winX, tw.winY);
                crop.set(tw.winCrop);

                // Fade in the app window.
                float alpha = getValue(0f, 1f, 0, alphaDuration,
                        appAnimator.getDuration() * percent, Interpolators.LINEAR);

                TransactionCompat t = new TransactionCompat();
                for (RemoteAnimationTargetCompat target : targets) {
                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
                        t.setAlpha(target.leash, alpha);

                        // TODO: This isn't correct at the beginning of the animation, but better
                        // than nothing.
                        matrix.postTranslate(target.position.x, target.position.y);
                        t.setMatrix(target.leash, matrix);
                        t.setWindowCrop(target.leash, crop);

                        if (!skipLauncherChanges) {
                            t.deferTransactionUntil(target.leash, surface, frameNumber);
                        }
                    }
                    if (isFirstFrame) {
                        t.show(target.leash);
                    }
                }
                t.setEarlyWakeup();
                t.apply();

                matrix.reset();
                isFirstFrame = false;
            }
        });
        return appAnimator;
    }

    /**
     * Content is everything on screen except the background and the floating view (if any).
     *
@@ -399,9 +268,9 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag

            ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas);
            alpha.setDuration(217);
            alpha.setInterpolator(Interpolators.LINEAR);
            alpha.setInterpolator(LINEAR);
            ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans);
            transY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
            transY.setInterpolator(AGGRESSIVE_EASE);
            transY.setDuration(350);

            launcherAnimator.play(alpha);
@@ -420,10 +289,10 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag

            ObjectAnimator dragLayerAlpha = ObjectAnimator.ofFloat(mDragLayer, View.ALPHA, alphas);
            dragLayerAlpha.setDuration(217);
            dragLayerAlpha.setInterpolator(Interpolators.LINEAR);
            dragLayerAlpha.setInterpolator(LINEAR);
            ObjectAnimator dragLayerTransY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y,
                    trans);
            dragLayerTransY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
            dragLayerTransY.setInterpolator(AGGRESSIVE_EASE);
            dragLayerTransY.setDuration(350);

            launcherAnimator.play(dragLayerAlpha);
@@ -515,8 +384,8 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        boolean isBelowCenterY = lp.topMargin < centerY;
        x.setDuration(isBelowCenterY ? APP_LAUNCH_DURATION : APP_LAUNCH_CURVED_DURATION);
        y.setDuration(isBelowCenterY ? APP_LAUNCH_CURVED_DURATION : APP_LAUNCH_DURATION);
        x.setInterpolator(Interpolators.AGGRESSIVE_EASE);
        y.setInterpolator(Interpolators.AGGRESSIVE_EASE);
        x.setInterpolator(AGGRESSIVE_EASE);
        y.setInterpolator(AGGRESSIVE_EASE);
        appIconAnimatorSet.play(x);
        appIconAnimatorSet.play(y);

@@ -534,7 +403,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
        alpha.setStartDelay(32);
        alpha.setDuration(50);
        alpha.setInterpolator(Interpolators.LINEAR);
        alpha.setInterpolator(LINEAR);
        appIconAnimatorSet.play(alpha);

        appIconAnimatorSet.addListener(new AnimatorListenerAdapter() {
@@ -569,11 +438,14 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag

        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
        appAnimator.setDuration(APP_LAUNCH_DURATION);
        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
            // Fade alpha for the app window.
            FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR);

            boolean isFirstFrame = true;

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
            public void onUpdate(float percent) {
                final Surface surface = getSurface(mFloatingView);
                final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
                if (frameNumber == -1) {
@@ -581,8 +453,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                    Log.w(TAG, "Failed to animate, surface got destroyed.");
                    return;
                }
                final float percent = animation.getAnimatedFraction();
                final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent);
                final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent);

                // Calculate app icon size.
                float iconWidth = bounds.width() * mFloatingView.getScaleX();
@@ -607,11 +478,6 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                float transY0 = floatingViewBounds[1] - offsetY;
                matrix.postTranslate(transX0, transY0);

                // Fade in the app window.
                float alphaDuration = 60;
                float alpha = getValue(0f, 1f, 0, alphaDuration,
                        appAnimator.getDuration() * percent, Interpolators.LINEAR);

                // Animate the window crop so that it starts off as a square, and then reveals
                // horizontally.
                float cropHeight = deviceHeight * easePercent + deviceWidth * (1 - easePercent);
@@ -624,7 +490,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                TransactionCompat t = new TransactionCompat();
                for (RemoteAnimationTargetCompat target : targets) {
                    if (target.mode == MODE_OPENING) {
                        t.setAlpha(target.leash, alpha);
                        t.setAlpha(target.leash, mAlpha.value);

                        // TODO: This isn't correct at the beginning of the animation, but better
                        // than nothing.
@@ -670,13 +536,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
    }

    private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
        int launcherTaskId = mLauncher.getTaskId();
        for (RemoteAnimationTargetCompat target : targets) {
            if (target.mode == mode && target.taskId == launcherTaskId) {
                return true;
            }
        }
        return false;
        return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode);
    }

    /**
@@ -730,30 +590,23 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag

        ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
        closingAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
        closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
            FloatProp mDx = new FloatProp(0, endX, 0, 350, AGGRESSIVE_EASE_IN_OUT);
            FloatProp mScale = new FloatProp(1f, 0.8f, 0, 267, AGGRESSIVE_EASE);
            FloatProp mAlpha = new FloatProp(1f, 0f, 0, 350, APP_CLOSE_ALPHA);

        closingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            boolean isFirstFrame = true;

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                final float percent = animation.getAnimatedFraction();
                float currentPlayTime = percent * closingAnimator.getDuration();

                float scale = getValue(1f, 0.8f, 0, 267, currentPlayTime,
                        Interpolators.AGGRESSIVE_EASE);

                float dX = getValue(0, endX, 0, 350, currentPlayTime,
                        Interpolators.AGGRESSIVE_EASE_IN_OUT);

            public void onUpdate(float percent) {
                TransactionCompat t = new TransactionCompat();
                for (RemoteAnimationTargetCompat app : targets) {
                    if (app.mode == RemoteAnimationTargetCompat.MODE_CLOSING) {
                        t.setAlpha(app.leash, getValue(1f, 0f, 0, 350, currentPlayTime,
                                Interpolators.APP_CLOSE_ALPHA));
                        matrix.setScale(scale, scale,
                        t.setAlpha(app.leash, mAlpha.value);
                        matrix.setScale(mScale.value, mScale.value,
                                app.sourceContainerBounds.centerX(),
                                app.sourceContainerBounds.centerY());
                        matrix.postTranslate(dX, 0);
                        matrix.postTranslate(mDx.value, 0);
                        matrix.postTranslate(app.position.x, app.position.y);
                        t.setMatrix(app.leash, matrix);
                    }
@@ -772,6 +625,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                isFirstFrame = false;
            }
        });

        return closingAnimator;
    }

@@ -832,16 +686,4 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Helper method that allows us to get interpolated values for embedded
     * animations with a delay and/or different duration.
     */
    private static float getValue(float start, float end, float delay, float duration,
            float currentPlayTime, Interpolator i) {
        float time = Math.max(0, currentPlayTime - delay);
        float newPercent = Math.min(1f, time / duration);
        newPercent = i.getInterpolation(newPercent);
        return end * newPercent + start * (1 - newPercent);
    }
}
+63 −2
Original line number Diff line number Diff line
@@ -15,16 +15,29 @@
 */
package com.android.quickstep;

import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAnimationRunner;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.uioverrides.UiFactory;
import com.android.launcher3.util.SystemUiController;
@@ -32,12 +45,18 @@ import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.fallback.FallbackRecentsView;
import com.android.quickstep.fallback.RecentsRootView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;

/**
 * A simple activity to show the recently launched tasks
 */
public class RecentsActivity extends BaseDraggingActivity {

    private Handler mUiHandler = new Handler(Looper.getMainLooper());
    private RecentsRootView mRecentsRootView;
    private FallbackRecentsView mFallbackRecentsView;

@@ -84,10 +103,51 @@ public class RecentsActivity extends BaseDraggingActivity {
    }

    @Override
    public ActivityOptions getActivityLaunchOptions(View v, boolean useDefaultLaunchOptions) {
    public ActivityOptions getActivityLaunchOptions(final View v, boolean useDefaultLaunchOptions) {
        if (useDefaultLaunchOptions || !(v instanceof TaskView)) {
            return null;
        }

        final TaskView taskView = (TaskView) v;
        RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler) {

            @Override
            public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
                return composeRecentsLaunchAnimator(taskView, targetCompats);
            }
        };
        return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
                runner, RECENTS_LAUNCH_DURATION,
                RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION));
    }

    /**
     * Composes the animations for a launch from the recents list if possible.
     */
    private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
            RemoteAnimationTargetCompat[] targets) {
        AnimatorSet target = new AnimatorSet();
        boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING);
        target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets)
                .setDuration(RECENTS_LAUNCH_DURATION));

        // Found a visible recents task that matches the opening app, lets launch the app from there
        if (activityClosing) {
            Animator adjacentAnimation = mFallbackRecentsView
                    .createAdjacentPageAnimForTaskLaunch(taskView);
            adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
            adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION);
            adjacentAnimation.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mFallbackRecentsView.resetTaskVisuals();
                }
            });
            target.play(adjacentAnimation);
        }
        return target;
    }

    @Override
    public void invalidateParent(ItemInfo info) { }

@@ -95,6 +155,7 @@ public class RecentsActivity extends BaseDraggingActivity {
    protected void onStart() {
        super.onStart();
        UiFactory.onStart(this);
        mFallbackRecentsView.resetTaskVisuals();
    }

    @Override
+167 −1

File changed.

Preview size limit exceeded, changes collapsed.

+68 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.quickstep.util;

import android.animation.ValueAnimator;
import android.view.animation.Interpolator;

import java.util.ArrayList;

/**
 * Utility class to update multiple values with different interpolators and durations during
 * the same animation.
 */
public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener {

    private final ArrayList<FloatProp> mAllProperties = new ArrayList<>();

    @Override
    public final void onAnimationUpdate(ValueAnimator animator) {
        final float percent = animator.getAnimatedFraction();
        final float currentPlayTime = percent * animator.getDuration();

        for (int i = mAllProperties.size() - 1; i >= 0; i--) {
            FloatProp prop = mAllProperties.get(i);
            float time = Math.max(0, currentPlayTime - prop.mDelay);
            float newPercent = Math.min(1f, time / prop.mDuration);
            newPercent = prop.mInterpolator.getInterpolation(newPercent);
            prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent);
        }
        onUpdate(percent);
    }

    public abstract void onUpdate(float percent);

    public final class FloatProp {

        public float value;

        private final float mStart;
        private final float mEnd;
        private final float mDelay;
        private final float mDuration;
        private final Interpolator mInterpolator;

        public FloatProp(float start, float end, float delay, float duration, Interpolator i) {
            value = mStart = start;
            mEnd = end;
            mDelay = delay;
            mDuration = duration;
            mInterpolator = i;

            mAllProperties.add(this);
        }
    }
}