AnimatedStateListDrawable.java revision 2d91f63ec20c4b06e87c80451a656462eceba17f
1/* 2 * Copyright (C) 2014 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 android.graphics.drawable; 18 19import android.animation.ObjectAnimator; 20import android.animation.TimeInterpolator; 21import android.annotation.NonNull; 22import android.annotation.Nullable; 23import android.content.res.Resources; 24import android.content.res.Resources.Theme; 25import android.content.res.TypedArray; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.util.LongSparseLongArray; 29import android.util.SparseIntArray; 30import android.util.StateSet; 31 32import com.android.internal.R; 33 34import org.xmlpull.v1.XmlPullParser; 35import org.xmlpull.v1.XmlPullParserException; 36 37import java.io.IOException; 38 39/** 40 * Drawable containing a set of Drawable keyframes where the currently displayed 41 * keyframe is chosen based on the current state set. Animations between 42 * keyframes may optionally be defined using transition elements. 43 * <p> 44 * This drawable can be defined in an XML file with the <code> 45 * <animated-selector></code> element. Each keyframe Drawable is defined in a 46 * nested <code><item></code> element. Transitions are defined in a nested 47 * <code><transition></code> element. 48 * 49 * @attr ref android.R.styleable#DrawableStates_state_focused 50 * @attr ref android.R.styleable#DrawableStates_state_window_focused 51 * @attr ref android.R.styleable#DrawableStates_state_enabled 52 * @attr ref android.R.styleable#DrawableStates_state_checkable 53 * @attr ref android.R.styleable#DrawableStates_state_checked 54 * @attr ref android.R.styleable#DrawableStates_state_selected 55 * @attr ref android.R.styleable#DrawableStates_state_activated 56 * @attr ref android.R.styleable#DrawableStates_state_active 57 * @attr ref android.R.styleable#DrawableStates_state_single 58 * @attr ref android.R.styleable#DrawableStates_state_first 59 * @attr ref android.R.styleable#DrawableStates_state_middle 60 * @attr ref android.R.styleable#DrawableStates_state_last 61 * @attr ref android.R.styleable#DrawableStates_state_pressed 62 */ 63public class AnimatedStateListDrawable extends StateListDrawable { 64 private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName(); 65 66 private static final String ELEMENT_TRANSITION = "transition"; 67 private static final String ELEMENT_ITEM = "item"; 68 69 private AnimatedStateListState mState; 70 71 /** The currently running transition, if any. */ 72 private Transition mTransition; 73 74 /** Index to be set after the transition ends. */ 75 private int mTransitionToIndex = -1; 76 77 /** Index away from which we are transitioning. */ 78 private int mTransitionFromIndex = -1; 79 80 private boolean mMutated; 81 82 public AnimatedStateListDrawable() { 83 this(null, null); 84 } 85 86 @Override 87 public boolean setVisible(boolean visible, boolean restart) { 88 final boolean changed = super.setVisible(visible, restart); 89 90 if (mTransition != null && (changed || restart)) { 91 if (visible) { 92 mTransition.start(); 93 } else { 94 // Ensure we're showing the correct state when visible. 95 jumpToCurrentState(); 96 } 97 } 98 99 return changed; 100 } 101 102 /** 103 * Add a new drawable to the set of keyframes. 104 * 105 * @param stateSet An array of resource IDs to associate with the keyframe 106 * @param drawable The drawable to show when in the specified state, may not be null 107 * @param id The unique identifier for the keyframe 108 */ 109 public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 110 if (drawable == null) { 111 throw new IllegalArgumentException("Drawable must not be null"); 112 } 113 114 mState.addStateSet(stateSet, drawable, id); 115 onStateChange(getState()); 116 } 117 118 /** 119 * Adds a new transition between keyframes. 120 * 121 * @param fromId Unique identifier of the starting keyframe 122 * @param toId Unique identifier of the ending keyframe 123 * @param transition An {@link Animatable} drawable to use as a transition, may not be null 124 * @param reversible Whether the transition can be reversed 125 */ 126 public <T extends Drawable & Animatable> void addTransition(int fromId, int toId, 127 @NonNull T transition, boolean reversible) { 128 if (transition == null) { 129 throw new IllegalArgumentException("Transition drawable must not be null"); 130 } 131 132 mState.addTransition(fromId, toId, transition, reversible); 133 } 134 135 @Override 136 public boolean isStateful() { 137 return true; 138 } 139 140 @Override 141 protected boolean onStateChange(int[] stateSet) { 142 // If we're not already at the target index, either attempt to find a 143 // valid transition to it or jump directly there. 144 final int targetIndex = mState.indexOfKeyframe(stateSet); 145 boolean changed = targetIndex != getCurrentIndex() 146 && (selectTransition(targetIndex) || selectDrawable(targetIndex)); 147 148 // We need to propagate the state change to the current drawable, but 149 // we can't call StateListDrawable.onStateChange() without changing the 150 // current drawable. 151 final Drawable current = getCurrent(); 152 if (current != null) { 153 changed |= current.setState(stateSet); 154 } 155 156 return changed; 157 } 158 159 private boolean selectTransition(int toIndex) { 160 final int fromIndex; 161 final Transition currentTransition = mTransition; 162 if (currentTransition != null) { 163 if (toIndex == mTransitionToIndex) { 164 // Already animating to that keyframe. 165 return true; 166 } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) { 167 // Reverse the current animation. 168 currentTransition.reverse(); 169 mTransitionToIndex = mTransitionFromIndex; 170 mTransitionFromIndex = toIndex; 171 return true; 172 } 173 174 // Start the next transition from the end of the current one. 175 fromIndex = mTransitionToIndex; 176 177 // Changing animation, end the current animation. 178 currentTransition.stop(); 179 } else { 180 fromIndex = getCurrentIndex(); 181 } 182 183 // Reset state. 184 mTransition = null; 185 mTransitionFromIndex = -1; 186 mTransitionToIndex = -1; 187 188 final AnimatedStateListState state = mState; 189 final int fromId = state.getKeyframeIdAt(fromIndex); 190 final int toId = state.getKeyframeIdAt(toIndex); 191 if (toId == 0 || fromId == 0) { 192 // Missing a keyframe ID. 193 return false; 194 } 195 196 final int transitionIndex = state.indexOfTransition(fromId, toId); 197 if (transitionIndex < 0) { 198 // Couldn't select a transition. 199 return false; 200 } 201 202 // This may fail if we're already on the transition, but that's okay! 203 selectDrawable(transitionIndex); 204 205 final Transition transition; 206 final Drawable d = getCurrent(); 207 if (d instanceof AnimationDrawable) { 208 final boolean reversed = state.isTransitionReversed(fromId, toId); 209 transition = new AnimationDrawableTransition((AnimationDrawable) d, reversed); 210 } else if (d instanceof AnimatedVectorDrawable) { 211 final boolean reversed = state.isTransitionReversed(fromId, toId); 212 transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, reversed); 213 } else if (d instanceof Animatable) { 214 transition = new AnimatableTransition((Animatable) d); 215 } else { 216 // We don't know how to animate this transition. 217 return false; 218 } 219 220 transition.start(); 221 222 mTransition = transition; 223 mTransitionFromIndex = fromIndex; 224 mTransitionToIndex = toIndex; 225 return true; 226 } 227 228 private static abstract class Transition { 229 public abstract void start(); 230 public abstract void stop(); 231 232 public void reverse() { 233 // Not supported by default. 234 } 235 236 public boolean canReverse() { 237 return false; 238 } 239 } 240 241 private static class AnimatableTransition extends Transition { 242 private final Animatable mA; 243 244 public AnimatableTransition(Animatable a) { 245 mA = a; 246 } 247 248 @Override 249 public void start() { 250 mA.start(); 251 } 252 253 @Override 254 public void stop() { 255 mA.stop(); 256 } 257 } 258 259 260 private static class AnimationDrawableTransition extends Transition { 261 private final ObjectAnimator mAnim; 262 263 public AnimationDrawableTransition(AnimationDrawable ad, boolean reversed) { 264 final int frameCount = ad.getNumberOfFrames(); 265 final int fromFrame = reversed ? frameCount - 1 : 0; 266 final int toFrame = reversed ? 0 : frameCount - 1; 267 final FrameInterpolator interp = new FrameInterpolator(ad, reversed); 268 final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame); 269 anim.setAutoCancel(true); 270 anim.setDuration(interp.getTotalDuration()); 271 anim.setInterpolator(interp); 272 273 mAnim = anim; 274 } 275 276 @Override 277 public boolean canReverse() { 278 return true; 279 } 280 281 @Override 282 public void start() { 283 mAnim.start(); 284 } 285 286 @Override 287 public void reverse() { 288 mAnim.reverse(); 289 } 290 291 @Override 292 public void stop() { 293 mAnim.cancel(); 294 } 295 } 296 297 private static class AnimatedVectorDrawableTransition extends Transition { 298 private final AnimatedVectorDrawable mAvd; 299 private final boolean mReversed; 300 301 public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed) { 302 mAvd = avd; 303 mReversed = reversed; 304 } 305 306 @Override 307 public boolean canReverse() { 308 return mAvd.canReverse(); 309 } 310 311 @Override 312 public void start() { 313 if (mReversed) { 314 reverse(); 315 } else { 316 mAvd.start(); 317 } 318 } 319 320 @Override 321 public void reverse() { 322 if (canReverse()) { 323 mAvd.reverse(); 324 } else { 325 Log.w(LOGTAG, "Reverse() is called on a drawable can't reverse"); 326 } 327 } 328 329 @Override 330 public void stop() { 331 mAvd.stop(); 332 } 333 } 334 335 336 @Override 337 public void jumpToCurrentState() { 338 super.jumpToCurrentState(); 339 340 if (mTransition != null) { 341 mTransition.stop(); 342 mTransition = null; 343 344 selectDrawable(mTransitionToIndex); 345 mTransitionToIndex = -1; 346 mTransitionFromIndex = -1; 347 } 348 } 349 350 @Override 351 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 352 @NonNull AttributeSet attrs, @Nullable Theme theme) 353 throws XmlPullParserException, IOException { 354 final TypedArray a = obtainAttributes( 355 r, theme, attrs, R.styleable.AnimatedStateListDrawable); 356 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); 357 updateStateFromTypedArray(a); 358 a.recycle(); 359 360 inflateChildElements(r, parser, attrs, theme); 361 362 init(); 363 } 364 365 @Override 366 public void applyTheme(@Nullable Theme theme) { 367 super.applyTheme(theme); 368 369 final AnimatedStateListState state = mState; 370 if (state == null || state.mAnimThemeAttrs == null) { 371 return; 372 } 373 374 final TypedArray a = theme.resolveAttributes( 375 state.mAnimThemeAttrs, R.styleable.AnimatedRotateDrawable); 376 updateStateFromTypedArray(a); 377 a.recycle(); 378 379 init(); 380 } 381 382 private void updateStateFromTypedArray(TypedArray a) { 383 final AnimatedStateListState state = mState; 384 385 // Account for any configuration changes. 386 state.mChangingConfigurations |= a.getChangingConfigurations(); 387 388 // Extract the theme attributes, if any. 389 state.mAnimThemeAttrs = a.extractThemeAttrs(); 390 391 state.setVariablePadding(a.getBoolean( 392 R.styleable.AnimatedStateListDrawable_variablePadding, state.mVariablePadding)); 393 state.setConstantSize(a.getBoolean( 394 R.styleable.AnimatedStateListDrawable_constantSize, state.mConstantSize)); 395 state.setEnterFadeDuration(a.getInt( 396 R.styleable.AnimatedStateListDrawable_enterFadeDuration, state.mEnterFadeDuration)); 397 state.setExitFadeDuration(a.getInt( 398 R.styleable.AnimatedStateListDrawable_exitFadeDuration, state.mExitFadeDuration)); 399 400 setDither(a.getBoolean( 401 R.styleable.AnimatedStateListDrawable_dither, state.mDither)); 402 setAutoMirrored(a.getBoolean( 403 R.styleable.AnimatedStateListDrawable_autoMirrored, state.mAutoMirrored)); 404 } 405 406 private void init() { 407 onStateChange(getState()); 408 } 409 410 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 411 Theme theme) throws XmlPullParserException, IOException { 412 int type; 413 414 final int innerDepth = parser.getDepth() + 1; 415 int depth; 416 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 417 && ((depth = parser.getDepth()) >= innerDepth 418 || type != XmlPullParser.END_TAG)) { 419 if (type != XmlPullParser.START_TAG) { 420 continue; 421 } 422 423 if (depth > innerDepth) { 424 continue; 425 } 426 427 if (parser.getName().equals(ELEMENT_ITEM)) { 428 parseItem(r, parser, attrs, theme); 429 } else if (parser.getName().equals(ELEMENT_TRANSITION)) { 430 parseTransition(r, parser, attrs, theme); 431 } 432 } 433 } 434 435 private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, 436 @NonNull AttributeSet attrs, @Nullable Theme theme) 437 throws XmlPullParserException, IOException { 438 // This allows state list drawable item elements to be themed at 439 // inflation time but does NOT make them work for Zygote preload. 440 final TypedArray a = obtainAttributes(r, theme, attrs, 441 R.styleable.AnimatedStateListDrawableTransition); 442 final int fromId = a.getResourceId( 443 R.styleable.AnimatedStateListDrawableTransition_fromId, 0); 444 final int toId = a.getResourceId( 445 R.styleable.AnimatedStateListDrawableTransition_toId, 0); 446 final boolean reversible = a.getBoolean( 447 R.styleable.AnimatedStateListDrawableTransition_reversible, false); 448 Drawable dr = a.getDrawable( 449 R.styleable.AnimatedStateListDrawableTransition_drawable); 450 a.recycle(); 451 452 // Loading child elements modifies the state of the AttributeSet's 453 // underlying parser, so it needs to happen after obtaining 454 // attributes and extracting states. 455 if (dr == null) { 456 int type; 457 while ((type = parser.next()) == XmlPullParser.TEXT) { 458 } 459 if (type != XmlPullParser.START_TAG) { 460 throw new XmlPullParserException( 461 parser.getPositionDescription() 462 + ": <transition> tag requires a 'drawable' attribute or " 463 + "child tag defining a drawable"); 464 } 465 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 466 } 467 468 return mState.addTransition(fromId, toId, dr, reversible); 469 } 470 471 private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, 472 @NonNull AttributeSet attrs, @Nullable Theme theme) 473 throws XmlPullParserException, IOException { 474 // This allows state list drawable item elements to be themed at 475 // inflation time but does NOT make them work for Zygote preload. 476 final TypedArray a = obtainAttributes(r, theme, attrs, 477 R.styleable.AnimatedStateListDrawableItem); 478 final int keyframeId = a.getResourceId(R.styleable.AnimatedStateListDrawableItem_id, 0); 479 Drawable dr = a.getDrawable(R.styleable.AnimatedStateListDrawableItem_drawable); 480 a.recycle(); 481 482 final int[] states = extractStateSet(attrs); 483 484 // Loading child elements modifies the state of the AttributeSet's 485 // underlying parser, so it needs to happen after obtaining 486 // attributes and extracting states. 487 if (dr == null) { 488 int type; 489 while ((type = parser.next()) == XmlPullParser.TEXT) { 490 } 491 if (type != XmlPullParser.START_TAG) { 492 throw new XmlPullParserException( 493 parser.getPositionDescription() 494 + ": <item> tag requires a 'drawable' attribute or " 495 + "child tag defining a drawable"); 496 } 497 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 498 } 499 500 return mState.addStateSet(states, dr, keyframeId); 501 } 502 503 @Override 504 public Drawable mutate() { 505 if (!mMutated && super.mutate() == this) { 506 final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null); 507 setConstantState(newState); 508 mMutated = true; 509 } 510 511 return this; 512 } 513 514 /** 515 * @hide 516 */ 517 public void clearMutated() { 518 super.clearMutated(); 519 mMutated = false; 520 } 521 522 static class AnimatedStateListState extends StateListState { 523 private static final int REVERSE_SHIFT = 32; 524 private static final int REVERSE_MASK = 0x1; 525 526 int[] mAnimThemeAttrs; 527 528 final LongSparseLongArray mTransitions; 529 final SparseIntArray mStateIds; 530 531 AnimatedStateListState(@Nullable AnimatedStateListState orig, 532 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { 533 super(orig, owner, res); 534 535 if (orig != null) { 536 mAnimThemeAttrs = orig.mAnimThemeAttrs; 537 mTransitions = orig.mTransitions.clone(); 538 mStateIds = orig.mStateIds.clone(); 539 } else { 540 mTransitions = new LongSparseLongArray(); 541 mStateIds = new SparseIntArray(); 542 } 543 } 544 545 int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { 546 final int pos = super.addChild(anim); 547 final long keyFromTo = generateTransitionKey(fromId, toId); 548 mTransitions.append(keyFromTo, pos); 549 550 if (reversible) { 551 final long keyToFrom = generateTransitionKey(toId, fromId); 552 mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT)); 553 } 554 555 return addChild(anim); 556 } 557 558 int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 559 final int index = super.addStateSet(stateSet, drawable); 560 mStateIds.put(index, id); 561 return index; 562 } 563 564 int indexOfKeyframe(@NonNull int[] stateSet) { 565 final int index = super.indexOfStateSet(stateSet); 566 if (index >= 0) { 567 return index; 568 } 569 570 return super.indexOfStateSet(StateSet.WILD_CARD); 571 } 572 573 int getKeyframeIdAt(int index) { 574 return index < 0 ? 0 : mStateIds.get(index, 0); 575 } 576 577 int indexOfTransition(int fromId, int toId) { 578 final long keyFromTo = generateTransitionKey(fromId, toId); 579 return (int) mTransitions.get(keyFromTo, -1); 580 } 581 582 boolean isTransitionReversed(int fromId, int toId) { 583 final long keyFromTo = generateTransitionKey(fromId, toId); 584 return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1; 585 } 586 587 @Override 588 public boolean canApplyTheme() { 589 return mAnimThemeAttrs != null || super.canApplyTheme(); 590 } 591 592 @Override 593 public Drawable newDrawable() { 594 return new AnimatedStateListDrawable(this, null); 595 } 596 597 @Override 598 public Drawable newDrawable(Resources res) { 599 return new AnimatedStateListDrawable(this, res); 600 } 601 602 private static long generateTransitionKey(int fromId, int toId) { 603 return (long) fromId << 32 | toId; 604 } 605 } 606 607 void setConstantState(@NonNull AnimatedStateListState state) { 608 super.setConstantState(state); 609 610 mState = state; 611 } 612 613 private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { 614 super(null); 615 616 final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); 617 setConstantState(newState); 618 onStateChange(getState()); 619 jumpToCurrentState(); 620 } 621 622 /** 623 * Interpolates between frames with respect to their individual durations. 624 */ 625 private static class FrameInterpolator implements TimeInterpolator { 626 private int[] mFrameTimes; 627 private int mFrames; 628 private int mTotalDuration; 629 630 public FrameInterpolator(AnimationDrawable d, boolean reversed) { 631 updateFrames(d, reversed); 632 } 633 634 public int updateFrames(AnimationDrawable d, boolean reversed) { 635 final int N = d.getNumberOfFrames(); 636 mFrames = N; 637 638 if (mFrameTimes == null || mFrameTimes.length < N) { 639 mFrameTimes = new int[N]; 640 } 641 642 final int[] frameTimes = mFrameTimes; 643 int totalDuration = 0; 644 for (int i = 0; i < N; i++) { 645 final int duration = d.getDuration(reversed ? N - i - 1 : i); 646 frameTimes[i] = duration; 647 totalDuration += duration; 648 } 649 650 mTotalDuration = totalDuration; 651 return totalDuration; 652 } 653 654 public int getTotalDuration() { 655 return mTotalDuration; 656 } 657 658 @Override 659 public float getInterpolation(float input) { 660 final int elapsed = (int) (input * mTotalDuration + 0.5f); 661 final int N = mFrames; 662 final int[] frameTimes = mFrameTimes; 663 664 // Find the current frame and remaining time within that frame. 665 int remaining = elapsed; 666 int i = 0; 667 while (i < N && remaining >= frameTimes[i]) { 668 remaining -= frameTimes[i]; 669 i++; 670 } 671 672 // Remaining time is relative of total duration. 673 final float frameElapsed; 674 if (i < N) { 675 frameElapsed = remaining / (float) mTotalDuration; 676 } else { 677 frameElapsed = 0; 678 } 679 680 return i / (float) N + frameElapsed; 681 } 682 } 683} 684