blob: d8946009483cb997a179ee5d2f2eacbfb231ec82 [file] [log] [blame]
/*
* 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>&lt;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);
}