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