diff options
author | Chet Haase <chet@google.com> | 2014-12-01 06:32:38 -0800 |
---|---|---|
committer | Chet Haase <chet@google.com> | 2015-01-15 07:21:54 -0800 |
commit | d430753cba09acb07af8b313286f247c78a41a32 (patch) | |
tree | 182d064255c2e8298ce7119daf99c1a7c6a78899 /core/java/android/animation | |
parent | a55e3ed8bf726e61186d5bb2271c3eab597b44d9 (diff) | |
download | frameworks_base-d430753cba09acb07af8b313286f247c78a41a32.zip frameworks_base-d430753cba09acb07af8b313286f247c78a41a32.tar.gz frameworks_base-d430753cba09acb07af8b313286f247c78a41a32.tar.bz2 |
Add resource attributes for Keyframes and PropertyValuesHolders
Issue #17939329 Expose multi-property and multi-keyframe capabilities in animation resources
Change-Id: I14822ced47665fa6cde4996f74d3078da2ada38a
Diffstat (limited to 'core/java/android/animation')
-rw-r--r-- | core/java/android/animation/AnimatorInflater.java | 399 | ||||
-rw-r--r-- | core/java/android/animation/AnimatorSet.java | 12 | ||||
-rw-r--r-- | core/java/android/animation/ObjectAnimator.java | 21 | ||||
-rw-r--r-- | core/java/android/animation/ValueAnimator.java | 16 |
4 files changed, 417 insertions, 31 deletions
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index 688d7e4..6ef3da8 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -42,6 +42,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** * This class is used to instantiate animator XML files into Animator objects. @@ -66,8 +67,7 @@ public class AnimatorInflater { private static final int VALUE_TYPE_FLOAT = 0; private static final int VALUE_TYPE_INT = 1; private static final int VALUE_TYPE_PATH = 2; - private static final int VALUE_TYPE_COLOR = 4; - private static final int VALUE_TYPE_CUSTOM = 5; + private static final int VALUE_TYPE_COLOR = 3; private static final boolean DBG_ANIMATOR_INFLATER = false; @@ -296,39 +296,50 @@ public class AnimatorInflater { } } - /** - * @param anim The animator, must not be null - * @param arrayAnimator Incoming typed array for Animator's attributes. - * @param arrayObjectAnimator Incoming typed array for Object Animator's - * attributes. - * @param pixelSize The relative pixel size, used to calculate the - * maximum error for path animations. - */ - private static void parseAnimatorFromTypeArray(ValueAnimator anim, - TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) { - long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); - - long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); - - int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, - VALUE_TYPE_FLOAT); - - TypeEvaluator evaluator = null; + private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType, + int valueFromId, int valueToId, String propertyName) { boolean getFloats = (valueType == VALUE_TYPE_FLOAT); - TypedValue tvFrom = arrayAnimator.peekValue(R.styleable.Animator_valueFrom); + TypedValue tvFrom = styledAttributes.peekValue(valueFromId); boolean hasFrom = (tvFrom != null); int fromType = hasFrom ? tvFrom.type : 0; - TypedValue tvTo = arrayAnimator.peekValue(R.styleable.Animator_valueTo); + TypedValue tvTo = styledAttributes.peekValue(valueToId); boolean hasTo = (tvTo != null); int toType = hasTo ? tvTo.type : 0; - // TODO: Further clean up this part of code into 4 types : path, color, - // integer and float. + PropertyValuesHolder returnValue = null; + if (valueType == VALUE_TYPE_PATH) { - evaluator = setupAnimatorForPath(anim, arrayAnimator); + String fromString = styledAttributes.getString(valueFromId); + String toString = styledAttributes.getString(valueToId); + PathParser.PathDataNode[] nodesFrom = PathParser.createNodesFromPathData(fromString); + PathParser.PathDataNode[] nodesTo = PathParser.createNodesFromPathData(toString); + + if (nodesFrom != null || nodesTo != null) { + if (nodesFrom != null) { + TypeEvaluator evaluator = + new PathDataEvaluator(PathParser.deepCopyNodes(nodesFrom)); + if (nodesTo != null) { + if (!PathParser.canMorph(nodesFrom, nodesTo)) { + throw new InflateException(" Can't morph from " + fromString + " to " + + toString); + } + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + nodesFrom, nodesTo); + } else { + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + (Object) nodesFrom); + } + } else if (nodesTo != null) { + TypeEvaluator evaluator = + new PathDataEvaluator(PathParser.deepCopyNodes(nodesTo)); + returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, + (Object) nodesTo); + } + } } else { + TypeEvaluator evaluator = null; // Integer and float value types are handled here. if ((hasFrom && (fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) || @@ -338,7 +349,101 @@ public class AnimatorInflater { getFloats = false; evaluator = ArgbEvaluator.getInstance(); } - setupValues(anim, arrayAnimator, getFloats, hasFrom, fromType, hasTo, toType); + if (getFloats) { + float valueFrom; + float valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = styledAttributes.getDimension(valueFromId, 0f); + } else { + valueFrom = styledAttributes.getFloat(valueFromId, 0f); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = styledAttributes.getDimension(valueToId, 0f); + } else { + valueTo = styledAttributes.getFloat(valueToId, 0f); + } + returnValue = PropertyValuesHolder.ofFloat(propertyName, + valueFrom, valueTo); + } else { + returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom); + } + } else { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = styledAttributes.getDimension(valueToId, 0f); + } else { + valueTo = styledAttributes.getFloat(valueToId, 0f); + } + returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo); + } + } else { + int valueFrom; + int valueTo; + if (hasFrom) { + if (fromType == TypedValue.TYPE_DIMENSION) { + valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f); + } else if ((fromType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (fromType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueFrom = styledAttributes.getColor(valueFromId, 0); + } else { + valueFrom = styledAttributes.getInt(valueFromId, 0); + } + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) styledAttributes.getDimension(valueToId, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = styledAttributes.getColor(valueToId, 0); + } else { + valueTo = styledAttributes.getInt(valueToId, 0); + } + returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo); + } else { + returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom); + } + } else { + if (hasTo) { + if (toType == TypedValue.TYPE_DIMENSION) { + valueTo = (int) styledAttributes.getDimension(valueToId, 0f); + } else if ((toType >= TypedValue.TYPE_FIRST_COLOR_INT) && + (toType <= TypedValue.TYPE_LAST_COLOR_INT)) { + valueTo = styledAttributes.getColor(valueToId, 0); + } else { + valueTo = styledAttributes.getInt(valueToId, 0); + } + returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo); + } + } + } + if (returnValue != null && evaluator != null) { + returnValue.setEvaluator(evaluator); + } + } + + return returnValue; + } + + /** + * @param anim The animator, must not be null + * @param arrayAnimator Incoming typed array for Animator's attributes. + * @param arrayObjectAnimator Incoming typed array for Object Animator's + * attributes. + * @param pixelSize The relative pixel size, used to calculate the + * maximum error for path animations. + */ + private static void parseAnimatorFromTypeArray(ValueAnimator anim, + TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) { + long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300); + + long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0); + + int valueType = arrayAnimator.getInt(R.styleable.Animator_valueType, VALUE_TYPE_FLOAT); + + PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType, + R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, ""); + if (pvh != null) { + anim.setValues(pvh); } anim.setDuration(duration); @@ -353,12 +458,10 @@ public class AnimatorInflater { arrayAnimator.getInt(R.styleable.Animator_repeatMode, ValueAnimator.RESTART)); } - if (evaluator != null) { - anim.setEvaluator(evaluator); - } if (arrayObjectAnimator != null) { - setupObjectAnimator(anim, arrayObjectAnimator, getFloats, pixelSize); + setupObjectAnimator(anim, arrayObjectAnimator, valueType == VALUE_TYPE_FLOAT, + pixelSize); } } @@ -570,6 +673,7 @@ public class AnimatorInflater { } String name = parser.getName(); + boolean gotValues = false; if (name.equals("objectAnimator")) { anim = loadObjectAnimator(res, theme, attrs, pixelSize); @@ -588,11 +692,18 @@ public class AnimatorInflater { createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, pixelSize); a.recycle(); + } else if (name.equals("propertyValuesHolder")) { + PropertyValuesHolder[] values = loadValues(res, theme, parser, + Xml.asAttributeSet(parser)); + if (values != null && anim != null && (anim instanceof ValueAnimator)) { + ((ValueAnimator) anim).setValues(values); + } + gotValues = true; } else { throw new RuntimeException("Unknown animator name: " + parser.getName()); } - if (parent != null) { + if (parent != null && !gotValues) { if (childAnims == null) { childAnims = new ArrayList<Animator>(); } @@ -612,7 +723,233 @@ public class AnimatorInflater { } } return anim; + } + + private static PropertyValuesHolder[] loadValues(Resources res, Theme theme, + XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { + ArrayList<PropertyValuesHolder> values = null; + + int type; + while ((type = parser.getEventType()) != XmlPullParser.END_TAG && + type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + parser.next(); + continue; + } + + String name = parser.getName(); + + if (name.equals("propertyValuesHolder")) { + TypedArray a; + if (theme != null) { + a = theme.obtainStyledAttributes(attrs, R.styleable.PropertyValuesHolder, 0, 0); + } else { + a = res.obtainAttributes(attrs, R.styleable.PropertyValuesHolder); + } + String propertyName = a.getString(R.styleable.PropertyValuesHolder_propertyName); + int valueType = a.getInt(R.styleable.PropertyValuesHolder_valueType, + VALUE_TYPE_FLOAT); + PropertyValuesHolder pvh = loadPvh(res, theme, parser, propertyName, valueType); + if (pvh == null) { + pvh = getPVH(a, valueType, + R.styleable.PropertyValuesHolder_valueFrom, + R.styleable.PropertyValuesHolder_valueTo, propertyName); + } + if (pvh != null) { + if (values == null) { + values = new ArrayList<PropertyValuesHolder>(); + } + values.add(pvh); + } + a.recycle(); + } + + parser.next(); + } + + PropertyValuesHolder[] valuesArray = null; + if (values != null) { + int count = values.size(); + valuesArray = new PropertyValuesHolder[count]; + for (int i = 0; i < count; ++i) { + valuesArray[i] = values.get(i); + } + } + return valuesArray; + } + + private static void dumpKeyframes(Object[] keyframes, String header) { + if (keyframes == null || keyframes.length == 0) { + return; + } + Log.d(TAG, header); + int count = keyframes.length; + for (int i = 0; i < count; ++i) { + Keyframe keyframe = (Keyframe) keyframes[i]; + Log.d(TAG, "Keyframe " + i + ": fraction " + + (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " + + ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null")); + } + } + + private static PropertyValuesHolder loadPvh(Resources res, Theme theme, XmlPullParser parser, + String propertyName, int valueType) + throws XmlPullParserException, IOException { + + PropertyValuesHolder value = null; + ArrayList<Keyframe> keyframes = null; + + int type; + while ((type = parser.next()) != XmlPullParser.END_TAG && + type != XmlPullParser.END_DOCUMENT) { + String name = parser.getName(); + if (name.equals("keyframe")) { + Keyframe keyframe = loadKeyframe(res, theme, Xml.asAttributeSet(parser), valueType); + if (keyframe != null) { + if (keyframes == null) { + keyframes = new ArrayList<Keyframe>(); + } + keyframes.add(keyframe); + } + parser.next(); + } + } + + int count; + if (keyframes != null && (count = keyframes.size()) > 0) { + // make sure we have keyframes at 0 and 1 + // If we have keyframes with set fractions, add keyframes at start/end + // appropriately. If start/end have no set fractions: + // if there's only one keyframe, set its fraction to 1 and add one at 0 + // if >1 keyframe, set the last fraction to 1, the first fraction to 0 + Keyframe firstKeyframe = keyframes.get(0); + Keyframe lastKeyframe = keyframes.get(count - 1); + float endFraction = lastKeyframe.getFraction(); + if (endFraction < 1) { + if (endFraction < 0) { + lastKeyframe.setFraction(1); + } else { + keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1)); + ++count; + } + } + float startFraction = firstKeyframe.getFraction(); + if (startFraction != 0) { + if (startFraction < 0) { + firstKeyframe.setFraction(0); + } else { + keyframes.add(0, createNewKeyframe(firstKeyframe, 0)); + ++count; + } + } + Keyframe[] keyframeArray = new Keyframe[count]; + keyframes.toArray(keyframeArray); + for (int i = 0; i < count; ++i) { + Keyframe keyframe = keyframeArray[i]; + if (keyframe.getFraction() < 0) { + if (i == 0) { + keyframe.setFraction(0); + } else if (i == count - 1) { + keyframe.setFraction(1); + } else { + // figure out the start/end parameters of the current gap + // in fractions and distribute the gap among those keyframes + int startIndex = i; + int endIndex = i; + for (int j = startIndex + 1; j < count - 1; ++j) { + if (keyframeArray[j].getFraction() >= 0) { + break; + } + endIndex = j; + } + float gap = keyframeArray[endIndex + 1].getFraction() - + keyframeArray[startIndex - 1].getFraction(); + distributeKeyframes(keyframeArray, gap, startIndex, endIndex); + } + } + } + value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray); + if (valueType == VALUE_TYPE_COLOR) { + value.setEvaluator(ArgbEvaluator.getInstance()); + } + } + + return value; + } + + private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) { + return sampleKeyframe.getType() == float.class ? + Keyframe.ofFloat(fraction) : + (sampleKeyframe.getType() == int.class) ? + Keyframe.ofInt(fraction) : + Keyframe.ofObject(fraction); + } + + /** + * Utility function to set fractions on keyframes to cover a gap in which the + * fractions are not currently set. Keyframe fractions will be distributed evenly + * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap + * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the + * keyframe before startIndex. + * Assumptions: + * - First and last keyframe fractions (bounding this spread) are already set. So, + * for example, if no fractions are set, we will already set first and last keyframe + * fraction values to 0 and 1. + * - startIndex must be >0 (which follows from first assumption). + * - endIndex must be >= startIndex. + * + * @param keyframes the array of keyframes + * @param gap The total gap we need to distribute + * @param startIndex The index of the first keyframe whose fraction must be set + * @param endIndex The index of the last keyframe whose fraction must be set + */ + private static void distributeKeyframes(Keyframe[] keyframes, float gap, + int startIndex, int endIndex) { + int count = endIndex - startIndex + 2; + float increment = gap / count; + for (int i = startIndex; i <= endIndex; ++i) { + keyframes[i].setFraction(keyframes[i-1].getFraction() + increment); + } + } + + private static Keyframe loadKeyframe(Resources res, Theme theme, AttributeSet attrs, + int valueType) + throws XmlPullParserException, IOException { + + TypedArray a; + if (theme != null) { + a = theme.obtainStyledAttributes(attrs, R.styleable.Keyframe, 0, 0); + } else { + a = res.obtainAttributes(attrs, R.styleable.Keyframe); + } + + Keyframe keyframe = null; + + float fraction = a.getFloat(R.styleable.Keyframe_fraction, -1); + + boolean hasValue = a.peekValue(R.styleable.Keyframe_value) != null; + + if (hasValue) { + switch (valueType) { + case VALUE_TYPE_FLOAT: + float value = a.getFloat(R.styleable.Keyframe_value, 0); + keyframe = Keyframe.ofFloat(fraction, value); + break; + case VALUE_TYPE_COLOR: + case VALUE_TYPE_INT: + int intValue = a.getInt(R.styleable.Keyframe_value, 0); + keyframe = Keyframe.ofInt(fraction, intValue); + break; + } + } else { + keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) : + Keyframe.ofInt(fraction); + } + + a.recycle(); + return keyframe; } private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs, diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 92762c3..53d5237 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -972,6 +972,18 @@ public final class AnimatorSet extends Animator { } } + @Override + public String toString() { + String returnVal = "AnimatorSet@" + Integer.toHexString(hashCode()) + "{"; + boolean prevNeedsSort = mNeedsSort; + sortNodes(); + mNeedsSort = prevNeedsSort; + for (Node node : mSortedNodes) { + returnVal += "\n " + node.animation.toString(); + } + return returnVal + "\n}"; + } + /** * Dependency holds information about the node that some other node is * dependent upon and the nature of that dependency. diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index 59daaab..71855da 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -33,6 +33,27 @@ import java.util.ArrayList; * are then determined internally and the animation will call these functions as necessary to * animate the property. * + * <p>Animators can be created from either code or resource files, as shown here:</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator.xml ObjectAnimatorResources} + * + * <p>When using resource files, it is possible to use {@link PropertyValuesHolder} and + * {@link Keyframe} to create more complex animations. Using PropertyValuesHolders + * allows animators to animate several properties in parallel, as shown in this sample:</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh.xml + * PropertyValuesHolderResources} + * + * <p>Using Keyframes allows animations to follow more complex paths from the start + * to the end values. Note that you can specify explicit fractional values (from 0 to 1) for + * each keyframe to determine when, in the overall duration, the animation should arrive at that + * value. Alternatively, you can leave the fractions off and the keyframes will be equally + * distributed within the total duration. Also, a keyframe with no value will derive its value + * from the target object when the animator starts, just like animators with only one + * value specified.</p> + * + * {@sample development/samples/ApiDemos/res/anim/object_animator_pvh_kf.xml KeyframeResources} + * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about animating with {@code ObjectAnimator}, read the diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 8b70ae6..e4becf5 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -17,6 +17,7 @@ package android.animation; import android.content.res.ConfigurationBoundResourceCache; +import android.os.Debug; import android.os.Looper; import android.os.Trace; import android.util.AndroidRuntimeException; @@ -40,6 +41,21 @@ import java.util.HashMap; * out of an animation. This behavior can be changed by calling * {@link ValueAnimator#setInterpolator(TimeInterpolator)}.</p> * + * <p>Animators can be created from either code or resource files. Here is an example + * of a ValueAnimator resource file:</p> + * + * {@sample development/samples/ApiDemos/res/anim/animator.xml ValueAnimatorResources} + * + * <p>It is also possible to use a combination of {@link PropertyValuesHolder} and + * {@link Keyframe} resource tags to create a multi-step animation. + * Note that you can specify explicit fractional values (from 0 to 1) for + * each keyframe to determine when, in the overall duration, the animation should arrive at that + * value. Alternatively, you can leave the fractions off and the keyframes will be equally + * distributed within the total duration:</p> + * + * {@sample development/samples/ApiDemos/res/anim/value_animator_pvh_kf.xml + * ValueAnimatorKeyframeResources} + * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about animating with {@code ValueAnimator}, read the |