| /* |
| * 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 android.annotation.AnimRes; |
| import android.annotation.InterpolatorRes; |
| import android.annotation.TestApi; |
| import android.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.Xml; |
| |
| 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 class AnimationState { |
| boolean animationClockLocked; |
| long currentVsyncTimeMillis; |
| long lastReportedTimeMillis; |
| }; |
| |
| private static ThreadLocal<AnimationState> sAnimationState |
| = new ThreadLocal<AnimationState>() { |
| @Override |
| protected AnimationState initialValue() { |
| return new AnimationState(); |
| } |
| }; |
| |
| /** |
| * 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. |
| * |
| * @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; |
| } |
| |
| /** |
| * 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 reference 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 ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } catch (IOException ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } 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 { |
| |
| 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 { |
| throw new RuntimeException("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 object reference 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 ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } catch (IOException ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } finally { |
| if (parser != null) parser.close(); |
| } |
| } |
| |
| private static LayoutAnimationController createLayoutAnimationFromXml(Context c, |
| XmlPullParser parser) throws XmlPullParserException, IOException { |
| |
| return createLayoutAnimationFromXml(c, parser, Xml.asAttributeSet(parser)); |
| } |
| |
| private static LayoutAnimationController createLayoutAnimationFromXml(Context c, |
| XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { |
| |
| 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 RuntimeException("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 animation object reference 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 ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } catch (IOException ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } 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 reference 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 ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } catch (IOException ex) { |
| NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" + |
| Integer.toHexString(id)); |
| rnf.initCause(ex); |
| throw rnf; |
| } finally { |
| if (parser != null) |
| parser.close(); |
| } |
| |
| } |
| |
| private static Interpolator createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser) |
| throws XmlPullParserException, IOException { |
| |
| 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 RuntimeException("Unknown interpolator name: " + parser.getName()); |
| } |
| } |
| return interpolator; |
| } |
| } |