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