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