Use intrinsic size for path animation in AnimatedVectorDrawable

Bug 16984007

Animated Vector Drawables were using the viewport dimensions for
calculating the allowable animation error. Instead of using viewport
dimensions, it is better to use the intrinsic dimensions. Using
the viewport dimensions meant that a small viewport (e.g. 1x1)
would mean that animation paths within would only have an accuracy
of 50% of the dimensions of the drawable.

Change-Id: Id0152eabb4effd1e50c644eea7a371b38baeb7c1
diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java
index f4e4671..25417ed 100644
--- a/core/java/android/animation/AnimatorInflater.java
+++ b/core/java/android/animation/AnimatorInflater.java
@@ -92,21 +92,27 @@
      */
     public static Animator loadAnimator(Resources resources, Theme theme, int id)
             throws NotFoundException {
+        return loadAnimator(resources, theme, id, 1);
+    }
+
+    /** @hide */
+    public static Animator loadAnimator(Resources resources, Theme theme, int id,
+            float pathErrorScale) throws NotFoundException {
 
         XmlResourceParser parser = null;
         try {
             parser = resources.getAnimation(id);
-            return createAnimatorFromXml(resources, theme, parser);
+            return createAnimatorFromXml(resources, theme, parser, pathErrorScale);
         } catch (XmlPullParserException ex) {
             Resources.NotFoundException rnf =
                     new Resources.NotFoundException("Can't load animation resource ID #0x" +
-                    Integer.toHexString(id));
+                            Integer.toHexString(id));
             rnf.initCause(ex);
             throw rnf;
         } catch (IOException ex) {
             Resources.NotFoundException rnf =
                     new Resources.NotFoundException("Can't load animation resource ID #0x" +
-                    Integer.toHexString(id));
+                            Integer.toHexString(id));
             rnf.initCause(ex);
             throw rnf;
         } finally {
@@ -177,7 +183,7 @@
                         }
                         if (animator == null) {
                             animator = createAnimatorFromXml(context.getResources(),
-                                    context.getTheme(), parser);
+                                    context.getTheme(), parser, 1f);
                         }
 
                         if (animator == null) {
@@ -248,9 +254,11 @@
      * @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) {
+            TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
         long duration = arrayAnimator.getInt(R.styleable.Animator_duration, 300);
 
         long startDelay = arrayAnimator.getInt(R.styleable.Animator_startOffset, 0);
@@ -303,7 +311,7 @@
         }
 
         if (arrayObjectAnimator != null) {
-            setupObjectAnimator(anim, arrayObjectAnimator, getFloats);
+            setupObjectAnimator(anim, arrayObjectAnimator, getFloats, pixelSize);
         }
     }
 
@@ -351,9 +359,11 @@
      * @param anim The target Animator which will be updated.
      * @param arrayObjectAnimator TypedArray for the ObjectAnimator.
      * @param getFloats True if the value type is float.
+     * @param pixelSize The relative pixel size, used to calculate the
+     *                  maximum error for path animations.
      */
     private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator,
-            boolean getFloats) {
+            boolean getFloats, float pixelSize) {
         ObjectAnimator oa = (ObjectAnimator) anim;
         String pathData = arrayObjectAnimator.getString(R.styleable.PropertyAnimator_pathData);
 
@@ -370,7 +380,8 @@
                         + " propertyXName or propertyYName is needed for PathData");
             } else {
                 Path path = PathParser.createPathFromPathData(pathData);
-                PathKeyframes keyframeSet = KeyframeSet.ofPath(path);
+                float error = 0.5f * pixelSize; // max half a pixel error
+                PathKeyframes keyframeSet = KeyframeSet.ofPath(path, error);
                 Keyframes xKeyframes;
                 Keyframes yKeyframes;
                 if (getFloats) {
@@ -487,13 +498,15 @@
         }
     }
 
-    private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser)
+    private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
+            float pixelSize)
             throws XmlPullParserException, IOException {
-        return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0);
+        return createAnimatorFromXml(res, theme, parser, Xml.asAttributeSet(parser), null, 0,
+                pixelSize);
     }
 
     private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
-            AttributeSet attrs, AnimatorSet parent, int sequenceOrdering)
+            AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
             throws XmlPullParserException, IOException {
 
         Animator anim = null;
@@ -513,9 +526,9 @@
             String name = parser.getName();
 
             if (name.equals("objectAnimator")) {
-                anim = loadObjectAnimator(res, theme, attrs);
+                anim = loadObjectAnimator(res, theme, attrs, pixelSize);
             } else if (name.equals("animator")) {
-                anim = loadAnimator(res, theme, attrs, null);
+                anim = loadAnimator(res, theme, attrs, null, pixelSize);
             } else if (name.equals("set")) {
                 anim = new AnimatorSet();
                 TypedArray a;
@@ -526,7 +539,8 @@
                 }
                 int ordering = a.getInt(R.styleable.AnimatorSet_ordering,
                         TOGETHER);
-                createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering);
+                createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering,
+                        pixelSize);
                 a.recycle();
             } else {
                 throw new RuntimeException("Unknown animator name: " + parser.getName());
@@ -556,11 +570,11 @@
 
     }
 
-    private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs)
-            throws NotFoundException {
+    private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
+            float pathErrorScale) throws NotFoundException {
         ObjectAnimator anim = new ObjectAnimator();
 
-        loadAnimator(res, theme, attrs, anim);
+        loadAnimator(res, theme, attrs, anim, pathErrorScale);
 
         return anim;
     }
@@ -575,7 +589,7 @@
      *            ObjectAnimator
      */
     private static ValueAnimator loadAnimator(Resources res, Theme theme,
-            AttributeSet attrs, ValueAnimator anim)
+            AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
             throws NotFoundException {
 
         TypedArray arrayAnimator = null;
@@ -601,7 +615,7 @@
             anim = new ValueAnimator();
         }
 
-        parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator);
+        parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
 
         final int resID =
                 arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java
index fc9bbb1..8d15db2 100644
--- a/core/java/android/animation/KeyframeSet.java
+++ b/core/java/android/animation/KeyframeSet.java
@@ -154,6 +154,10 @@
         return new PathKeyframes(path);
     }
 
+    public static PathKeyframes ofPath(Path path, float error) {
+        return new PathKeyframes(path, error);
+    }
+
     /**
      * Sets the TypeEvaluator to be used when calculating animated values. This object
      * is required only for KeyframeSets that are not either IntKeyframeSet or FloatKeyframeSet,
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index ba22550..e5e2f18 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -252,6 +252,7 @@
             throws XmlPullParserException, IOException {
 
         int eventType = parser.getEventType();
+        float pathErrorScale = 1;
         while (eventType != XmlPullParser.END_DOCUMENT) {
             if (eventType == XmlPullParser.START_TAG) {
                 final String tagName = parser.getName();
@@ -261,9 +262,11 @@
                     int drawableRes = a.getResourceId(
                             R.styleable.AnimatedVectorDrawable_drawable, 0);
                     if (drawableRes != 0) {
-                        mAnimatedVectorState.mVectorDrawable = (VectorDrawable) res.getDrawable(
+                        VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
                                 drawableRes, theme).mutate();
-                        mAnimatedVectorState.mVectorDrawable.setAllowCaching(false);
+                        vectorDrawable.setAllowCaching(false);
+                        pathErrorScale = vectorDrawable.getPixelSize();
+                        mAnimatedVectorState.mVectorDrawable = vectorDrawable;
                     }
                     a.recycle();
                 } else if (TARGET.equals(tagName)) {
@@ -275,7 +278,8 @@
                     int id = a.getResourceId(
                             R.styleable.AnimatedVectorDrawableTarget_animation, 0);
                     if (id != 0) {
-                        Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id);
+                        Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id,
+                                pathErrorScale);
                         setupAnimatorsForTarget(target, objectAnimator);
                     }
                     a.recycle();
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index 042da5b..a07ccc4 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -368,6 +368,29 @@
         }
     }
 
+    /**
+     * The size of a pixel when scaled from the intrinsic dimension to the viewport dimension.
+     * This is used to calculate the path animation accuracy.
+     *
+     * @hide
+     */
+    public float getPixelSize() {
+        if (mVectorState == null && mVectorState.mVPathRenderer == null ||
+                mVectorState.mVPathRenderer.mBaseWidth == 0 ||
+                mVectorState.mVPathRenderer.mBaseHeight == 0 ||
+                mVectorState.mVPathRenderer.mViewportHeight == 0 ||
+                mVectorState.mVPathRenderer.mViewportWidth == 0) {
+            return 1; // fall back to 1:1 pixel mapping.
+        }
+        float intrinsicWidth = mVectorState.mVPathRenderer.mBaseWidth;
+        float intrinsicHeight = mVectorState.mVPathRenderer.mBaseHeight;
+        float viewportWidth = mVectorState.mVPathRenderer.mViewportWidth;
+        float viewportHeight = mVectorState.mVPathRenderer.mViewportHeight;
+        float scaleX = viewportWidth / intrinsicWidth;
+        float scaleY = viewportHeight / intrinsicHeight;
+        return Math.min(scaleX, scaleY);
+    }
+
     /** @hide */
     public static VectorDrawable create(Resources resources, int rid) {
         try {