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