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