1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package androidx.vectordrawable.graphics.drawable; 18 19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21import static java.lang.Math.min; 22 23import android.animation.Animator; 24import android.animation.AnimatorInflater; 25import android.animation.AnimatorSet; 26import android.animation.Keyframe; 27import android.animation.ObjectAnimator; 28import android.animation.PropertyValuesHolder; 29import android.animation.TypeEvaluator; 30import android.animation.ValueAnimator; 31import android.content.Context; 32import android.content.res.Resources; 33import android.content.res.Resources.NotFoundException; 34import android.content.res.Resources.Theme; 35import android.content.res.TypedArray; 36import android.content.res.XmlResourceParser; 37import android.graphics.Path; 38import android.graphics.PathMeasure; 39import android.os.Build; 40import android.util.AttributeSet; 41import android.util.Log; 42import android.util.TypedValue; 43import android.util.Xml; 44import android.view.InflateException; 45import android.view.animation.Interpolator; 46 47import androidx.annotation.AnimatorRes; 48import androidx.annotation.RestrictTo; 49import androidx.core.content.res.TypedArrayUtils; 50import androidx.core.graphics.PathParser; 51 52import org.xmlpull.v1.XmlPullParser; 53import org.xmlpull.v1.XmlPullParserException; 54 55import java.io.IOException; 56import java.util.ArrayList; 57 58/** 59 * This class is used to instantiate animator XML files into Animator objects. 60 * <p> 61 * For performance reasons, inflation relies heavily on pre-processing of 62 * XML files that is done at build time. Therefore, it is not currently possible 63 * to use this inflater with an XmlPullParser over a plain XML file at runtime; 64 * it only works with an XmlPullParser returned from a compiled resource (R. 65 * <em>something</em> file.) 66 * @hide 67 */ 68@RestrictTo(LIBRARY_GROUP) 69public class AnimatorInflaterCompat { 70 private static final String TAG = "AnimatorInflater"; 71 /** 72 * These flags are used when parsing AnimatorSet objects 73 */ 74 private static final int TOGETHER = 0; 75 private static final int MAX_NUM_POINTS = 100; 76 /** 77 * Enum values used in XML attributes to indicate the value for mValueType 78 */ 79 private static final int VALUE_TYPE_FLOAT = 0; 80 private static final int VALUE_TYPE_INT = 1; 81 private static final int VALUE_TYPE_PATH = 2; 82 private static final int VALUE_TYPE_COLOR = 3; 83 private static final int VALUE_TYPE_UNDEFINED = 4; 84 85 private static final boolean DBG_ANIMATOR_INFLATER = false; 86 87 /** 88 * Loads an {@link Animator} object from a context 89 * 90 * @param context Application context used to access resources 91 * @param id The resource id of the animation to load 92 * @return The animator object reference by the specified id 93 * @throws NotFoundException when the animation cannot be loaded 94 */ 95 public static Animator loadAnimator(Context context, @AnimatorRes int id) 96 throws NotFoundException { 97 Animator objectAnimator; 98 // Since AVDC will fall back onto AVD when API is >= 24, therefore, PathParser will need 99 // to match the accordingly to be able to call into the right setter/ getter for animation. 100 if (Build.VERSION.SDK_INT >= 24) { 101 objectAnimator = AnimatorInflater.loadAnimator(context, id); 102 } else { 103 objectAnimator = loadAnimator(context, context.getResources(), context.getTheme(), id); 104 } 105 return objectAnimator; 106 } 107 108 /** 109 * Loads an {@link Animator} object from a resource, context is for loading interpolator. 110 * 111 * @param resources The resources 112 * @param theme The theme 113 * @param id The resource id of the animation to load 114 * @return The animator object reference by the specified id 115 * @throws NotFoundException when the animation cannot be loaded 116 */ 117 public static Animator loadAnimator(Context context, Resources resources, Theme theme, 118 @AnimatorRes int id) throws NotFoundException { 119 return loadAnimator(context, resources, theme, id, 1); 120 } 121 122 /** 123 * Loads an {@link Animator} object from a resource, context is for loading interpolator. 124 */ 125 public static Animator loadAnimator(Context context, Resources resources, Theme theme, 126 @AnimatorRes int id, float pathErrorScale) throws NotFoundException { 127 Animator animator; 128 129 XmlResourceParser parser = null; 130 try { 131 parser = resources.getAnimation(id); 132 animator = createAnimatorFromXml(context, resources, theme, parser, pathErrorScale); 133 return animator; 134 } catch (XmlPullParserException ex) { 135 Resources.NotFoundException rnf = 136 new Resources.NotFoundException("Can't load animation resource ID #0x" 137 + Integer.toHexString(id)); 138 rnf.initCause(ex); 139 throw rnf; 140 } catch (IOException ex) { 141 Resources.NotFoundException rnf = 142 new Resources.NotFoundException("Can't load animation resource ID #0x" 143 + Integer.toHexString(id)); 144 rnf.initCause(ex); 145 throw rnf; 146 } finally { 147 if (parser != null) parser.close(); 148 } 149 } 150 151 /** 152 * PathDataEvaluator is used to interpolate between two paths which are 153 * represented in the same format but different control points' values. 154 * The path is represented as an array of PathDataNode here, which is 155 * fundamentally an array of floating point numbers. 156 */ 157 private static class PathDataEvaluator implements 158 TypeEvaluator<PathParser.PathDataNode[]> { 159 private PathParser.PathDataNode[] mNodeArray; 160 161 /** 162 * Create a PathParser.PathDataNode[] that does not reuse the animated value. 163 * Care must be taken when using this option because on every evaluation 164 * a new <code>PathParser.PathDataNode[]</code> will be allocated. 165 */ 166 private PathDataEvaluator() { 167 } 168 169 /** 170 * Create a PathDataEvaluator that reuses <code>nodeArray</code> for every evaluate() call. 171 * Caution must be taken to ensure that the value returned from 172 * {@link android.animation.ValueAnimator#getAnimatedValue()} is not cached, modified, or 173 * used across threads. The value will be modified on each <code>evaluate()</code> call. 174 * 175 * @param nodeArray The array to modify and return from <code>evaluate</code>. 176 */ 177 PathDataEvaluator(PathParser.PathDataNode[] nodeArray) { 178 mNodeArray = nodeArray; 179 } 180 181 @Override 182 public PathParser.PathDataNode[] evaluate(float fraction, 183 PathParser.PathDataNode[] startPathData, 184 PathParser.PathDataNode[] endPathData) { 185 if (!PathParser.canMorph(startPathData, endPathData)) { 186 throw new IllegalArgumentException("Can't interpolate between" 187 + " two incompatible pathData"); 188 } 189 190 if (mNodeArray == null || !PathParser.canMorph(mNodeArray, startPathData)) { 191 mNodeArray = PathParser.deepCopyNodes(startPathData); 192 } 193 194 for (int i = 0; i < startPathData.length; i++) { 195 mNodeArray[i].interpolatePathDataNode(startPathData[i], 196 endPathData[i], fraction); 197 } 198 199 return mNodeArray; 200 } 201 } 202 203 204 private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType, 205 int valueFromId, int valueToId, String propertyName) { 206 207 TypedValue tvFrom = styledAttributes.peekValue(valueFromId); 208 boolean hasFrom = (tvFrom != null); 209 int fromType = hasFrom ? tvFrom.type : 0; 210 TypedValue tvTo = styledAttributes.peekValue(valueToId); 211 boolean hasTo = (tvTo != null); 212 int toType = hasTo ? tvTo.type : 0; 213 214 if (valueType == VALUE_TYPE_UNDEFINED) { 215 // Check whether it's color type. If not, fall back to default type (i.e. float type) 216 if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) { 217 valueType = VALUE_TYPE_COLOR; 218 } else { 219 valueType = VALUE_TYPE_FLOAT; 220 } 221 } 222 223 boolean getFloats = (valueType == VALUE_TYPE_FLOAT); 224 225 PropertyValuesHolder returnValue = null; 226 227 if (valueType == VALUE_TYPE_PATH) { 228 String fromString = styledAttributes.getString(valueFromId); 229 String toString = styledAttributes.getString(valueToId); 230 231 PathParser.PathDataNode[] nodesFrom = 232 PathParser.createNodesFromPathData(fromString); 233 PathParser.PathDataNode[] nodesTo = 234 PathParser.createNodesFromPathData(toString); 235 if (nodesFrom != null || nodesTo != null) { 236 if (nodesFrom != null) { 237 TypeEvaluator evaluator = new PathDataEvaluator(); 238 if (nodesTo != null) { 239 if (!PathParser.canMorph(nodesFrom, nodesTo)) { 240 throw new InflateException(" Can't morph from " + fromString + " to " 241 + toString); 242 } 243 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 244 nodesFrom, nodesTo); 245 } else { 246 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 247 (Object) nodesFrom); 248 } 249 } else if (nodesTo != null) { 250 TypeEvaluator evaluator = new PathDataEvaluator(); 251 returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator, 252 (Object) nodesTo); 253 } 254 } 255 } else { 256 TypeEvaluator evaluator = null; 257 // Integer and float value types are handled here. 258 if (valueType == VALUE_TYPE_COLOR) { 259 // special case for colors: ignore valueType and get ints 260 evaluator = ArgbEvaluator.getInstance(); 261 } 262 if (getFloats) { 263 float valueFrom; 264 float valueTo; 265 if (hasFrom) { 266 if (fromType == TypedValue.TYPE_DIMENSION) { 267 valueFrom = styledAttributes.getDimension(valueFromId, 0f); 268 } else { 269 valueFrom = styledAttributes.getFloat(valueFromId, 0f); 270 } 271 if (hasTo) { 272 if (toType == TypedValue.TYPE_DIMENSION) { 273 valueTo = styledAttributes.getDimension(valueToId, 0f); 274 } else { 275 valueTo = styledAttributes.getFloat(valueToId, 0f); 276 } 277 returnValue = PropertyValuesHolder.ofFloat(propertyName, 278 valueFrom, valueTo); 279 } else { 280 returnValue = PropertyValuesHolder.ofFloat(propertyName, valueFrom); 281 } 282 } else { 283 if (toType == TypedValue.TYPE_DIMENSION) { 284 valueTo = styledAttributes.getDimension(valueToId, 0f); 285 } else { 286 valueTo = styledAttributes.getFloat(valueToId, 0f); 287 } 288 returnValue = PropertyValuesHolder.ofFloat(propertyName, valueTo); 289 } 290 } else { 291 int valueFrom; 292 int valueTo; 293 if (hasFrom) { 294 if (fromType == TypedValue.TYPE_DIMENSION) { 295 valueFrom = (int) styledAttributes.getDimension(valueFromId, 0f); 296 } else if (isColorType(fromType)) { 297 valueFrom = styledAttributes.getColor(valueFromId, 0); 298 } else { 299 valueFrom = styledAttributes.getInt(valueFromId, 0); 300 } 301 if (hasTo) { 302 if (toType == TypedValue.TYPE_DIMENSION) { 303 valueTo = (int) styledAttributes.getDimension(valueToId, 0f); 304 } else if (isColorType(toType)) { 305 valueTo = styledAttributes.getColor(valueToId, 0); 306 } else { 307 valueTo = styledAttributes.getInt(valueToId, 0); 308 } 309 returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom, valueTo); 310 } else { 311 returnValue = PropertyValuesHolder.ofInt(propertyName, valueFrom); 312 } 313 } else { 314 if (hasTo) { 315 if (toType == TypedValue.TYPE_DIMENSION) { 316 valueTo = (int) styledAttributes.getDimension(valueToId, 0f); 317 } else if (isColorType(toType)) { 318 valueTo = styledAttributes.getColor(valueToId, 0); 319 } else { 320 valueTo = styledAttributes.getInt(valueToId, 0); 321 } 322 returnValue = PropertyValuesHolder.ofInt(propertyName, valueTo); 323 } 324 } 325 } 326 if (returnValue != null && evaluator != null) { 327 returnValue.setEvaluator(evaluator); 328 } 329 } 330 331 return returnValue; 332 } 333 334 /** 335 * @param anim The animator, must not be null 336 * @param arrayAnimator Incoming typed array for Animator's attributes. 337 * @param arrayObjectAnimator Incoming typed array for Object Animator's 338 * attributes. 339 * @param pixelSize The relative pixel size, used to calculate the 340 * maximum error for path animations. 341 */ 342 private static void parseAnimatorFromTypeArray(ValueAnimator anim, 343 TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize, 344 XmlPullParser parser) { 345 long duration = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "duration", 346 AndroidResources.STYLEABLE_ANIMATOR_DURATION, 300); 347 long startDelay = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "startOffset", 348 AndroidResources.STYLEABLE_ANIMATOR_START_OFFSET, 0); 349 int valueType = TypedArrayUtils.getNamedInt(arrayAnimator, parser, "valueType", 350 AndroidResources.STYLEABLE_ANIMATOR_VALUE_TYPE, VALUE_TYPE_UNDEFINED); 351 352 // Change to requiring both value from and to, otherwise, throw exception for now. 353 if (TypedArrayUtils.hasAttribute(parser, "valueFrom") 354 && TypedArrayUtils.hasAttribute(parser, "valueTo")) { 355 if (valueType == VALUE_TYPE_UNDEFINED) { 356 valueType = inferValueTypeFromValues(arrayAnimator, 357 AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM, 358 AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO); 359 } 360 PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType, 361 AndroidResources.STYLEABLE_ANIMATOR_VALUE_FROM, 362 AndroidResources.STYLEABLE_ANIMATOR_VALUE_TO, ""); 363 if (pvh != null) { 364 anim.setValues(pvh); 365 } 366 } 367 anim.setDuration(duration); 368 anim.setStartDelay(startDelay); 369 370 anim.setRepeatCount(TypedArrayUtils.getNamedInt(arrayAnimator, parser, "repeatCount", 371 AndroidResources.STYLEABLE_ANIMATOR_REPEAT_COUNT, 0)); 372 anim.setRepeatMode(TypedArrayUtils.getNamedInt(arrayAnimator, parser, "repeatMode", 373 AndroidResources.STYLEABLE_ANIMATOR_REPEAT_MODE, ValueAnimator.RESTART)); 374 375 if (arrayObjectAnimator != null) { 376 setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize, parser); 377 } 378 } 379 380 381 /** 382 * Setup ObjectAnimator's property or values from pathData. 383 * 384 * @param anim The target Animator which will be updated. 385 * @param arrayObjectAnimator TypedArray for the ObjectAnimator. 386 * @param pixelSize The relative pixel size, used to calculate the 387 */ 388 private static void setupObjectAnimator(ValueAnimator anim, TypedArray arrayObjectAnimator, 389 int valueType, float pixelSize, XmlPullParser parser) { 390 ObjectAnimator oa = (ObjectAnimator) anim; 391 String pathData = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, "pathData", 392 AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA); 393 394 // Path can be involved in an ObjectAnimator in the following 3 ways: 395 // 1) Path morphing: the property to be animated is pathData, and valueFrom and valueTo 396 // are both of pathType. valueType = pathType needs to be explicitly defined. 397 // 2) A property in X or Y dimension can be animated along a path: the property needs to be 398 // defined in propertyXName or propertyYName attribute, the path will be defined in the 399 // pathData attribute. valueFrom and valueTo will not be necessary for this animation. 400 // 3) PathInterpolator can also define a path (in pathData) for its interpolation curve. 401 // Here we are dealing with case 2: 402 if (pathData != null) { 403 String propertyXName = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, 404 "propertyXName", AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME); 405 String propertyYName = TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, 406 "propertyYName", AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME); 407 408 409 if (valueType == VALUE_TYPE_PATH || valueType == VALUE_TYPE_UNDEFINED) { 410 // When pathData is defined, we are in case #2 mentioned above. ValueType can only 411 // be float type, or int type. Otherwise we fallback to default type. 412 valueType = VALUE_TYPE_FLOAT; 413 } 414 if (propertyXName == null && propertyYName == null) { 415 throw new InflateException(arrayObjectAnimator.getPositionDescription() 416 + " propertyXName or propertyYName is needed for PathData"); 417 } else { 418 Path path = PathParser.createPathFromPathData(pathData); 419 setupPathMotion(path, oa, 0.5f * pixelSize, propertyXName, propertyYName); 420 } 421 } else { 422 String propertyName = 423 TypedArrayUtils.getNamedString(arrayObjectAnimator, parser, "propertyName", 424 AndroidResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME); 425 oa.setPropertyName(propertyName); 426 } 427 428 429 return; 430 431 } 432 433 private static void setupPathMotion(Path path, ObjectAnimator oa, float precision, 434 String propertyXName, String propertyYName) { 435 // Measure the total length the whole path. 436 final PathMeasure measureForTotalLength = new PathMeasure(path, false); 437 float totalLength = 0; 438 // The sum of the previous contour plus the current one. Using the sum here b/c we want to 439 // directly substract from it later. 440 ArrayList<Float> contourLengths = new ArrayList<>(); 441 contourLengths.add(0f); 442 do { 443 final float pathLength = measureForTotalLength.getLength(); 444 totalLength += pathLength; 445 contourLengths.add(totalLength); 446 447 } while (measureForTotalLength.nextContour()); 448 449 // Now determine how many sample points we need, and the step for next sample. 450 final PathMeasure pathMeasure = new PathMeasure(path, false); 451 452 final int numPoints = min(MAX_NUM_POINTS, (int) (totalLength / precision) + 1); 453 454 float[] mX = new float[numPoints]; 455 float[] mY = new float[numPoints]; 456 final float[] position = new float[2]; 457 458 int contourIndex = 0; 459 float step = totalLength / (numPoints - 1); 460 float currentDistance = 0; 461 462 // For each sample point, determine whether we need to move on to next contour. 463 // After we find the right contour, then sample it using the current distance value minus 464 // the previously sampled contours' total length. 465 for (int i = 0; i < numPoints; ++i) { 466 pathMeasure.getPosTan(currentDistance, position, null); 467 468 mX[i] = position[0]; 469 mY[i] = position[1]; 470 currentDistance += step; 471 if ((contourIndex + 1) < contourLengths.size() 472 && currentDistance > contourLengths.get(contourIndex + 1)) { 473 currentDistance -= contourLengths.get(contourIndex + 1); 474 contourIndex++; 475 pathMeasure.nextContour(); 476 } 477 } 478 479 // Given the x and y value of the sample points, setup the ObjectAnimator properly. 480 PropertyValuesHolder x = null; 481 PropertyValuesHolder y = null; 482 if (propertyXName != null) { 483 x = PropertyValuesHolder.ofFloat(propertyXName, mX); 484 } 485 if (propertyYName != null) { 486 y = PropertyValuesHolder.ofFloat(propertyYName, mY); 487 } 488 if (x == null) { 489 oa.setValues(y); 490 } else if (y == null) { 491 oa.setValues(x); 492 } else { 493 oa.setValues(x, y); 494 } 495 } 496 497 private static Animator createAnimatorFromXml(Context context, Resources res, Theme theme, 498 XmlPullParser parser, 499 float pixelSize) 500 throws XmlPullParserException, IOException { 501 return createAnimatorFromXml(context, res, theme, parser, Xml.asAttributeSet(parser), null, 502 0, pixelSize); 503 } 504 505 private static Animator createAnimatorFromXml(Context context, Resources res, Theme theme, 506 XmlPullParser parser, 507 AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize) 508 throws XmlPullParserException, IOException { 509 Animator anim = null; 510 ArrayList<Animator> childAnims = null; 511 512 // Make sure we are on a start tag. 513 int type; 514 int depth = parser.getDepth(); 515 516 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 517 && type != XmlPullParser.END_DOCUMENT) { 518 519 if (type != XmlPullParser.START_TAG) { 520 continue; 521 } 522 523 String name = parser.getName(); 524 boolean gotValues = false; 525 526 if (name.equals("objectAnimator")) { 527 anim = loadObjectAnimator(context, res, theme, attrs, pixelSize, parser); 528 } else if (name.equals("animator")) { 529 anim = loadAnimator(context, res, theme, attrs, null, pixelSize, parser); 530 } else if (name.equals("set")) { 531 anim = new AnimatorSet(); 532 TypedArray a = TypedArrayUtils.obtainAttributes(res, theme, attrs, 533 AndroidResources.STYLEABLE_ANIMATOR_SET); 534 535 int ordering = TypedArrayUtils.getNamedInt(a, parser, "ordering", 536 AndroidResources.STYLEABLE_ANIMATOR_SET_ORDERING, TOGETHER); 537 538 createAnimatorFromXml(context, res, theme, parser, attrs, (AnimatorSet) anim, 539 ordering, pixelSize); 540 a.recycle(); 541 } else if (name.equals("propertyValuesHolder")) { 542 PropertyValuesHolder[] values = loadValues(context, res, theme, parser, 543 Xml.asAttributeSet(parser)); 544 if (values != null && anim != null && (anim instanceof ValueAnimator)) { 545 ((ValueAnimator) anim).setValues(values); 546 } 547 gotValues = true; 548 } else { 549 throw new RuntimeException("Unknown animator name: " + parser.getName()); 550 } 551 552 if (parent != null && !gotValues) { 553 if (childAnims == null) { 554 childAnims = new ArrayList<Animator>(); 555 } 556 childAnims.add(anim); 557 } 558 } 559 if (parent != null && childAnims != null) { 560 Animator[] animsArray = new Animator[childAnims.size()]; 561 int index = 0; 562 for (Animator a : childAnims) { 563 animsArray[index++] = a; 564 } 565 if (sequenceOrdering == TOGETHER) { 566 parent.playTogether(animsArray); 567 } else { 568 parent.playSequentially(animsArray); 569 } 570 } 571 return anim; 572 } 573 574 private static PropertyValuesHolder[] loadValues(Context context, Resources res, Theme theme, 575 XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { 576 ArrayList<PropertyValuesHolder> values = null; 577 578 int type; 579 while ((type = parser.getEventType()) != XmlPullParser.END_TAG 580 && type != XmlPullParser.END_DOCUMENT) { 581 582 if (type != XmlPullParser.START_TAG) { 583 parser.next(); 584 continue; 585 } 586 587 String name = parser.getName(); 588 589 if (name.equals("propertyValuesHolder")) { 590 TypedArray a = TypedArrayUtils.obtainAttributes(res, theme, attrs, 591 AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER); 592 593 String propertyName = TypedArrayUtils.getNamedString(a, parser, "propertyName", 594 AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME); 595 int valueType = TypedArrayUtils.getNamedInt(a, parser, "valueType", 596 AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE, 597 VALUE_TYPE_UNDEFINED); 598 599 PropertyValuesHolder pvh = loadPvh(context, res, theme, parser, propertyName, 600 valueType); 601 if (pvh == null) { 602 pvh = getPVH(a, valueType, 603 AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM, 604 AndroidResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO, 605 propertyName); 606 } 607 if (pvh != null) { 608 if (values == null) { 609 values = new ArrayList<PropertyValuesHolder>(); 610 } 611 values.add(pvh); 612 } 613 a.recycle(); 614 } 615 616 parser.next(); 617 } 618 619 PropertyValuesHolder[] valuesArray = null; 620 if (values != null) { 621 int count = values.size(); 622 valuesArray = new PropertyValuesHolder[count]; 623 for (int i = 0; i < count; ++i) { 624 valuesArray[i] = values.get(i); 625 } 626 } 627 return valuesArray; 628 } 629 630 // When no value type is provided in keyframe, we need to infer the type from the value. i.e. 631 // if value is defined in the style of a color value, then the color type is returned. 632 // Otherwise, default float type is returned. 633 private static int inferValueTypeOfKeyframe(Resources res, Theme theme, AttributeSet attrs, 634 XmlPullParser parser) { 635 int valueType; 636 TypedArray a = TypedArrayUtils.obtainAttributes(res, theme, attrs, 637 AndroidResources.STYLEABLE_KEYFRAME); 638 639 TypedValue keyframeValue = TypedArrayUtils.peekNamedValue(a, parser, "value", 640 AndroidResources.STYLEABLE_KEYFRAME_VALUE); 641 boolean hasValue = (keyframeValue != null); 642 // When no value type is provided, check whether it's a color type first. 643 // If not, fall back to default value type (i.e. float type). 644 if (hasValue && isColorType(keyframeValue.type)) { 645 valueType = VALUE_TYPE_COLOR; 646 } else { 647 valueType = VALUE_TYPE_FLOAT; 648 } 649 a.recycle(); 650 return valueType; 651 } 652 653 private static int inferValueTypeFromValues(TypedArray styledAttributes, int valueFromId, 654 int valueToId) { 655 TypedValue tvFrom = styledAttributes.peekValue(valueFromId); 656 boolean hasFrom = (tvFrom != null); 657 int fromType = hasFrom ? tvFrom.type : 0; 658 TypedValue tvTo = styledAttributes.peekValue(valueToId); 659 boolean hasTo = (tvTo != null); 660 int toType = hasTo ? tvTo.type : 0; 661 662 int valueType; 663 // Check whether it's color type. If not, fall back to default type (i.e. float type) 664 if ((hasFrom && isColorType(fromType)) || (hasTo && isColorType(toType))) { 665 valueType = VALUE_TYPE_COLOR; 666 } else { 667 valueType = VALUE_TYPE_FLOAT; 668 } 669 return valueType; 670 } 671 672 private static void dumpKeyframes(Object[] keyframes, String header) { 673 if (keyframes == null || keyframes.length == 0) { 674 return; 675 } 676 Log.d(TAG, header); 677 int count = keyframes.length; 678 for (int i = 0; i < count; ++i) { 679 Keyframe keyframe = (Keyframe) keyframes[i]; 680 Log.d(TAG, "Keyframe " + i + ": fraction " 681 + (keyframe.getFraction() < 0 ? "null" : keyframe.getFraction()) + ", " 682 + ", value : " + ((keyframe.hasValue()) ? keyframe.getValue() : "null")); 683 } 684 } 685 686 // Load property values holder if there are keyframes defined in it. Otherwise return null. 687 private static PropertyValuesHolder loadPvh(Context context, Resources res, Theme theme, 688 XmlPullParser parser, 689 String propertyName, int valueType) 690 throws XmlPullParserException, IOException { 691 692 PropertyValuesHolder value = null; 693 ArrayList<Keyframe> keyframes = null; 694 695 int type; 696 while ((type = parser.next()) != XmlPullParser.END_TAG 697 && type != XmlPullParser.END_DOCUMENT) { 698 String name = parser.getName(); 699 if (name.equals("keyframe")) { 700 if (valueType == VALUE_TYPE_UNDEFINED) { 701 valueType = inferValueTypeOfKeyframe(res, theme, Xml.asAttributeSet(parser), 702 parser); 703 } 704 Keyframe keyframe = loadKeyframe(context, res, theme, Xml.asAttributeSet(parser), 705 valueType, parser); 706 if (keyframe != null) { 707 if (keyframes == null) { 708 keyframes = new ArrayList<Keyframe>(); 709 } 710 keyframes.add(keyframe); 711 } 712 parser.next(); 713 } 714 } 715 716 int count; 717 if (keyframes != null && (count = keyframes.size()) > 0) { 718 // make sure we have keyframes at 0 and 1 719 // If we have keyframes with set fractions, add keyframes at start/end 720 // appropriately. If start/end have no set fractions: 721 // if there's only one keyframe, set its fraction to 1 and add one at 0 722 // if >1 keyframe, set the last fraction to 1, the first fraction to 0 723 Keyframe firstKeyframe = keyframes.get(0); 724 Keyframe lastKeyframe = keyframes.get(count - 1); 725 float endFraction = lastKeyframe.getFraction(); 726 if (endFraction < 1) { 727 if (endFraction < 0) { 728 lastKeyframe.setFraction(1); 729 } else { 730 keyframes.add(keyframes.size(), createNewKeyframe(lastKeyframe, 1)); 731 ++count; 732 } 733 } 734 float startFraction = firstKeyframe.getFraction(); 735 if (startFraction != 0) { 736 if (startFraction < 0) { 737 firstKeyframe.setFraction(0); 738 } else { 739 keyframes.add(0, createNewKeyframe(firstKeyframe, 0)); 740 ++count; 741 } 742 } 743 Keyframe[] keyframeArray = new Keyframe[count]; 744 keyframes.toArray(keyframeArray); 745 for (int i = 0; i < count; ++i) { 746 Keyframe keyframe = keyframeArray[i]; 747 if (keyframe.getFraction() < 0) { 748 if (i == 0) { 749 keyframe.setFraction(0); 750 } else if (i == count - 1) { 751 keyframe.setFraction(1); 752 } else { 753 // figure out the start/end parameters of the current gap 754 // in fractions and distribute the gap among those keyframes 755 int startIndex = i; 756 int endIndex = i; 757 for (int j = startIndex + 1; j < count - 1; ++j) { 758 if (keyframeArray[j].getFraction() >= 0) { 759 break; 760 } 761 endIndex = j; 762 } 763 float gap = keyframeArray[endIndex + 1].getFraction() 764 - keyframeArray[startIndex - 1].getFraction(); 765 distributeKeyframes(keyframeArray, gap, startIndex, endIndex); 766 } 767 } 768 } 769 value = PropertyValuesHolder.ofKeyframe(propertyName, keyframeArray); 770 if (valueType == VALUE_TYPE_COLOR) { 771 value.setEvaluator(ArgbEvaluator.getInstance()); 772 } 773 } 774 775 return value; 776 } 777 778 private static Keyframe createNewKeyframe(Keyframe sampleKeyframe, float fraction) { 779 return sampleKeyframe.getType() == float.class 780 ? Keyframe.ofFloat(fraction) : 781 (sampleKeyframe.getType() == int.class) 782 ? Keyframe.ofInt(fraction) : 783 Keyframe.ofObject(fraction); 784 } 785 786 /** 787 * Utility function to set fractions on keyframes to cover a gap in which the 788 * fractions are not currently set. Keyframe fractions will be distributed evenly 789 * in this gap. For example, a gap of 1 keyframe in the range 0-1 will be at .5, a gap 790 * of .6 spread between two keyframes will be at .2 and .4 beyond the fraction at the 791 * keyframe before startIndex. 792 * Assumptions: 793 * - First and last keyframe fractions (bounding this spread) are already set. So, 794 * for example, if no fractions are set, we will already set first and last keyframe 795 * fraction values to 0 and 1. 796 * - startIndex must be >0 (which follows from first assumption). 797 * - endIndex must be >= startIndex. 798 * 799 * @param keyframes the array of keyframes 800 * @param gap The total gap we need to distribute 801 * @param startIndex The index of the first keyframe whose fraction must be set 802 * @param endIndex The index of the last keyframe whose fraction must be set 803 */ 804 private static void distributeKeyframes(Keyframe[] keyframes, float gap, 805 int startIndex, int endIndex) { 806 int count = endIndex - startIndex + 2; 807 float increment = gap / count; 808 for (int i = startIndex; i <= endIndex; ++i) { 809 keyframes[i].setFraction(keyframes[i - 1].getFraction() + increment); 810 } 811 } 812 813 private static Keyframe loadKeyframe(Context context, Resources res, Theme theme, 814 AttributeSet attrs, 815 int valueType, XmlPullParser parser) 816 throws XmlPullParserException, IOException { 817 818 TypedArray a = TypedArrayUtils.obtainAttributes(res, theme, attrs, 819 AndroidResources.STYLEABLE_KEYFRAME); 820 821 Keyframe keyframe = null; 822 823 float fraction = TypedArrayUtils.getNamedFloat(a, parser, "fraction", 824 AndroidResources.STYLEABLE_KEYFRAME_FRACTION, -1); 825 826 TypedValue keyframeValue = TypedArrayUtils.peekNamedValue(a, parser, "value", 827 AndroidResources.STYLEABLE_KEYFRAME_VALUE); 828 boolean hasValue = (keyframeValue != null); 829 if (valueType == VALUE_TYPE_UNDEFINED) { 830 // When no value type is provided, check whether it's a color type first. 831 // If not, fall back to default value type (i.e. float type). 832 if (hasValue && isColorType(keyframeValue.type)) { 833 valueType = VALUE_TYPE_COLOR; 834 } else { 835 valueType = VALUE_TYPE_FLOAT; 836 } 837 } 838 839 if (hasValue) { 840 switch (valueType) { 841 case VALUE_TYPE_FLOAT: 842 float value = TypedArrayUtils.getNamedFloat(a, parser, "value", 843 AndroidResources.STYLEABLE_KEYFRAME_VALUE, 0); 844 keyframe = Keyframe.ofFloat(fraction, value); 845 break; 846 case VALUE_TYPE_COLOR: 847 case VALUE_TYPE_INT: 848 int intValue = TypedArrayUtils.getNamedInt(a, parser, "value", 849 AndroidResources.STYLEABLE_KEYFRAME_VALUE, 0); 850 keyframe = Keyframe.ofInt(fraction, intValue); 851 break; 852 } 853 } else { 854 keyframe = (valueType == VALUE_TYPE_FLOAT) ? Keyframe.ofFloat(fraction) : 855 Keyframe.ofInt(fraction); 856 } 857 858 final int resID = TypedArrayUtils.getNamedResourceId(a, parser, "interpolator", 859 AndroidResources.STYLEABLE_KEYFRAME_INTERPOLATOR, 0); 860 if (resID > 0) { 861 final Interpolator interpolator = AnimationUtilsCompat.loadInterpolator(context, resID); 862 keyframe.setInterpolator(interpolator); 863 } 864 a.recycle(); 865 866 return keyframe; 867 } 868 869 private static ObjectAnimator loadObjectAnimator(Context context, Resources res, Theme theme, 870 AttributeSet attrs, 871 float pathErrorScale, XmlPullParser parser) throws NotFoundException { 872 ObjectAnimator anim = new ObjectAnimator(); 873 874 loadAnimator(context, res, theme, attrs, anim, pathErrorScale, parser); 875 876 return anim; 877 } 878 879 /** 880 * Creates a new animation whose parameters come from the specified context 881 * and attributes set. 882 * 883 * @param res The resources 884 * @param attrs The set of attributes holding the animation parameters 885 * @param anim Null if this is a ValueAnimator, otherwise this is an 886 */ 887 private static ValueAnimator loadAnimator(Context context, Resources res, Theme theme, 888 AttributeSet attrs, ValueAnimator anim, float pathErrorScale, XmlPullParser parser) 889 throws NotFoundException { 890 TypedArray arrayAnimator = TypedArrayUtils.obtainAttributes(res, theme, attrs, 891 AndroidResources.STYLEABLE_ANIMATOR); 892 TypedArray arrayObjectAnimator = TypedArrayUtils.obtainAttributes(res, theme, attrs, 893 AndroidResources.STYLEABLE_PROPERTY_ANIMATOR); 894 895 if (anim == null) { 896 anim = new ValueAnimator(); 897 } 898 899 parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale, 900 parser); 901 902 final int resID = TypedArrayUtils.getNamedResourceId(arrayAnimator, parser, "interpolator", 903 AndroidResources.STYLEABLE_ANIMATOR_INTERPOLATOR, 0); 904 if (resID > 0) { 905 final Interpolator interpolator = AnimationUtilsCompat.loadInterpolator(context, resID); 906 anim.setInterpolator(interpolator); 907 } 908 909 arrayAnimator.recycle(); 910 if (arrayObjectAnimator != null) { 911 arrayObjectAnimator.recycle(); 912 } 913 return anim; 914 } 915 916 private static boolean isColorType(int type) { 917 return (type >= TypedValue.TYPE_FIRST_COLOR_INT) && (type 918 <= TypedValue.TYPE_LAST_COLOR_INT); 919 } 920 921 private AnimatorInflaterCompat() { 922 } 923} 924 925