Home | History | Annotate | Download | only in drawable
      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 {
     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      */
    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
    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      */
    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
    183     public int getLoopCount(int loopCount) {
    184         return getRepeatCount();
    185     }
    186 
    187     /**
    188      * Create an empty AnimatedImageDrawable.
    189      */
    190     public AnimatedImageDrawable() {
    191         mState = new State(0, null, null);
    192     }
    193 
    194     @Override
    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 
    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      */
    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
    321     public int getIntrinsicWidth() {
    322         return mIntrinsicWidth;
    323     }
    324 
    325     @Override
    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
    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
    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
    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
    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
    398     public ColorFilter getColorFilter() {
    399         return mColorFilter;
    400     }
    401 
    402     @Override
    403     public @PixelFormat.Opacity int getOpacity() {
    404         return PixelFormat.TRANSLUCENT;
    405     }
    406 
    407     @Override
    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
    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
    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
    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
    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
    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
    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
    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
    520     public void clearAnimationCallbacks() {
    521         if (mAnimationCallbacks != null) {
    522             mAnimationCallbacks = null;
    523             nSetOnAnimationEndListener(mState.mNativePtr, null);
    524         }
    525     }
    526 
    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 
    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 
    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")
    565     private void onAnimationEnd() {
    566         if (mAnimationCallbacks != null) {
    567             for (Animatable2.AnimationCallback callback : mAnimationCallbacks) {
    568                 callback.onAnimationEnd(this);
    569             }
    570         }
    571     }
    572 
    573 
    574     private static native long nCreate(long nativeImageDecoder,
    575             @Nullable ImageDecoder decoder, int width, int height, Rect cropRect)
    576         throws IOException;
    577     @FastNative
    578     private static native long nGetNativeFinalizer();
    579     private static native long nDraw(long nativePtr, long canvasNativePtr);
    580     @FastNative
    581     private static native void nSetAlpha(long nativePtr, int alpha);
    582     @FastNative
    583     private static native int nGetAlpha(long nativePtr);
    584     @FastNative
    585     private static native void nSetColorFilter(long nativePtr, long nativeFilter);
    586     @FastNative
    587     private static native boolean nIsRunning(long nativePtr);
    588     // Return whether the animation started.
    589     @FastNative
    590     private static native boolean nStart(long nativePtr);
    591     @FastNative
    592     private static native boolean nStop(long nativePtr);
    593     @FastNative
    594     private static native int nGetRepeatCount(long nativePtr);
    595     @FastNative
    596     private static native void nSetRepeatCount(long nativePtr, int repeatCount);
    597     // Pass the drawable down to native so it can call onAnimationEnd.
    598     private static native void nSetOnAnimationEndListener(long nativePtr,
    599             @Nullable AnimatedImageDrawable drawable);
    600     @FastNative
    601     private static native long nNativeByteSize(long nativePtr);
    602     @FastNative
    603     private static native void nSetMirrored(long nativePtr, boolean mirror);
    604 }
    605