| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.drawable; |
| |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.ImageDecoder; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.TypedValue; |
| import android.view.View; |
| |
| import com.android.internal.R; |
| |
| import dalvik.annotation.optimization.FastNative; |
| |
| import libcore.util.NativeAllocationRegistry; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| |
| /** |
| * {@link Drawable} for drawing animated images (like GIF). |
| * |
| * <p>The framework handles decoding subsequent frames in another thread and |
| * updating when necessary. The drawable will only animate while it is being |
| * displayed.</p> |
| * |
| * <p>Created by {@link ImageDecoder#decodeDrawable}. A user needs to call |
| * {@link #start} to start the animation.</p> |
| * |
| * <p>It can also be defined in XML using the <code><animated-image></code> |
| * element.</p> |
| * |
| * @attr ref android.R.styleable#AnimatedImageDrawable_src |
| * @attr ref android.R.styleable#AnimatedImageDrawable_autoStart |
| * @attr ref android.R.styleable#AnimatedImageDrawable_repeatCount |
| * @attr ref android.R.styleable#AnimatedImageDrawable_autoMirrored |
| */ |
| public class AnimatedImageDrawable extends Drawable implements Animatable2 { |
| private int mIntrinsicWidth; |
| private int mIntrinsicHeight; |
| |
| private boolean mStarting; |
| |
| private Handler mHandler; |
| |
| private class State { |
| State(long nativePtr, InputStream is, AssetFileDescriptor afd) { |
| mNativePtr = nativePtr; |
| mInputStream = is; |
| mAssetFd = afd; |
| } |
| |
| final long mNativePtr; |
| |
| // These just keep references so the native code can continue using them. |
| private final InputStream mInputStream; |
| private final AssetFileDescriptor mAssetFd; |
| |
| int[] mThemeAttrs = null; |
| boolean mAutoMirrored = false; |
| int mRepeatCount = REPEAT_UNDEFINED; |
| } |
| |
| private State mState; |
| |
| private Runnable mRunnable; |
| |
| private ColorFilter mColorFilter; |
| |
| /** |
| * Pass this to {@link #setRepeatCount} to repeat infinitely. |
| * |
| * <p>{@link Animatable2.AnimationCallback#onAnimationEnd} will never be |
| * called unless there is an error.</p> |
| */ |
| public static final int REPEAT_INFINITE = -1; |
| |
| /** @removed |
| * @deprecated Replaced with REPEAT_INFINITE to match other APIs. |
| */ |
| @java.lang.Deprecated |
| public static final int LOOP_INFINITE = REPEAT_INFINITE; |
| |
| private static final int REPEAT_UNDEFINED = -2; |
| |
| /** |
| * Specify the number of times to repeat the animation. |
| * |
| * <p>By default, the repeat count in the encoded data is respected. If set |
| * to {@link #REPEAT_INFINITE}, the animation will repeat as long as it is |
| * displayed. If the value is {@code 0}, the animation will play once.</p> |
| * |
| * <p>This call replaces the current repeat count. If the encoded data |
| * specified a repeat count of {@code 2} (meaning that |
| * {@link #getRepeatCount()} returns {@code 2}, the animation will play |
| * three times. Calling {@code setRepeatCount(1)} will result in playing only |
| * twice and {@link #getRepeatCount()} returning {@code 1}.</p> |
| * |
| * <p>If the animation is already playing, the iterations that have already |
| * occurred count towards the new count. If the animation has already |
| * repeated the appropriate number of times (or more), it will finish its |
| * current iteration and then stop.</p> |
| */ |
| public void setRepeatCount(@IntRange(from = REPEAT_INFINITE) int repeatCount) { |
| if (repeatCount < REPEAT_INFINITE) { |
| throw new IllegalArgumentException("invalid value passed to setRepeatCount" |
| + repeatCount); |
| } |
| if (mState.mRepeatCount != repeatCount) { |
| mState.mRepeatCount = repeatCount; |
| if (mState.mNativePtr != 0) { |
| nSetRepeatCount(mState.mNativePtr, repeatCount); |
| } |
| } |
| } |
| |
| /** @removed |
| * @deprecated Replaced with setRepeatCount to match other APIs. |
| */ |
| @java.lang.Deprecated |
| public void setLoopCount(int loopCount) { |
| setRepeatCount(loopCount); |
| } |
| |
| /** |
| * Retrieve the number of times the animation will repeat. |
| * |
| * <p>By default, the repeat count in the encoded data is respected. If the |
| * value is {@link #REPEAT_INFINITE}, the animation will repeat as long as |
| * it is displayed. If the value is {@code 0}, it will play once.</p> |
| * |
| * <p>Calling {@link #setRepeatCount} will make future calls to this method |
| * return the value passed to {@link #setRepeatCount}.</p> |
| */ |
| public int getRepeatCount() { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called getRepeatCount on empty AnimatedImageDrawable"); |
| } |
| if (mState.mRepeatCount == REPEAT_UNDEFINED) { |
| mState.mRepeatCount = nGetRepeatCount(mState.mNativePtr); |
| |
| } |
| return mState.mRepeatCount; |
| } |
| |
| /** @removed |
| * @deprecated Replaced with getRepeatCount to match other APIs. |
| */ |
| @java.lang.Deprecated |
| public int getLoopCount(int loopCount) { |
| return getRepeatCount(); |
| } |
| |
| /** |
| * Create an empty AnimatedImageDrawable. |
| */ |
| public AnimatedImageDrawable() { |
| mState = new State(0, null, null); |
| } |
| |
| @Override |
| public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| super.inflate(r, parser, attrs, theme); |
| |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedImageDrawable); |
| updateStateFromTypedArray(a, mSrcDensityOverride); |
| } |
| |
| private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride) |
| throws XmlPullParserException { |
| State oldState = mState; |
| final Resources r = a.getResources(); |
| final int srcResId = a.getResourceId(R.styleable.AnimatedImageDrawable_src, 0); |
| if (srcResId != 0) { |
| // Follow the density handling in BitmapDrawable. |
| final TypedValue value = new TypedValue(); |
| r.getValueForDensity(srcResId, srcDensityOverride, value, true); |
| if (srcDensityOverride > 0 && value.density > 0 |
| && value.density != TypedValue.DENSITY_NONE) { |
| if (value.density == srcDensityOverride) { |
| value.density = r.getDisplayMetrics().densityDpi; |
| } else { |
| value.density = |
| (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride; |
| } |
| } |
| |
| int density = Bitmap.DENSITY_NONE; |
| if (value.density == TypedValue.DENSITY_DEFAULT) { |
| density = DisplayMetrics.DENSITY_DEFAULT; |
| } else if (value.density != TypedValue.DENSITY_NONE) { |
| density = value.density; |
| } |
| |
| Drawable drawable = null; |
| try { |
| InputStream is = r.openRawResource(srcResId, value); |
| ImageDecoder.Source source = ImageDecoder.createSource(r, is, density); |
| drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { |
| if (!info.isAnimated()) { |
| throw new IllegalArgumentException("image is not animated"); |
| } |
| }); |
| } catch (IOException e) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| ": <animated-image> requires a valid 'src' attribute", null, e); |
| } |
| |
| if (!(drawable instanceof AnimatedImageDrawable)) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| ": <animated-image> did not decode animated"); |
| } |
| |
| // This may have previously been set without a src if we were waiting for a |
| // theme. |
| final int repeatCount = mState.mRepeatCount; |
| // Transfer the state of other to this one. other will be discarded. |
| AnimatedImageDrawable other = (AnimatedImageDrawable) drawable; |
| mState = other.mState; |
| other.mState = null; |
| mIntrinsicWidth = other.mIntrinsicWidth; |
| mIntrinsicHeight = other.mIntrinsicHeight; |
| if (repeatCount != REPEAT_UNDEFINED) { |
| this.setRepeatCount(repeatCount); |
| } |
| } |
| |
| mState.mThemeAttrs = a.extractThemeAttrs(); |
| if (mState.mNativePtr == 0 && (mState.mThemeAttrs == null |
| || mState.mThemeAttrs[R.styleable.AnimatedImageDrawable_src] == 0)) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| ": <animated-image> requires a valid 'src' attribute"); |
| } |
| |
| mState.mAutoMirrored = a.getBoolean( |
| R.styleable.AnimatedImageDrawable_autoMirrored, oldState.mAutoMirrored); |
| |
| int repeatCount = a.getInt( |
| R.styleable.AnimatedImageDrawable_repeatCount, REPEAT_UNDEFINED); |
| if (repeatCount != REPEAT_UNDEFINED) { |
| this.setRepeatCount(repeatCount); |
| } |
| |
| boolean autoStart = a.getBoolean( |
| R.styleable.AnimatedImageDrawable_autoStart, false); |
| if (autoStart && mState.mNativePtr != 0) { |
| this.start(); |
| } |
| } |
| |
| /** |
| * @hide |
| * This should only be called by ImageDecoder. |
| * |
| * decoder is only non-null if it has a PostProcess |
| */ |
| public AnimatedImageDrawable(long nativeImageDecoder, |
| @Nullable ImageDecoder decoder, int width, int height, |
| long colorSpaceHandle, boolean extended, int srcDensity, int dstDensity, |
| Rect cropRect, InputStream inputStream, AssetFileDescriptor afd) |
| throws IOException { |
| width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity); |
| height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity); |
| |
| if (cropRect == null) { |
| mIntrinsicWidth = width; |
| mIntrinsicHeight = height; |
| } else { |
| cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity), |
| Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity), |
| Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity), |
| Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity)); |
| mIntrinsicWidth = cropRect.width(); |
| mIntrinsicHeight = cropRect.height(); |
| } |
| |
| mState = new State(nCreate(nativeImageDecoder, decoder, width, height, colorSpaceHandle, |
| extended, cropRect), inputStream, afd); |
| |
| final long nativeSize = nNativeByteSize(mState.mNativePtr); |
| NativeAllocationRegistry registry = NativeAllocationRegistry.createMalloced( |
| AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); |
| registry.registerNativeAllocation(mState, mState.mNativePtr); |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mIntrinsicWidth; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mIntrinsicHeight; |
| } |
| |
| // nDraw returns -1 if the animation has finished. |
| private static final int FINISHED = -1; |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called draw on empty AnimatedImageDrawable"); |
| } |
| |
| if (mStarting) { |
| mStarting = false; |
| |
| postOnAnimationStart(); |
| } |
| |
| long nextUpdate = nDraw(mState.mNativePtr, canvas.getNativeCanvasWrapper()); |
| // a value <= 0 indicates that the drawable is stopped or that renderThread |
| // will manage the animation |
| if (nextUpdate > 0) { |
| if (mRunnable == null) { |
| mRunnable = this::invalidateSelf; |
| } |
| scheduleSelf(mRunnable, nextUpdate + SystemClock.uptimeMillis()); |
| } else if (nextUpdate == FINISHED) { |
| // This means the animation was drawn in software mode and ended. |
| postOnAnimationEnd(); |
| } |
| } |
| |
| @Override |
| public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { |
| if (alpha < 0 || alpha > 255) { |
| throw new IllegalArgumentException("Alpha must be between 0 and" |
| + " 255! provided " + alpha); |
| } |
| |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable"); |
| } |
| |
| nSetAlpha(mState.mNativePtr, alpha); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getAlpha() { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable"); |
| } |
| return nGetAlpha(mState.mNativePtr); |
| } |
| |
| @Override |
| public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable"); |
| } |
| |
| if (colorFilter != mColorFilter) { |
| mColorFilter = colorFilter; |
| long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance(); |
| nSetColorFilter(mState.mNativePtr, nativeFilter); |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public ColorFilter getColorFilter() { |
| return mColorFilter; |
| } |
| |
| @Override |
| public @PixelFormat.Opacity int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| public void setAutoMirrored(boolean mirrored) { |
| if (mState.mAutoMirrored != mirrored) { |
| mState.mAutoMirrored = mirrored; |
| if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL && mState.mNativePtr != 0) { |
| nSetMirrored(mState.mNativePtr, mirrored); |
| invalidateSelf(); |
| } |
| } |
| } |
| |
| @Override |
| public boolean onLayoutDirectionChanged(int layoutDirection) { |
| if (!mState.mAutoMirrored || mState.mNativePtr == 0) { |
| return false; |
| } |
| |
| final boolean mirror = layoutDirection == View.LAYOUT_DIRECTION_RTL; |
| nSetMirrored(mState.mNativePtr, mirror); |
| return true; |
| } |
| |
| @Override |
| public final boolean isAutoMirrored() { |
| return mState.mAutoMirrored; |
| } |
| |
| // Animatable overrides |
| /** |
| * Return whether the animation is currently running. |
| * |
| * <p>When this drawable is created, this will return {@code false}. A client |
| * needs to call {@link #start} to start the animation.</p> |
| */ |
| @Override |
| public boolean isRunning() { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable"); |
| } |
| return nIsRunning(mState.mNativePtr); |
| } |
| |
| /** |
| * Start the animation. |
| * |
| * <p>Does nothing if the animation is already running. If the animation is stopped, |
| * this will reset it.</p> |
| * |
| * <p>When the drawable is drawn, starting the animation, |
| * {@link Animatable2.AnimationCallback#onAnimationStart} will be called.</p> |
| */ |
| @Override |
| public void start() { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called start on empty AnimatedImageDrawable"); |
| } |
| |
| if (nStart(mState.mNativePtr)) { |
| mStarting = true; |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Stop the animation. |
| * |
| * <p>If the animation is stopped, it will continue to display the frame |
| * it was displaying when stopped.</p> |
| */ |
| @Override |
| public void stop() { |
| if (mState.mNativePtr == 0) { |
| throw new IllegalStateException("called stop on empty AnimatedImageDrawable"); |
| } |
| if (nStop(mState.mNativePtr)) { |
| postOnAnimationEnd(); |
| } |
| } |
| |
| // Animatable2 overrides |
| private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; |
| |
| @Override |
| public void registerAnimationCallback(@NonNull AnimationCallback callback) { |
| if (callback == null) { |
| return; |
| } |
| |
| if (mAnimationCallbacks == null) { |
| mAnimationCallbacks = new ArrayList<Animatable2.AnimationCallback>(); |
| nSetOnAnimationEndListener(mState.mNativePtr, this); |
| } |
| |
| if (!mAnimationCallbacks.contains(callback)) { |
| mAnimationCallbacks.add(callback); |
| } |
| } |
| |
| @Override |
| public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { |
| if (callback == null || mAnimationCallbacks == null |
| || !mAnimationCallbacks.remove(callback)) { |
| return false; |
| } |
| |
| if (mAnimationCallbacks.isEmpty()) { |
| clearAnimationCallbacks(); |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public void clearAnimationCallbacks() { |
| if (mAnimationCallbacks != null) { |
| mAnimationCallbacks = null; |
| nSetOnAnimationEndListener(mState.mNativePtr, null); |
| } |
| } |
| |
| private void postOnAnimationStart() { |
| if (mAnimationCallbacks == null) { |
| return; |
| } |
| |
| getHandler().post(() -> { |
| for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { |
| callback.onAnimationStart(this); |
| } |
| }); |
| } |
| |
| private void postOnAnimationEnd() { |
| if (mAnimationCallbacks == null) { |
| return; |
| } |
| |
| getHandler().post(() -> { |
| for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { |
| callback.onAnimationEnd(this); |
| } |
| }); |
| } |
| |
| private Handler getHandler() { |
| if (mHandler == null) { |
| mHandler = new Handler(Looper.getMainLooper()); |
| } |
| return mHandler; |
| } |
| |
| /** |
| * Called by JNI. |
| * |
| * The JNI code has already posted this to the thread that created the |
| * callback, so no need to post. |
| */ |
| @SuppressWarnings("unused") |
| @UnsupportedAppUsage |
| private void onAnimationEnd() { |
| if (mAnimationCallbacks != null) { |
| for (Animatable2.AnimationCallback callback : mAnimationCallbacks) { |
| callback.onAnimationEnd(this); |
| } |
| } |
| } |
| |
| |
| private static native long nCreate(long nativeImageDecoder, |
| @Nullable ImageDecoder decoder, int width, int height, long colorSpaceHandle, |
| boolean extended, Rect cropRect) throws IOException; |
| @FastNative |
| private static native long nGetNativeFinalizer(); |
| private static native long nDraw(long nativePtr, long canvasNativePtr); |
| @FastNative |
| private static native void nSetAlpha(long nativePtr, int alpha); |
| @FastNative |
| private static native int nGetAlpha(long nativePtr); |
| @FastNative |
| private static native void nSetColorFilter(long nativePtr, long nativeFilter); |
| @FastNative |
| private static native boolean nIsRunning(long nativePtr); |
| // Return whether the animation started. |
| @FastNative |
| private static native boolean nStart(long nativePtr); |
| @FastNative |
| private static native boolean nStop(long nativePtr); |
| @FastNative |
| private static native int nGetRepeatCount(long nativePtr); |
| @FastNative |
| private static native void nSetRepeatCount(long nativePtr, int repeatCount); |
| // Pass the drawable down to native so it can call onAnimationEnd. |
| private static native void nSetOnAnimationEndListener(long nativePtr, |
| @Nullable AnimatedImageDrawable drawable); |
| @FastNative |
| private static native long nNativeByteSize(long nativePtr); |
| @FastNative |
| private static native void nSetMirrored(long nativePtr, boolean mirror); |
| } |