500 lines
20 KiB
Java
500 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2007 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 android.view.animation;
|
|
|
|
import static android.view.flags.Flags.FLAG_EXPECTED_PRESENTATION_TIME_READ_ONLY;
|
|
import static android.view.flags.Flags.expectedPresentationTimeReadOnly;
|
|
|
|
import android.annotation.AnimRes;
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.InterpolatorRes;
|
|
import android.annotation.TestApi;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.content.res.Resources.NotFoundException;
|
|
import android.content.res.Resources.Theme;
|
|
import android.content.res.XmlResourceParser;
|
|
import android.os.SystemClock;
|
|
import android.util.AttributeSet;
|
|
import android.util.TimeUtils;
|
|
import android.util.Xml;
|
|
import android.view.InflateException;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.IOException;
|
|
|
|
/**
|
|
* Defines common utilities for working with animations.
|
|
*
|
|
*/
|
|
public class AnimationUtils {
|
|
|
|
/**
|
|
* These flags are used when parsing AnimatorSet objects
|
|
*/
|
|
private static final int TOGETHER = 0;
|
|
private static final int SEQUENTIALLY = 1;
|
|
|
|
private static boolean sExpectedPresentationTimeFlagValue;
|
|
static {
|
|
sExpectedPresentationTimeFlagValue = expectedPresentationTimeReadOnly();
|
|
}
|
|
|
|
private static class AnimationState {
|
|
boolean animationClockLocked;
|
|
long currentVsyncTimeMillis;
|
|
long lastReportedTimeMillis;
|
|
long mExpectedPresentationTimeNanos;
|
|
};
|
|
|
|
private static ThreadLocal<AnimationState> sAnimationState
|
|
= new ThreadLocal<AnimationState>() {
|
|
@Override
|
|
protected AnimationState initialValue() {
|
|
return new AnimationState();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Locks AnimationUtils{@link #currentAnimationTimeMillis()} and
|
|
* AnimationUtils{@link #expectedPresentationTimeNanos()} to a fixed value for the current
|
|
* thread. This is used by {@link android.view.Choreographer} to ensure that all accesses
|
|
* during a vsync update are synchronized to the timestamp of the vsync.
|
|
*
|
|
* It is also exposed to tests to allow for rapid, flake-free headless testing.
|
|
*
|
|
* Must be followed by a call to {@link #unlockAnimationClock()} to allow time to
|
|
* progress. Failing to do this will result in stuck animations, scrolls, and flings.
|
|
*
|
|
* Note that time is not allowed to "rewind" and must perpetually flow forward. So the
|
|
* lock may fail if the time is in the past from a previously returned value, however
|
|
* time will be frozen for the duration of the lock. The clock is a thread-local, so
|
|
* ensure that {@link #lockAnimationClock(long)}, {@link #unlockAnimationClock()},
|
|
* {@link #currentAnimationTimeMillis()}, and {@link #expectedPresentationTimeNanos()}
|
|
* are all called on the same thread.
|
|
*
|
|
* This is also not reference counted in any way. Any call to {@link #unlockAnimationClock()}
|
|
* will unlock the clock for everyone on the same thread. It is therefore recommended
|
|
* for tests to use their own thread to ensure that there is no collision with any existing
|
|
* {@link android.view.Choreographer} instance.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@FlaggedApi(FLAG_EXPECTED_PRESENTATION_TIME_READ_ONLY)
|
|
public static void lockAnimationClock(long vsyncMillis, long expectedPresentationTimeNanos) {
|
|
AnimationState state = sAnimationState.get();
|
|
state.animationClockLocked = true;
|
|
state.currentVsyncTimeMillis = vsyncMillis;
|
|
if (!sExpectedPresentationTimeFlagValue) {
|
|
state.mExpectedPresentationTimeNanos = expectedPresentationTimeNanos;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Locks AnimationUtils{@link #currentAnimationTimeMillis()} to a fixed value for the current
|
|
* thread. This is used by {@link android.view.Choreographer} to ensure that all accesses
|
|
* during a vsync update are synchronized to the timestamp of the vsync.
|
|
*
|
|
* It is also exposed to tests to allow for rapid, flake-free headless testing.
|
|
*
|
|
* Must be followed by a call to {@link #unlockAnimationClock()} to allow time to
|
|
* progress. Failing to do this will result in stuck animations, scrolls, and flings.
|
|
*
|
|
* Note that time is not allowed to "rewind" and must perpetually flow forward. So the
|
|
* lock may fail if the time is in the past from a previously returned value, however
|
|
* time will be frozen for the duration of the lock. The clock is a thread-local, so
|
|
* ensure that {@link #lockAnimationClock(long)}, {@link #unlockAnimationClock()}, and
|
|
* {@link #currentAnimationTimeMillis()} are all called on the same thread.
|
|
*
|
|
* This is also not reference counted in any way. Any call to {@link #unlockAnimationClock()}
|
|
* will unlock the clock for everyone on the same thread. It is therefore recommended
|
|
* for tests to use their own thread to ensure that there is no collision with any existing
|
|
* {@link android.view.Choreographer} instance.
|
|
*
|
|
* Have to add the method back because of b/307888459.
|
|
* Remove this method once the lockAnimationClock(long, long) change
|
|
* is landed to aosp/android14-tests-dev branch.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
public static void lockAnimationClock(long vsyncMillis) {
|
|
AnimationState state = sAnimationState.get();
|
|
state.animationClockLocked = true;
|
|
state.currentVsyncTimeMillis = vsyncMillis;
|
|
}
|
|
|
|
/**
|
|
* Frees the time lock set in place by {@link #lockAnimationClock(long)}. Must be called
|
|
* to allow the animation clock to self-update.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
public static void unlockAnimationClock() {
|
|
sAnimationState.get().animationClockLocked = false;
|
|
}
|
|
|
|
/**
|
|
* Returns the current animation time in milliseconds. This time should be used when invoking
|
|
* {@link Animation#setStartTime(long)}. Refer to {@link android.os.SystemClock} for more
|
|
* information about the different available clocks. The clock used by this method is
|
|
* <em>not</em> the "wall" clock (it is not {@link System#currentTimeMillis}).
|
|
*
|
|
* @return the current animation time in milliseconds
|
|
*
|
|
* @see android.os.SystemClock
|
|
*/
|
|
public static long currentAnimationTimeMillis() {
|
|
AnimationState state = sAnimationState.get();
|
|
if (state.animationClockLocked) {
|
|
// It's important that time never rewinds
|
|
return Math.max(state.currentVsyncTimeMillis,
|
|
state.lastReportedTimeMillis);
|
|
}
|
|
state.lastReportedTimeMillis = SystemClock.uptimeMillis();
|
|
return state.lastReportedTimeMillis;
|
|
}
|
|
|
|
/**
|
|
* The expected presentation time of a frame in the {@link System#nanoTime()}.
|
|
* Developers should prefer using this method over {@link #currentAnimationTimeMillis()}
|
|
* because it offers a more accurate time for the calculating animation progress.
|
|
*
|
|
* @return the expected presentation time of a frame in the
|
|
* {@link System#nanoTime()} time base.
|
|
*/
|
|
@FlaggedApi(FLAG_EXPECTED_PRESENTATION_TIME_READ_ONLY)
|
|
public static long getExpectedPresentationTimeNanos() {
|
|
if (!sExpectedPresentationTimeFlagValue) {
|
|
return SystemClock.uptimeMillis() * TimeUtils.NANOS_PER_MS;
|
|
}
|
|
|
|
AnimationState state = sAnimationState.get();
|
|
return state.mExpectedPresentationTimeNanos;
|
|
}
|
|
|
|
/**
|
|
* The expected presentation time of a frame in the {@link SystemClock#uptimeMillis()}.
|
|
* Developers should prefer using this method over {@link #currentAnimationTimeMillis()}
|
|
* because it offers a more accurate time for the calculating animation progress.
|
|
*
|
|
* @return the expected presentation time of a frame in the
|
|
* {@link SystemClock#uptimeMillis()} time base.
|
|
*/
|
|
@FlaggedApi(FLAG_EXPECTED_PRESENTATION_TIME_READ_ONLY)
|
|
public static long getExpectedPresentationTimeMillis() {
|
|
return getExpectedPresentationTimeNanos() / TimeUtils.NANOS_PER_MS;
|
|
}
|
|
|
|
/**
|
|
* Loads an {@link Animation} object from a resource
|
|
*
|
|
* @param context Application context used to access resources
|
|
* @param id The resource id of the animation to load
|
|
* @return The animation object referenced by the specified id
|
|
* @throws NotFoundException when the animation cannot be loaded
|
|
*/
|
|
public static Animation loadAnimation(Context context, @AnimRes int id)
|
|
throws NotFoundException {
|
|
|
|
XmlResourceParser parser = null;
|
|
try {
|
|
parser = context.getResources().getAnimation(id);
|
|
return createAnimationFromXml(context, parser);
|
|
} catch (XmlPullParserException | IOException ex) {
|
|
throw new NotFoundException(
|
|
"Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
|
|
} finally {
|
|
if (parser != null) parser.close();
|
|
}
|
|
}
|
|
|
|
private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
private static Animation createAnimationFromXml(
|
|
Context c, XmlPullParser parser, AnimationSet parent, AttributeSet attrs)
|
|
throws XmlPullParserException, IOException, InflateException {
|
|
|
|
Animation anim = null;
|
|
|
|
// Make sure we are on a start tag.
|
|
int type;
|
|
int depth = parser.getDepth();
|
|
|
|
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
|
&& type != XmlPullParser.END_DOCUMENT) {
|
|
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
|
|
String name = parser.getName();
|
|
|
|
if (name.equals("set")) {
|
|
anim = new AnimationSet(c, attrs);
|
|
createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
|
|
} else if (name.equals("alpha")) {
|
|
anim = new AlphaAnimation(c, attrs);
|
|
} else if (name.equals("scale")) {
|
|
anim = new ScaleAnimation(c, attrs);
|
|
} else if (name.equals("rotate")) {
|
|
anim = new RotateAnimation(c, attrs);
|
|
} else if (name.equals("translate")) {
|
|
anim = new TranslateAnimation(c, attrs);
|
|
} else if (name.equals("cliprect")) {
|
|
anim = new ClipRectAnimation(c, attrs);
|
|
} else if (name.equals("extend")) {
|
|
anim = new ExtendAnimation(c, attrs);
|
|
} else {
|
|
throw new InflateException("Unknown animation name: " + parser.getName());
|
|
}
|
|
|
|
if (parent != null) {
|
|
parent.addAnimation(anim);
|
|
}
|
|
}
|
|
|
|
return anim;
|
|
|
|
}
|
|
|
|
/**
|
|
* Loads a {@link LayoutAnimationController} object from a resource
|
|
*
|
|
* @param context Application context used to access resources
|
|
* @param id The resource id of the animation to load
|
|
* @return The animation controller object referenced by the specified id
|
|
* @throws NotFoundException when the layout animation controller cannot be loaded
|
|
*/
|
|
public static LayoutAnimationController loadLayoutAnimation(Context context, @AnimRes int id)
|
|
throws NotFoundException {
|
|
|
|
XmlResourceParser parser = null;
|
|
try {
|
|
parser = context.getResources().getAnimation(id);
|
|
return createLayoutAnimationFromXml(context, parser);
|
|
} catch (XmlPullParserException | IOException | InflateException ex) {
|
|
throw new NotFoundException(
|
|
"Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
|
|
} finally {
|
|
if (parser != null) parser.close();
|
|
}
|
|
}
|
|
|
|
private static LayoutAnimationController createLayoutAnimationFromXml(
|
|
Context c, XmlPullParser parser)
|
|
throws XmlPullParserException, IOException, InflateException {
|
|
|
|
return createLayoutAnimationFromXml(c, parser, Xml.asAttributeSet(parser));
|
|
}
|
|
|
|
private static LayoutAnimationController createLayoutAnimationFromXml(
|
|
Context c, XmlPullParser parser, AttributeSet attrs)
|
|
throws XmlPullParserException, IOException, InflateException {
|
|
|
|
LayoutAnimationController controller = null;
|
|
|
|
int type;
|
|
int depth = parser.getDepth();
|
|
|
|
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
|
&& type != XmlPullParser.END_DOCUMENT) {
|
|
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
|
|
String name = parser.getName();
|
|
|
|
if ("layoutAnimation".equals(name)) {
|
|
controller = new LayoutAnimationController(c, attrs);
|
|
} else if ("gridLayoutAnimation".equals(name)) {
|
|
controller = new GridLayoutAnimationController(c, attrs);
|
|
} else {
|
|
throw new InflateException("Unknown layout animation name: " + name);
|
|
}
|
|
}
|
|
|
|
return controller;
|
|
}
|
|
|
|
/**
|
|
* Make an animation for objects becoming visible. Uses a slide and fade
|
|
* effect.
|
|
*
|
|
* @param c Context for loading resources
|
|
* @param fromLeft is the object to be animated coming from the left
|
|
* @return The new animation
|
|
*/
|
|
public static Animation makeInAnimation(Context c, boolean fromLeft) {
|
|
Animation a;
|
|
if (fromLeft) {
|
|
a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_left);
|
|
} else {
|
|
a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_right);
|
|
}
|
|
|
|
a.setInterpolator(new DecelerateInterpolator());
|
|
a.setStartTime(currentAnimationTimeMillis());
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* Make an animation for objects becoming invisible. Uses a slide and fade
|
|
* effect.
|
|
*
|
|
* @param c Context for loading resources
|
|
* @param toRight is the object to be animated exiting to the right
|
|
* @return The new animation
|
|
*/
|
|
public static Animation makeOutAnimation(Context c, boolean toRight) {
|
|
Animation a;
|
|
if (toRight) {
|
|
a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_right);
|
|
} else {
|
|
a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_left);
|
|
}
|
|
|
|
a.setInterpolator(new AccelerateInterpolator());
|
|
a.setStartTime(currentAnimationTimeMillis());
|
|
return a;
|
|
}
|
|
|
|
|
|
/**
|
|
* Make an animation for objects becoming visible. Uses a slide up and fade
|
|
* effect.
|
|
*
|
|
* @param c Context for loading resources
|
|
* @return The new animation
|
|
*/
|
|
public static Animation makeInChildBottomAnimation(Context c) {
|
|
Animation a;
|
|
a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_child_bottom);
|
|
a.setInterpolator(new AccelerateInterpolator());
|
|
a.setStartTime(currentAnimationTimeMillis());
|
|
return a;
|
|
}
|
|
|
|
/**
|
|
* Loads an {@link Interpolator} object from a resource
|
|
*
|
|
* @param context Application context used to access resources
|
|
* @param id The resource id of the animation to load
|
|
* @return The interpolator object referenced by the specified id
|
|
* @throws NotFoundException
|
|
*/
|
|
public static Interpolator loadInterpolator(Context context, @AnimRes @InterpolatorRes int id)
|
|
throws NotFoundException {
|
|
XmlResourceParser parser = null;
|
|
try {
|
|
parser = context.getResources().getAnimation(id);
|
|
return createInterpolatorFromXml(context.getResources(), context.getTheme(), parser);
|
|
} catch (XmlPullParserException | IOException | InflateException ex) {
|
|
throw new NotFoundException(
|
|
"Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
|
|
} finally {
|
|
if (parser != null) parser.close();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Loads an {@link Interpolator} object from a resource
|
|
*
|
|
* @param res The resources
|
|
* @param id The resource id of the animation to load
|
|
* @return The interpolator object referenced by the specified id
|
|
* @throws NotFoundException
|
|
* @hide
|
|
*/
|
|
public static Interpolator loadInterpolator(Resources res, Theme theme, int id)
|
|
throws NotFoundException {
|
|
XmlResourceParser parser = null;
|
|
try {
|
|
parser = res.getAnimation(id);
|
|
return createInterpolatorFromXml(res, theme, parser);
|
|
} catch (XmlPullParserException | IOException | InflateException ex) {
|
|
throw new NotFoundException(
|
|
"Can't load animation resource ID #0x" + Integer.toHexString(id), ex);
|
|
} finally {
|
|
if (parser != null) {
|
|
parser.close();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private static Interpolator createInterpolatorFromXml(
|
|
Resources res, Theme theme, XmlPullParser parser)
|
|
throws XmlPullParserException, IOException, InflateException {
|
|
|
|
BaseInterpolator interpolator = null;
|
|
|
|
// Make sure we are on a start tag.
|
|
int type;
|
|
int depth = parser.getDepth();
|
|
|
|
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
|
|
&& type != XmlPullParser.END_DOCUMENT) {
|
|
|
|
if (type != XmlPullParser.START_TAG) {
|
|
continue;
|
|
}
|
|
|
|
AttributeSet attrs = Xml.asAttributeSet(parser);
|
|
|
|
String name = parser.getName();
|
|
|
|
if (name.equals("linearInterpolator")) {
|
|
interpolator = new LinearInterpolator();
|
|
} else if (name.equals("accelerateInterpolator")) {
|
|
interpolator = new AccelerateInterpolator(res, theme, attrs);
|
|
} else if (name.equals("decelerateInterpolator")) {
|
|
interpolator = new DecelerateInterpolator(res, theme, attrs);
|
|
} else if (name.equals("accelerateDecelerateInterpolator")) {
|
|
interpolator = new AccelerateDecelerateInterpolator();
|
|
} else if (name.equals("cycleInterpolator")) {
|
|
interpolator = new CycleInterpolator(res, theme, attrs);
|
|
} else if (name.equals("anticipateInterpolator")) {
|
|
interpolator = new AnticipateInterpolator(res, theme, attrs);
|
|
} else if (name.equals("overshootInterpolator")) {
|
|
interpolator = new OvershootInterpolator(res, theme, attrs);
|
|
} else if (name.equals("anticipateOvershootInterpolator")) {
|
|
interpolator = new AnticipateOvershootInterpolator(res, theme, attrs);
|
|
} else if (name.equals("bounceInterpolator")) {
|
|
interpolator = new BounceInterpolator();
|
|
} else if (name.equals("pathInterpolator")) {
|
|
interpolator = new PathInterpolator(res, theme, attrs);
|
|
} else {
|
|
throw new InflateException("Unknown interpolator name: " + parser.getName());
|
|
}
|
|
}
|
|
return interpolator;
|
|
}
|
|
}
|