AnimatedStateListDrawable.java revision 727cae197b123ef764a1f8fbe08a995b000d14c3
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 final int keyframeIndex = mState.indexOfKeyframe(stateSet); 143 if (keyframeIndex == getCurrentIndex()) { 144 // Propagate state change to current keyframe. 145 final Drawable current = getCurrent(); 146 if (current != null) { 147 return current.setState(stateSet); 148 } 149 return false; 150 } 151 152 // Attempt to find a valid transition to the keyframe. 153 if (selectTransition(keyframeIndex)) { 154 return true; 155 } 156 157 // No valid transition, attempt to jump directly to the keyframe. 158 if (selectDrawable(keyframeIndex)) { 159 return true; 160 } 161 162 return super.onStateChange(stateSet); 163 } 164 165 private boolean selectTransition(int toIndex) { 166 final int fromIndex; 167 final Transition currentTransition = mTransition; 168 if (currentTransition != null) { 169 if (toIndex == mTransitionToIndex) { 170 // Already animating to that keyframe. 171 return true; 172 } else if (toIndex == mTransitionFromIndex && currentTransition.canReverse()) { 173 // Reverse the current animation. 174 currentTransition.reverse(); 175 mTransitionToIndex = mTransitionFromIndex; 176 mTransitionFromIndex = toIndex; 177 return true; 178 } 179 180 // Start the next transition from the end of the current one. 181 fromIndex = mTransitionToIndex; 182 183 // Changing animation, end the current animation. 184 currentTransition.stop(); 185 } else { 186 fromIndex = getCurrentIndex(); 187 } 188 189 // Reset state. 190 mTransition = null; 191 mTransitionFromIndex = -1; 192 mTransitionToIndex = -1; 193 194 final AnimatedStateListState state = mState; 195 final int fromId = state.getKeyframeIdAt(fromIndex); 196 final int toId = state.getKeyframeIdAt(toIndex); 197 if (toId == 0 || fromId == 0) { 198 // Missing a keyframe ID. 199 return false; 200 } 201 202 final int transitionIndex = state.indexOfTransition(fromId, toId); 203 if (transitionIndex < 0) { 204 // Couldn't select a transition. 205 return false; 206 } 207 208 // This may fail if we're already on the transition, but that's okay! 209 selectDrawable(transitionIndex); 210 211 final Transition transition; 212 final Drawable d = getCurrent(); 213 if (d instanceof AnimationDrawable) { 214 final boolean reversed = state.isTransitionReversed(fromId, toId); 215 transition = new AnimationDrawableTransition((AnimationDrawable) d, reversed); 216 } else if (d instanceof AnimatedVectorDrawable) { 217 final boolean reversed = state.isTransitionReversed(fromId, toId); 218 transition = new AnimatedVectorDrawableTransition((AnimatedVectorDrawable) d, reversed); 219 } else if (d instanceof Animatable) { 220 transition = new AnimatableTransition((Animatable) d); 221 } else { 222 // We don't know how to animate this transition. 223 return false; 224 } 225 226 transition.start(); 227 228 mTransition = transition; 229 mTransitionFromIndex = fromIndex; 230 mTransitionToIndex = toIndex; 231 return true; 232 } 233 234 private static abstract class Transition { 235 public abstract void start(); 236 public abstract void stop(); 237 238 public void reverse() { 239 // Not supported by default. 240 } 241 242 public boolean canReverse() { 243 return false; 244 } 245 } 246 247 private static class AnimatableTransition extends Transition { 248 private final Animatable mA; 249 250 public AnimatableTransition(Animatable a) { 251 mA = a; 252 } 253 254 @Override 255 public void start() { 256 mA.start(); 257 } 258 259 @Override 260 public void stop() { 261 mA.stop(); 262 } 263 } 264 265 266 private static class AnimationDrawableTransition extends Transition { 267 private final ObjectAnimator mAnim; 268 269 public AnimationDrawableTransition(AnimationDrawable ad, boolean reversed) { 270 final int frameCount = ad.getNumberOfFrames(); 271 final int fromFrame = reversed ? frameCount - 1 : 0; 272 final int toFrame = reversed ? 0 : frameCount - 1; 273 final FrameInterpolator interp = new FrameInterpolator(ad, reversed); 274 final ObjectAnimator anim = ObjectAnimator.ofInt(ad, "currentIndex", fromFrame, toFrame); 275 anim.setAutoCancel(true); 276 anim.setDuration(interp.getTotalDuration()); 277 anim.setInterpolator(interp); 278 279 mAnim = anim; 280 } 281 282 @Override 283 public boolean canReverse() { 284 return true; 285 } 286 287 @Override 288 public void start() { 289 mAnim.start(); 290 } 291 292 @Override 293 public void reverse() { 294 mAnim.reverse(); 295 } 296 297 @Override 298 public void stop() { 299 mAnim.cancel(); 300 } 301 } 302 303 private static class AnimatedVectorDrawableTransition extends Transition { 304 private final AnimatedVectorDrawable mAvd; 305 private final boolean mReversed; 306 307 public AnimatedVectorDrawableTransition(AnimatedVectorDrawable avd, boolean reversed) { 308 mAvd = avd; 309 mReversed = reversed; 310 } 311 312 @Override 313 public boolean canReverse() { 314 return mAvd.canReverse(); 315 } 316 317 @Override 318 public void start() { 319 if (mReversed) { 320 reverse(); 321 } else { 322 mAvd.start(); 323 } 324 } 325 326 @Override 327 public void reverse() { 328 if (canReverse()) { 329 mAvd.reverse(); 330 } else { 331 Log.w(LOGTAG, "Reverse() is called on a drawable can't reverse"); 332 } 333 } 334 335 @Override 336 public void stop() { 337 mAvd.stop(); 338 } 339 } 340 341 342 @Override 343 public void jumpToCurrentState() { 344 super.jumpToCurrentState(); 345 346 if (mTransition != null) { 347 mTransition.stop(); 348 mTransition = null; 349 350 selectDrawable(mTransitionToIndex); 351 mTransitionToIndex = -1; 352 mTransitionFromIndex = -1; 353 } 354 } 355 356 @Override 357 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 358 @NonNull AttributeSet attrs, @Nullable Theme theme) 359 throws XmlPullParserException, IOException { 360 final TypedArray a = obtainAttributes( 361 r, theme, attrs, R.styleable.AnimatedStateListDrawable); 362 363 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); 364 365 final StateListState stateListState = getStateListState(); 366 stateListState.setVariablePadding(a.getBoolean( 367 R.styleable.AnimatedStateListDrawable_variablePadding, false)); 368 stateListState.setConstantSize(a.getBoolean( 369 R.styleable.AnimatedStateListDrawable_constantSize, false)); 370 stateListState.setEnterFadeDuration(a.getInt( 371 R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0)); 372 stateListState.setExitFadeDuration(a.getInt( 373 R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0)); 374 375 setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true)); 376 setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false)); 377 378 a.recycle(); 379 380 int type; 381 382 final int innerDepth = parser.getDepth() + 1; 383 int depth; 384 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 385 && ((depth = parser.getDepth()) >= innerDepth 386 || type != XmlPullParser.END_TAG)) { 387 if (type != XmlPullParser.START_TAG) { 388 continue; 389 } 390 391 if (depth > innerDepth) { 392 continue; 393 } 394 395 if (parser.getName().equals(ELEMENT_ITEM)) { 396 parseItem(r, parser, attrs, theme); 397 } else if (parser.getName().equals(ELEMENT_TRANSITION)) { 398 parseTransition(r, parser, attrs, theme); 399 } 400 } 401 402 onStateChange(getState()); 403 } 404 405 private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, 406 @NonNull AttributeSet attrs, @Nullable Theme theme) 407 throws XmlPullParserException, IOException { 408 int drawableRes = 0; 409 int fromId = 0; 410 int toId = 0; 411 boolean reversible = false; 412 413 final int numAttrs = attrs.getAttributeCount(); 414 for (int i = 0; i < numAttrs; i++) { 415 final int stateResId = attrs.getAttributeNameResource(i); 416 switch (stateResId) { 417 case 0: 418 break; 419 case R.attr.fromId: 420 fromId = attrs.getAttributeResourceValue(i, 0); 421 break; 422 case R.attr.toId: 423 toId = attrs.getAttributeResourceValue(i, 0); 424 break; 425 case R.attr.drawable: 426 drawableRes = attrs.getAttributeResourceValue(i, 0); 427 break; 428 case R.attr.reversible: 429 reversible = attrs.getAttributeBooleanValue(i, false); 430 break; 431 } 432 } 433 434 final Drawable dr; 435 if (drawableRes != 0) { 436 dr = r.getDrawable(drawableRes, theme); 437 } else { 438 int type; 439 while ((type = parser.next()) == XmlPullParser.TEXT) { 440 } 441 if (type != XmlPullParser.START_TAG) { 442 throw new XmlPullParserException( 443 parser.getPositionDescription() 444 + ": <item> tag requires a 'drawable' attribute or " 445 + "child tag defining a drawable"); 446 } 447 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 448 } 449 450 return mState.addTransition(fromId, toId, dr, reversible); 451 } 452 453 private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, 454 @NonNull AttributeSet attrs, @Nullable Theme theme) 455 throws XmlPullParserException, IOException { 456 int drawableRes = 0; 457 int keyframeId = 0; 458 459 int j = 0; 460 final int numAttrs = attrs.getAttributeCount(); 461 int[] states = new int[numAttrs]; 462 for (int i = 0; i < numAttrs; i++) { 463 final int stateResId = attrs.getAttributeNameResource(i); 464 switch (stateResId) { 465 case 0: 466 break; 467 case R.attr.id: 468 keyframeId = attrs.getAttributeResourceValue(i, 0); 469 break; 470 case R.attr.drawable: 471 drawableRes = attrs.getAttributeResourceValue(i, 0); 472 break; 473 default: 474 final boolean hasState = attrs.getAttributeBooleanValue(i, false); 475 states[j++] = hasState ? stateResId : -stateResId; 476 } 477 } 478 states = StateSet.trimStateSet(states, j); 479 480 final Drawable dr; 481 if (drawableRes != 0) { 482 dr = r.getDrawable(drawableRes, theme); 483 } else { 484 int type; 485 while ((type = parser.next()) == XmlPullParser.TEXT) { 486 } 487 if (type != XmlPullParser.START_TAG) { 488 throw new XmlPullParserException( 489 parser.getPositionDescription() 490 + ": <item> tag requires a 'drawable' attribute or " 491 + "child tag defining a drawable"); 492 } 493 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 494 } 495 496 return mState.addStateSet(states, dr, keyframeId); 497 } 498 499 @Override 500 public Drawable mutate() { 501 if (!mMutated && super.mutate() == this) { 502 final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null); 503 setConstantState(newState); 504 mMutated = true; 505 } 506 507 return this; 508 } 509 510 /** 511 * @hide 512 */ 513 public void clearMutated() { 514 super.clearMutated(); 515 mMutated = false; 516 } 517 518 static class AnimatedStateListState extends StateListState { 519 private static final int REVERSE_SHIFT = 32; 520 private static final int REVERSE_MASK = 0x1; 521 522 final LongSparseLongArray mTransitions; 523 final SparseIntArray mStateIds; 524 525 AnimatedStateListState(@Nullable AnimatedStateListState orig, 526 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { 527 super(orig, owner, res); 528 529 if (orig != null) { 530 mTransitions = orig.mTransitions.clone(); 531 mStateIds = orig.mStateIds.clone(); 532 } else { 533 mTransitions = new LongSparseLongArray(); 534 mStateIds = new SparseIntArray(); 535 } 536 } 537 538 int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { 539 final int pos = super.addChild(anim); 540 final long keyFromTo = generateTransitionKey(fromId, toId); 541 mTransitions.append(keyFromTo, pos); 542 543 if (reversible) { 544 final long keyToFrom = generateTransitionKey(toId, fromId); 545 mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT)); 546 } 547 548 return addChild(anim); 549 } 550 551 int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 552 final int index = super.addStateSet(stateSet, drawable); 553 mStateIds.put(index, id); 554 return index; 555 } 556 557 int indexOfKeyframe(@NonNull int[] stateSet) { 558 final int index = super.indexOfStateSet(stateSet); 559 if (index >= 0) { 560 return index; 561 } 562 563 return super.indexOfStateSet(StateSet.WILD_CARD); 564 } 565 566 int getKeyframeIdAt(int index) { 567 return index < 0 ? 0 : mStateIds.get(index, 0); 568 } 569 570 int indexOfTransition(int fromId, int toId) { 571 final long keyFromTo = generateTransitionKey(fromId, toId); 572 return (int) mTransitions.get(keyFromTo, -1); 573 } 574 575 boolean isTransitionReversed(int fromId, int toId) { 576 final long keyFromTo = generateTransitionKey(fromId, toId); 577 return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1; 578 } 579 580 @Override 581 public Drawable newDrawable() { 582 return new AnimatedStateListDrawable(this, null); 583 } 584 585 @Override 586 public Drawable newDrawable(Resources res) { 587 return new AnimatedStateListDrawable(this, res); 588 } 589 590 private static long generateTransitionKey(int fromId, int toId) { 591 return (long) fromId << 32 | toId; 592 } 593 } 594 595 void setConstantState(@NonNull AnimatedStateListState state) { 596 super.setConstantState(state); 597 598 mState = state; 599 } 600 601 private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { 602 super(null); 603 604 final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); 605 setConstantState(newState); 606 onStateChange(getState()); 607 jumpToCurrentState(); 608 } 609 610 /** 611 * Interpolates between frames with respect to their individual durations. 612 */ 613 private static class FrameInterpolator implements TimeInterpolator { 614 private int[] mFrameTimes; 615 private int mFrames; 616 private int mTotalDuration; 617 618 public FrameInterpolator(AnimationDrawable d, boolean reversed) { 619 updateFrames(d, reversed); 620 } 621 622 public int updateFrames(AnimationDrawable d, boolean reversed) { 623 final int N = d.getNumberOfFrames(); 624 mFrames = N; 625 626 if (mFrameTimes == null || mFrameTimes.length < N) { 627 mFrameTimes = new int[N]; 628 } 629 630 final int[] frameTimes = mFrameTimes; 631 int totalDuration = 0; 632 for (int i = 0; i < N; i++) { 633 final int duration = d.getDuration(reversed ? N - i - 1 : i); 634 frameTimes[i] = duration; 635 totalDuration += duration; 636 } 637 638 mTotalDuration = totalDuration; 639 return totalDuration; 640 } 641 642 public int getTotalDuration() { 643 return mTotalDuration; 644 } 645 646 @Override 647 public float getInterpolation(float input) { 648 final int elapsed = (int) (input * mTotalDuration + 0.5f); 649 final int N = mFrames; 650 final int[] frameTimes = mFrameTimes; 651 652 // Find the current frame and remaining time within that frame. 653 int remaining = elapsed; 654 int i = 0; 655 while (i < N && remaining >= frameTimes[i]) { 656 remaining -= frameTimes[i]; 657 i++; 658 } 659 660 // Remaining time is relative of total duration. 661 final float frameElapsed; 662 if (i < N) { 663 frameElapsed = remaining / (float) mTotalDuration; 664 } else { 665 frameElapsed = 0; 666 } 667 668 return i / (float) N + frameElapsed; 669 } 670 } 671} 672