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