AnimatedStateListDrawable.java revision 7bc6a3f023ca3e1dde91fc97b6036dee3ba538a2
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.Log; 30import android.util.LongSparseLongArray; 31import android.util.SparseIntArray; 32import android.util.StateSet; 33 34import com.android.internal.R; 35 36import org.xmlpull.v1.XmlPullParser; 37import org.xmlpull.v1.XmlPullParserException; 38 39import java.io.IOException; 40 41/** 42 * Drawable containing a set of Drawable keyframes where the currently displayed 43 * keyframe is chosen based on the current state set. Animations between 44 * keyframes may optionally be defined using transition elements. 45 * <p> 46 * This drawable can be defined in an XML file with the <code> 47 * <animated-selector></code> element. Each keyframe Drawable is defined in a 48 * nested <code><item></code> element. Transitions are defined in a nested 49 * <code><transition></code> element. 50 * 51 * @attr ref android.R.styleable#DrawableStates_state_focused 52 * @attr ref android.R.styleable#DrawableStates_state_window_focused 53 * @attr ref android.R.styleable#DrawableStates_state_enabled 54 * @attr ref android.R.styleable#DrawableStates_state_checkable 55 * @attr ref android.R.styleable#DrawableStates_state_checked 56 * @attr ref android.R.styleable#DrawableStates_state_selected 57 * @attr ref android.R.styleable#DrawableStates_state_activated 58 * @attr ref android.R.styleable#DrawableStates_state_active 59 * @attr ref android.R.styleable#DrawableStates_state_single 60 * @attr ref android.R.styleable#DrawableStates_state_first 61 * @attr ref android.R.styleable#DrawableStates_state_middle 62 * @attr ref android.R.styleable#DrawableStates_state_last 63 * @attr ref android.R.styleable#DrawableStates_state_pressed 64 */ 65public class AnimatedStateListDrawable extends StateListDrawable { 66 private static final String LOGTAG = AnimatedStateListDrawable.class.getSimpleName(); 67 68 private static final String ELEMENT_TRANSITION = "transition"; 69 private static final String ELEMENT_ITEM = "item"; 70 71 private AnimatedStateListState mState; 72 73 /** The currently running transition, if any. */ 74 private Transition mTransition; 75 76 /** Index to be set after the transition ends. */ 77 private int mTransitionToIndex = -1; 78 79 /** Index away from which we are transitioning. */ 80 private int mTransitionFromIndex = -1; 81 82 private boolean mMutated; 83 84 public AnimatedStateListDrawable() { 85 this(null, null); 86 } 87 88 @Override 89 public boolean setVisible(boolean visible, boolean restart) { 90 final boolean changed = super.setVisible(visible, restart); 91 92 if (mTransition != null && (changed || restart)) { 93 if (visible) { 94 mTransition.start(); 95 } else { 96 mTransition.stop(); 97 } 98 } 99 100 return changed; 101 } 102 103 /** 104 * Add a new drawable to the set of keyframes. 105 * 106 * @param stateSet An array of resource IDs to associate with the keyframe 107 * @param drawable The drawable to show when in the specified state, may not be null 108 * @param id The unique identifier for the keyframe 109 */ 110 public void addState(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 111 if (drawable == null) { 112 throw new IllegalArgumentException("Drawable must not be null"); 113 } 114 115 mState.addStateSet(stateSet, drawable, id); 116 onStateChange(getState()); 117 } 118 119 /** 120 * Adds a new transition between keyframes. 121 * 122 * @param fromId Unique identifier of the starting keyframe 123 * @param toId Unique identifier of the ending keyframe 124 * @param transition An animatable drawable to use as a transition, may not be null 125 * @param reversible Whether the transition can be reversed 126 */ 127 public void addTransition(int fromId, int toId, @NonNull Drawable transition, 128 boolean reversible) { 129 if (transition == null) { 130 throw new IllegalArgumentException("Transition drawable must not be null"); 131 } 132 133 mState.addTransition(fromId, toId, transition, reversible); 134 } 135 136 @Override 137 public boolean isStateful() { 138 return true; 139 } 140 141 @Override 142 protected boolean onStateChange(int[] stateSet) { 143 final int keyframeIndex = mState.indexOfKeyframe(stateSet); 144 if (keyframeIndex == getCurrentIndex()) { 145 // No transition needed. 146 return false; 147 } 148 149 // Attempt to find a valid transition to the keyframe. 150 if (selectTransition(keyframeIndex)) { 151 return true; 152 } 153 154 // No valid transition, attempt to jump directly to the keyframe. 155 if (selectDrawable(keyframeIndex)) { 156 return true; 157 } 158 159 return super.onStateChange(stateSet); 160 } 161 162 private boolean selectTransition(int toIndex) { 163 if (toIndex == mTransitionToIndex) { 164 // Already animating to that keyframe. 165 return true; 166 } 167 168 final Transition currentTransition = mTransition; 169 if (currentTransition != null) { 170 if (toIndex == mTransitionToIndex) { 171 return true; 172 } else if (toIndex == mTransitionFromIndex) { 173 // Reverse the current animation. 174 currentTransition.reverse(); 175 mTransitionFromIndex = mTransitionToIndex; 176 mTransitionToIndex = toIndex; 177 return true; 178 } 179 180 // Changing animation, end the current animation. 181 currentTransition.stop(); 182 mTransition = null; 183 } 184 185 // Reset state. 186 mTransitionFromIndex = -1; 187 mTransitionToIndex = -1; 188 189 final AnimatedStateListState state = mState; 190 final int fromIndex = getCurrentIndex(); 191 final int fromId = state.getKeyframeIdAt(fromIndex); 192 final int toId = state.getKeyframeIdAt(toIndex); 193 194 if (toId == 0 || fromId == 0) { 195 // Missing a keyframe ID. 196 return false; 197 } 198 199 final int transitionIndex = state.indexOfTransition(fromId, toId); 200 if (transitionIndex < 0 || !selectDrawable(transitionIndex)) { 201 // Couldn't select a transition. 202 return false; 203 } 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 = r.obtainAttributes(attrs, R.styleable.AnimatedStateListDrawable); 355 356 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedStateListDrawable_visible); 357 358 final StateListState stateListState = getStateListState(); 359 stateListState.setVariablePadding(a.getBoolean( 360 R.styleable.AnimatedStateListDrawable_variablePadding, false)); 361 stateListState.setConstantSize(a.getBoolean( 362 R.styleable.AnimatedStateListDrawable_constantSize, false)); 363 stateListState.setEnterFadeDuration(a.getInt( 364 R.styleable.AnimatedStateListDrawable_enterFadeDuration, 0)); 365 stateListState.setExitFadeDuration(a.getInt( 366 R.styleable.AnimatedStateListDrawable_exitFadeDuration, 0)); 367 368 setDither(a.getBoolean(R.styleable.AnimatedStateListDrawable_dither, true)); 369 setAutoMirrored(a.getBoolean(R.styleable.AnimatedStateListDrawable_autoMirrored, false)); 370 371 a.recycle(); 372 373 int type; 374 375 final int innerDepth = parser.getDepth() + 1; 376 int depth; 377 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 378 && ((depth = parser.getDepth()) >= innerDepth 379 || type != XmlPullParser.END_TAG)) { 380 if (type != XmlPullParser.START_TAG) { 381 continue; 382 } 383 384 if (depth > innerDepth) { 385 continue; 386 } 387 388 if (parser.getName().equals(ELEMENT_ITEM)) { 389 parseItem(r, parser, attrs, theme); 390 } else if (parser.getName().equals(ELEMENT_TRANSITION)) { 391 parseTransition(r, parser, attrs, theme); 392 } 393 } 394 395 onStateChange(getState()); 396 } 397 398 private int parseTransition(@NonNull Resources r, @NonNull XmlPullParser parser, 399 @NonNull AttributeSet attrs, @Nullable Theme theme) 400 throws XmlPullParserException, IOException { 401 int drawableRes = 0; 402 int fromId = 0; 403 int toId = 0; 404 boolean reversible = false; 405 406 final int numAttrs = attrs.getAttributeCount(); 407 for (int i = 0; i < numAttrs; i++) { 408 final int stateResId = attrs.getAttributeNameResource(i); 409 switch (stateResId) { 410 case 0: 411 break; 412 case R.attr.fromId: 413 fromId = attrs.getAttributeResourceValue(i, 0); 414 break; 415 case R.attr.toId: 416 toId = attrs.getAttributeResourceValue(i, 0); 417 break; 418 case R.attr.drawable: 419 drawableRes = attrs.getAttributeResourceValue(i, 0); 420 break; 421 case R.attr.reversible: 422 reversible = attrs.getAttributeBooleanValue(i, false); 423 break; 424 } 425 } 426 427 final Drawable dr; 428 if (drawableRes != 0) { 429 dr = r.getDrawable(drawableRes); 430 } else { 431 int type; 432 while ((type = parser.next()) == XmlPullParser.TEXT) { 433 } 434 if (type != XmlPullParser.START_TAG) { 435 throw new XmlPullParserException( 436 parser.getPositionDescription() 437 + ": <item> tag requires a 'drawable' attribute or " 438 + "child tag defining a drawable"); 439 } 440 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 441 } 442 443 return mState.addTransition(fromId, toId, dr, reversible); 444 } 445 446 private int parseItem(@NonNull Resources r, @NonNull XmlPullParser parser, 447 @NonNull AttributeSet attrs, @Nullable Theme theme) 448 throws XmlPullParserException, IOException { 449 int drawableRes = 0; 450 int keyframeId = 0; 451 452 int j = 0; 453 final int numAttrs = attrs.getAttributeCount(); 454 int[] states = new int[numAttrs]; 455 for (int i = 0; i < numAttrs; i++) { 456 final int stateResId = attrs.getAttributeNameResource(i); 457 switch (stateResId) { 458 case 0: 459 break; 460 case R.attr.id: 461 keyframeId = attrs.getAttributeResourceValue(i, 0); 462 break; 463 case R.attr.drawable: 464 drawableRes = attrs.getAttributeResourceValue(i, 0); 465 break; 466 default: 467 final boolean hasState = attrs.getAttributeBooleanValue(i, false); 468 states[j++] = hasState ? stateResId : -stateResId; 469 } 470 } 471 states = StateSet.trimStateSet(states, j); 472 473 final Drawable dr; 474 if (drawableRes != 0) { 475 dr = r.getDrawable(drawableRes); 476 } else { 477 int type; 478 while ((type = parser.next()) == XmlPullParser.TEXT) { 479 } 480 if (type != XmlPullParser.START_TAG) { 481 throw new XmlPullParserException( 482 parser.getPositionDescription() 483 + ": <item> tag requires a 'drawable' attribute or " 484 + "child tag defining a drawable"); 485 } 486 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 487 } 488 489 return mState.addStateSet(states, dr, keyframeId); 490 } 491 492 @Override 493 public Drawable mutate() { 494 if (!mMutated) { 495 final AnimatedStateListState newState = new AnimatedStateListState(mState, this, null); 496 setConstantState(newState); 497 mMutated = true; 498 } 499 500 return this; 501 } 502 503 private final AnimatorListenerAdapter mAnimListener = new AnimatorListenerAdapter() { 504 @Override 505 public void onAnimationEnd(Animator anim) { 506 selectDrawable(mTransitionToIndex); 507 508 mTransitionToIndex = -1; 509 mTransitionFromIndex = -1; 510 mTransition = null; 511 } 512 }; 513 514 static class AnimatedStateListState extends StateListState { 515 private static final int REVERSE_SHIFT = 32; 516 private static final int REVERSE_MASK = 0x1; 517 518 final LongSparseLongArray mTransitions; 519 final SparseIntArray mStateIds; 520 521 AnimatedStateListState(@Nullable AnimatedStateListState orig, 522 @NonNull AnimatedStateListDrawable owner, @Nullable Resources res) { 523 super(orig, owner, res); 524 525 if (orig != null) { 526 mTransitions = orig.mTransitions.clone(); 527 mStateIds = orig.mStateIds.clone(); 528 } else { 529 mTransitions = new LongSparseLongArray(); 530 mStateIds = new SparseIntArray(); 531 } 532 } 533 534 int addTransition(int fromId, int toId, @NonNull Drawable anim, boolean reversible) { 535 final int pos = super.addChild(anim); 536 final long keyFromTo = generateTransitionKey(fromId, toId); 537 mTransitions.append(keyFromTo, pos); 538 539 if (reversible) { 540 final long keyToFrom = generateTransitionKey(toId, fromId); 541 mTransitions.append(keyToFrom, pos | (1L << REVERSE_SHIFT)); 542 } 543 544 return addChild(anim); 545 } 546 547 int addStateSet(@NonNull int[] stateSet, @NonNull Drawable drawable, int id) { 548 final int index = super.addStateSet(stateSet, drawable); 549 mStateIds.put(index, id); 550 return index; 551 } 552 553 int indexOfKeyframe(@NonNull int[] stateSet) { 554 final int index = super.indexOfStateSet(stateSet); 555 if (index >= 0) { 556 return index; 557 } 558 559 return super.indexOfStateSet(StateSet.WILD_CARD); 560 } 561 562 int getKeyframeIdAt(int index) { 563 return index < 0 ? 0 : mStateIds.get(index, 0); 564 } 565 566 int indexOfTransition(int fromId, int toId) { 567 final long keyFromTo = generateTransitionKey(fromId, toId); 568 return (int) mTransitions.get(keyFromTo, -1); 569 } 570 571 boolean isTransitionReversed(int fromId, int toId) { 572 final long keyFromTo = generateTransitionKey(fromId, toId); 573 return (mTransitions.get(keyFromTo, -1) >> REVERSE_SHIFT & REVERSE_MASK) == 1; 574 } 575 576 @Override 577 public Drawable newDrawable() { 578 return new AnimatedStateListDrawable(this, null); 579 } 580 581 @Override 582 public Drawable newDrawable(Resources res) { 583 return new AnimatedStateListDrawable(this, res); 584 } 585 586 private static long generateTransitionKey(int fromId, int toId) { 587 return (long) fromId << 32 | toId; 588 } 589 } 590 591 void setConstantState(@NonNull AnimatedStateListState state) { 592 super.setConstantState(state); 593 594 mState = state; 595 } 596 597 private AnimatedStateListDrawable(@Nullable AnimatedStateListState state, @Nullable Resources res) { 598 super(null); 599 600 final AnimatedStateListState newState = new AnimatedStateListState(state, this, res); 601 setConstantState(newState); 602 onStateChange(getState()); 603 jumpToCurrentState(); 604 } 605 606 /** 607 * Interpolates between frames with respect to their individual durations. 608 */ 609 private static class FrameInterpolator implements TimeInterpolator { 610 private int[] mFrameTimes; 611 private int mFrames; 612 private int mTotalDuration; 613 614 public FrameInterpolator(AnimationDrawable d, boolean reversed) { 615 updateFrames(d, reversed); 616 } 617 618 public int updateFrames(AnimationDrawable d, boolean reversed) { 619 final int N = d.getNumberOfFrames(); 620 mFrames = N; 621 622 if (mFrameTimes == null || mFrameTimes.length < N) { 623 mFrameTimes = new int[N]; 624 } 625 626 final int[] frameTimes = mFrameTimes; 627 int totalDuration = 0; 628 for (int i = 0; i < N; i++) { 629 final int duration = d.getDuration(reversed ? N - i - 1 : i); 630 frameTimes[i] = duration; 631 totalDuration += duration; 632 } 633 634 mTotalDuration = totalDuration; 635 return totalDuration; 636 } 637 638 public int getTotalDuration() { 639 return mTotalDuration; 640 } 641 642 @Override 643 public float getInterpolation(float input) { 644 final int elapsed = (int) (input * mTotalDuration + 0.5f); 645 final int N = mFrames; 646 final int[] frameTimes = mFrameTimes; 647 648 // Find the current frame and remaining time within that frame. 649 int remaining = elapsed; 650 int i = 0; 651 while (i < N && remaining >= frameTimes[i]) { 652 remaining -= frameTimes[i]; 653 i++; 654 } 655 656 // Remaining time is relative of total duration. 657 final float frameElapsed; 658 if (i < N) { 659 frameElapsed = remaining / (float) mTotalDuration; 660 } else { 661 frameElapsed = 0; 662 } 663 664 return i / (float) N + frameElapsed; 665 } 666 } 667} 668