1 /* 2 * Copyright (C) 2018 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 17 package android.graphics.drawable; 18 19 import android.annotation.IntRange; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.content.res.AssetFileDescriptor; 23 import android.content.res.Resources; 24 import android.content.res.Resources.Theme; 25 import android.content.res.TypedArray; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.ColorFilter; 29 import android.graphics.ImageDecoder; 30 import android.graphics.PixelFormat; 31 import android.graphics.Rect; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.SystemClock; 35 import android.util.AttributeSet; 36 import android.util.DisplayMetrics; 37 import android.util.TypedValue; 38 import android.view.View; 39 40 import com.android.internal.R; 41 42 import dalvik.annotation.optimization.FastNative; 43 44 import libcore.util.NativeAllocationRegistry; 45 46 import org.xmlpull.v1.XmlPullParser; 47 import org.xmlpull.v1.XmlPullParserException; 48 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.util.ArrayList; 52 53 /** 54 * {@link Drawable} for drawing animated images (like GIF). 55 * 56 * <p>The framework handles decoding subsequent frames in another thread and 57 * updating when necessary. The drawable will only animate while it is being 58 * displayed.</p> 59 * 60 * <p>Created by {@link ImageDecoder#decodeDrawable}. A user needs to call 61 * {@link #start} to start the animation.</p> 62 * 63 * <p>It can also be defined in XML using the <code><animated-image></code> 64 * element.</p> 65 * 66 * @attr ref android.R.styleable#AnimatedImageDrawable_src 67 * @attr ref android.R.styleable#AnimatedImageDrawable_autoStart 68 * @attr ref android.R.styleable#AnimatedImageDrawable_repeatCount 69 * @attr ref android.R.styleable#AnimatedImageDrawable_autoMirrored 70 */ 71 public class AnimatedImageDrawable extends Drawable implements Animatable2 { 72 private int mIntrinsicWidth; 73 private int mIntrinsicHeight; 74 75 private boolean mStarting; 76 77 private Handler mHandler; 78 79 private class State { State(long nativePtr, InputStream is, AssetFileDescriptor afd)80 State(long nativePtr, InputStream is, AssetFileDescriptor afd) { 81 mNativePtr = nativePtr; 82 mInputStream = is; 83 mAssetFd = afd; 84 } 85 86 final long mNativePtr; 87 88 // These just keep references so the native code can continue using them. 89 private final InputStream mInputStream; 90 private final AssetFileDescriptor mAssetFd; 91 92 int[] mThemeAttrs = null; 93 boolean mAutoMirrored = false; 94 int mRepeatCount = REPEAT_UNDEFINED; 95 } 96 97 private State mState; 98 99 private Runnable mRunnable; 100 101 private ColorFilter mColorFilter; 102 103 /** 104 * Pass this to {@link #setRepeatCount} to repeat infinitely. 105 * 106 * <p>{@link Animatable2.AnimationCallback#onAnimationEnd} will never be 107 * called unless there is an error.</p> 108 */ 109 public static final int REPEAT_INFINITE = -1; 110 111 /** @removed 112 * @deprecated Replaced with REPEAT_INFINITE to match other APIs. 113 */ 114 @java.lang.Deprecated 115 public static final int LOOP_INFINITE = REPEAT_INFINITE; 116 117 private static final int REPEAT_UNDEFINED = -2; 118 119 /** 120 * Specify the number of times to repeat the animation. 121 * 122 * <p>By default, the repeat count in the encoded data is respected. If set 123 * to {@link #REPEAT_INFINITE}, the animation will repeat as long as it is 124 * displayed. If the value is {@code 0}, the animation will play once.</p> 125 * 126 * <p>This call replaces the current repeat count. If the encoded data 127 * specified a repeat count of {@code 2} (meaning that 128 * {@link #getRepeatCount()} returns {@code 2}, the animation will play 129 * three times. Calling {@code setRepeatCount(1)} will result in playing only 130 * twice and {@link #getRepeatCount()} returning {@code 1}.</p> 131 * 132 * <p>If the animation is already playing, the iterations that have already 133 * occurred count towards the new count. If the animation has already 134 * repeated the appropriate number of times (or more), it will finish its 135 * current iteration and then stop.</p> 136 */ setRepeatCount(@ntRangefrom = REPEAT_INFINITE) int repeatCount)137 public void setRepeatCount(@IntRange(from = REPEAT_INFINITE) int repeatCount) { 138 if (repeatCount < REPEAT_INFINITE) { 139 throw new IllegalArgumentException("invalid value passed to setRepeatCount" 140 + repeatCount); 141 } 142 if (mState.mRepeatCount != repeatCount) { 143 mState.mRepeatCount = repeatCount; 144 if (mState.mNativePtr != 0) { 145 nSetRepeatCount(mState.mNativePtr, repeatCount); 146 } 147 } 148 } 149 150 /** @removed 151 * @deprecated Replaced with setRepeatCount to match other APIs. 152 */ 153 @java.lang.Deprecated setLoopCount(int loopCount)154 public void setLoopCount(int loopCount) { 155 setRepeatCount(loopCount); 156 } 157 158 /** 159 * Retrieve the number of times the animation will repeat. 160 * 161 * <p>By default, the repeat count in the encoded data is respected. If the 162 * value is {@link #REPEAT_INFINITE}, the animation will repeat as long as 163 * it is displayed. If the value is {@code 0}, it will play once.</p> 164 * 165 * <p>Calling {@link #setRepeatCount} will make future calls to this method 166 * return the value passed to {@link #setRepeatCount}.</p> 167 */ getRepeatCount()168 public int getRepeatCount() { 169 if (mState.mNativePtr == 0) { 170 throw new IllegalStateException("called getRepeatCount on empty AnimatedImageDrawable"); 171 } 172 if (mState.mRepeatCount == REPEAT_UNDEFINED) { 173 mState.mRepeatCount = nGetRepeatCount(mState.mNativePtr); 174 175 } 176 return mState.mRepeatCount; 177 } 178 179 /** @removed 180 * @deprecated Replaced with getRepeatCount to match other APIs. 181 */ 182 @java.lang.Deprecated getLoopCount(int loopCount)183 public int getLoopCount(int loopCount) { 184 return getRepeatCount(); 185 } 186 187 /** 188 * Create an empty AnimatedImageDrawable. 189 */ AnimatedImageDrawable()190 public AnimatedImageDrawable() { 191 mState = new State(0, null, null); 192 } 193 194 @Override inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)195 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 196 throws XmlPullParserException, IOException { 197 super.inflate(r, parser, attrs, theme); 198 199 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedImageDrawable); 200 updateStateFromTypedArray(a, mSrcDensityOverride); 201 } 202 updateStateFromTypedArray(TypedArray a, int srcDensityOverride)203 private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride) 204 throws XmlPullParserException { 205 State oldState = mState; 206 final Resources r = a.getResources(); 207 final int srcResId = a.getResourceId(R.styleable.AnimatedImageDrawable_src, 0); 208 if (srcResId != 0) { 209 // Follow the density handling in BitmapDrawable. 210 final TypedValue value = new TypedValue(); 211 r.getValueForDensity(srcResId, srcDensityOverride, value, true); 212 if (srcDensityOverride > 0 && value.density > 0 213 && value.density != TypedValue.DENSITY_NONE) { 214 if (value.density == srcDensityOverride) { 215 value.density = r.getDisplayMetrics().densityDpi; 216 } else { 217 value.density = 218 (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride; 219 } 220 } 221 222 int density = Bitmap.DENSITY_NONE; 223 if (value.density == TypedValue.DENSITY_DEFAULT) { 224 density = DisplayMetrics.DENSITY_DEFAULT; 225 } else if (value.density != TypedValue.DENSITY_NONE) { 226 density = value.density; 227 } 228 229 Drawable drawable = null; 230 try { 231 InputStream is = r.openRawResource(srcResId, value); 232 ImageDecoder.Source source = ImageDecoder.createSource(r, is, density); 233 drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { 234 if (!info.isAnimated()) { 235 throw new IllegalArgumentException("image is not animated"); 236 } 237 }); 238 } catch (IOException e) { 239 throw new XmlPullParserException(a.getPositionDescription() + 240 ": <animated-image> requires a valid 'src' attribute", null, e); 241 } 242 243 if (!(drawable instanceof AnimatedImageDrawable)) { 244 throw new XmlPullParserException(a.getPositionDescription() + 245 ": <animated-image> did not decode animated"); 246 } 247 248 // This may have previously been set without a src if we were waiting for a 249 // theme. 250 final int repeatCount = mState.mRepeatCount; 251 // Transfer the state of other to this one. other will be discarded. 252 AnimatedImageDrawable other = (AnimatedImageDrawable) drawable; 253 mState = other.mState; 254 other.mState = null; 255 mIntrinsicWidth = other.mIntrinsicWidth; 256 mIntrinsicHeight = other.mIntrinsicHeight; 257 if (repeatCount != REPEAT_UNDEFINED) { 258 this.setRepeatCount(repeatCount); 259 } 260 } 261 262 mState.mThemeAttrs = a.extractThemeAttrs(); 263 if (mState.mNativePtr == 0 && (mState.mThemeAttrs == null 264 || mState.mThemeAttrs[R.styleable.AnimatedImageDrawable_src] == 0)) { 265 throw new XmlPullParserException(a.getPositionDescription() + 266 ": <animated-image> requires a valid 'src' attribute"); 267 } 268 269 mState.mAutoMirrored = a.getBoolean( 270 R.styleable.AnimatedImageDrawable_autoMirrored, oldState.mAutoMirrored); 271 272 int repeatCount = a.getInt( 273 R.styleable.AnimatedImageDrawable_repeatCount, REPEAT_UNDEFINED); 274 if (repeatCount != REPEAT_UNDEFINED) { 275 this.setRepeatCount(repeatCount); 276 } 277 278 boolean autoStart = a.getBoolean( 279 R.styleable.AnimatedImageDrawable_autoStart, false); 280 if (autoStart && mState.mNativePtr != 0) { 281 this.start(); 282 } 283 } 284 285 /** 286 * @hide 287 * This should only be called by ImageDecoder. 288 * 289 * decoder is only non-null if it has a PostProcess 290 */ AnimatedImageDrawable(long nativeImageDecoder, @Nullable ImageDecoder decoder, int width, int height, int srcDensity, int dstDensity, Rect cropRect, InputStream inputStream, AssetFileDescriptor afd)291 public AnimatedImageDrawable(long nativeImageDecoder, 292 @Nullable ImageDecoder decoder, int width, int height, 293 int srcDensity, int dstDensity, Rect cropRect, 294 InputStream inputStream, AssetFileDescriptor afd) 295 throws IOException { 296 width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity); 297 height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity); 298 299 if (cropRect == null) { 300 mIntrinsicWidth = width; 301 mIntrinsicHeight = height; 302 } else { 303 cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity), 304 Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity), 305 Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity), 306 Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity)); 307 mIntrinsicWidth = cropRect.width(); 308 mIntrinsicHeight = cropRect.height(); 309 } 310 311 mState = new State(nCreate(nativeImageDecoder, decoder, width, height, cropRect), 312 inputStream, afd); 313 314 final long nativeSize = nNativeByteSize(mState.mNativePtr); 315 NativeAllocationRegistry registry = new NativeAllocationRegistry( 316 AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); 317 registry.registerNativeAllocation(mState, mState.mNativePtr); 318 } 319 320 @Override getIntrinsicWidth()321 public int getIntrinsicWidth() { 322 return mIntrinsicWidth; 323 } 324 325 @Override getIntrinsicHeight()326 public int getIntrinsicHeight() { 327 return mIntrinsicHeight; 328 } 329 330 // nDraw returns -1 if the animation has finished. 331 private static final int FINISHED = -1; 332 333 @Override draw(@onNull Canvas canvas)334 public void draw(@NonNull Canvas canvas) { 335 if (mState.mNativePtr == 0) { 336 throw new IllegalStateException("called draw on empty AnimatedImageDrawable"); 337 } 338 339 if (mStarting) { 340 mStarting = false; 341 342 postOnAnimationStart(); 343 } 344 345 long nextUpdate = nDraw(mState.mNativePtr, canvas.getNativeCanvasWrapper()); 346 // a value <= 0 indicates that the drawable is stopped or that renderThread 347 // will manage the animation 348 if (nextUpdate > 0) { 349 if (mRunnable == null) { 350 mRunnable = this::invalidateSelf; 351 } 352 scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis()); 353 } else if (nextUpdate == FINISHED) { 354 // This means the animation was drawn in software mode and ended. 355 postOnAnimationEnd(); 356 } 357 } 358 359 @Override setAlpha(@ntRangefrom = 0, to = 255) int alpha)360 public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { 361 if (alpha < 0 || alpha > 255) { 362 throw new IllegalArgumentException("Alpha must be between 0 and" 363 + " 255! provided " + alpha); 364 } 365 366 if (mState.mNativePtr == 0) { 367 throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable"); 368 } 369 370 nSetAlpha(mState.mNativePtr, alpha); 371 invalidateSelf(); 372 } 373 374 @Override getAlpha()375 public int getAlpha() { 376 if (mState.mNativePtr == 0) { 377 throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable"); 378 } 379 return nGetAlpha(mState.mNativePtr); 380 } 381 382 @Override setColorFilter(@ullable ColorFilter colorFilter)383 public void setColorFilter(@Nullable ColorFilter colorFilter) { 384 if (mState.mNativePtr == 0) { 385 throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable"); 386 } 387 388 if (colorFilter != mColorFilter) { 389 mColorFilter = colorFilter; 390 long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance(); 391 nSetColorFilter(mState.mNativePtr, nativeFilter); 392 invalidateSelf(); 393 } 394 } 395 396 @Override 397 @Nullable getColorFilter()398 public ColorFilter getColorFilter() { 399 return mColorFilter; 400 } 401 402 @Override getOpacity()403 public @PixelFormat.Opacity int getOpacity() { 404 return PixelFormat.TRANSLUCENT; 405 } 406 407 @Override setAutoMirrored(boolean mirrored)408 public void setAutoMirrored(boolean mirrored) { 409 if (mState.mAutoMirrored != mirrored) { 410 mState.mAutoMirrored = mirrored; 411 if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL && mState.mNativePtr != 0) { 412 nSetMirrored(mState.mNativePtr, mirrored); 413 invalidateSelf(); 414 } 415 } 416 } 417 418 @Override onLayoutDirectionChanged(int layoutDirection)419 public boolean onLayoutDirectionChanged(int layoutDirection) { 420 if (!mState.mAutoMirrored || mState.mNativePtr == 0) { 421 return false; 422 } 423 424 final boolean mirror = layoutDirection == View.LAYOUT_DIRECTION_RTL; 425 nSetMirrored(mState.mNativePtr, mirror); 426 return true; 427 } 428 429 @Override isAutoMirrored()430 public final boolean isAutoMirrored() { 431 return mState.mAutoMirrored; 432 } 433 434 // Animatable overrides 435 /** 436 * Return whether the animation is currently running. 437 * 438 * <p>When this drawable is created, this will return {@code false}. A client 439 * needs to call {@link #start} to start the animation.</p> 440 */ 441 @Override isRunning()442 public boolean isRunning() { 443 if (mState.mNativePtr == 0) { 444 throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable"); 445 } 446 return nIsRunning(mState.mNativePtr); 447 } 448 449 /** 450 * Start the animation. 451 * 452 * <p>Does nothing if the animation is already running. If the animation is stopped, 453 * this will reset it.</p> 454 * 455 * <p>When the drawable is drawn, starting the animation, 456 * {@link Animatable2.AnimationCallback#onAnimationStart} will be called.</p> 457 */ 458 @Override start()459 public void start() { 460 if (mState.mNativePtr == 0) { 461 throw new IllegalStateException("called start on empty AnimatedImageDrawable"); 462 } 463 464 if (nStart(mState.mNativePtr)) { 465 mStarting = true; 466 invalidateSelf(); 467 } 468 } 469 470 /** 471 * Stop the animation. 472 * 473 * <p>If the animation is stopped, it will continue to display the frame 474 * it was displaying when stopped.</p> 475 */ 476 @Override stop()477 public void stop() { 478 if (mState.mNativePtr == 0) { 479 throw new IllegalStateException("called stop on empty AnimatedImageDrawable"); 480 } 481 if (nStop(mState.mNativePtr)) { 482 postOnAnimationEnd(); 483 } 484 } 485 486 // Animatable2 overrides 487 private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; 488 489 @Override registerAnimationCallback(@onNull AnimationCallback callback)490 public void registerAnimationCallback(@NonNull AnimationCallback callback) { 491 if (callback == null) { 492 return; 493 } 494 495 if (mAnimationCallbacks == null) { 496 mAnimationCallbacks = new ArrayList<Animatable2.AnimationCallback>(); 497 nSetOnAnimationEndListener(mState.mNativePtr, this); 498 } 499 500 if (!mAnimationCallbacks.contains(callback)) { 501 mAnimationCallbacks.add(callback); 502 } 503 } 504 505 @Override unregisterAnimationCallback(@onNull AnimationCallback callback)506 public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { 507 if (callback == null || mAnimationCallbacks == null 508 || !mAnimationCallbacks.remove(callback)) { 509 return false; 510 } 511 512 if (mAnimationCallbacks.isEmpty()) { 513 clearAnimationCallbacks(); 514 } 515 516 return true; 517 } 518 519 @Override clearAnimationCallbacks()520 public void clearAnimationCallbacks() { 521 if (mAnimationCallbacks != null) { 522 mAnimationCallbacks = null; 523 nSetOnAnimationEndListener(mState.mNativePtr, null); 524 } 525 } 526 postOnAnimationStart()527 private void postOnAnimationStart() { 528 if (mAnimationCallbacks == null) { 529 return; 530 } 531 532 getHandler().post(() -> { 533 for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { 534 callback.onAnimationStart(this); 535 } 536 }); 537 } 538 postOnAnimationEnd()539 private void postOnAnimationEnd() { 540 if (mAnimationCallbacks == null) { 541 return; 542 } 543 544 getHandler().post(() -> { 545 for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { 546 callback.onAnimationEnd(this); 547 } 548 }); 549 } 550 getHandler()551 private Handler getHandler() { 552 if (mHandler == null) { 553 mHandler = new Handler(Looper.getMainLooper()); 554 } 555 return mHandler; 556 } 557 558 /** 559 * Called by JNI. 560 * 561 * The JNI code has already posted this to the thread that created the 562 * callback, so no need to post. 563 */ 564 @SuppressWarnings("unused") onAnimationEnd()565 private void onAnimationEnd() { 566 if (mAnimationCallbacks != null) { 567 for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { 568 callback.onAnimationEnd(this); 569 } 570 } 571 } 572 573 nCreate(long nativeImageDecoder, @Nullable ImageDecoder decoder, int width, int height, Rect cropRect)574 private static native long nCreate(long nativeImageDecoder, 575 @Nullable ImageDecoder decoder, int width, int height, Rect cropRect) 576 throws IOException; 577 @FastNative nGetNativeFinalizer()578 private static native long nGetNativeFinalizer(); nDraw(long nativePtr, long canvasNativePtr)579 private static native long nDraw(long nativePtr, long canvasNativePtr); 580 @FastNative nSetAlpha(long nativePtr, int alpha)581 private static native void nSetAlpha(long nativePtr, int alpha); 582 @FastNative nGetAlpha(long nativePtr)583 private static native int nGetAlpha(long nativePtr); 584 @FastNative nSetColorFilter(long nativePtr, long nativeFilter)585 private static native void nSetColorFilter(long nativePtr, long nativeFilter); 586 @FastNative nIsRunning(long nativePtr)587 private static native boolean nIsRunning(long nativePtr); 588 // Return whether the animation started. 589 @FastNative nStart(long nativePtr)590 private static native boolean nStart(long nativePtr); 591 @FastNative nStop(long nativePtr)592 private static native boolean nStop(long nativePtr); 593 @FastNative nGetRepeatCount(long nativePtr)594 private static native int nGetRepeatCount(long nativePtr); 595 @FastNative nSetRepeatCount(long nativePtr, int repeatCount)596 private static native void nSetRepeatCount(long nativePtr, int repeatCount); 597 // Pass the drawable down to native so it can call onAnimationEnd. nSetOnAnimationEndListener(long nativePtr, @Nullable AnimatedImageDrawable drawable)598 private static native void nSetOnAnimationEndListener(long nativePtr, 599 @Nullable AnimatedImageDrawable drawable); 600 @FastNative nNativeByteSize(long nativePtr)601 private static native long nNativeByteSize(long nativePtr); 602 @FastNative nSetMirrored(long nativePtr, boolean mirror)603 private static native void nSetMirrored(long nativePtr, boolean mirror); 604 } 605