| /* |
| * 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; |
| } |
| } |