• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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>&lt;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