/*
* 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 dalvik.annotation.optimization.FastNative;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.InflateException;
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.util.DisplayMetrics;
import android.util.TypedValue;
import com.android.internal.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import libcore.io.IoUtils;
import libcore.util.NativeAllocationRegistry;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Runnable;
import java.util.ArrayList;
/**
* {@link Drawable} for drawing animated images (like GIF).
*
*
Created by {@link ImageDecoder#decodeDrawable}. A user needs to call
* {@link #start} to start the animation.
*/
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;
}
public final long mNativePtr;
// These just keep references so the native code can continue using them.
private final InputStream mInputStream;
private final AssetFileDescriptor mAssetFd;
}
private State mState;
private Runnable mRunnable;
/**
* Pass this to {@link #setLoopCount} to loop infinitely.
*
* {@link Animatable2.AnimationCallback#onAnimationEnd} will never be
* called unless there is an error.
*/
public static final int LOOP_INFINITE = -1;
/**
* Specify the number of times to loop the animation.
*
* By default, the loop count in the encoded data is respected.
*/
public void setLoopCount(int loopCount) {
if (mState == null) {
throw new IllegalStateException("called setLoopCount on empty AnimatedImageDrawable");
}
nSetLoopCount(mState.mNativePtr, loopCount);
}
/**
* Create an empty AnimatedImageDrawable.
*/
public AnimatedImageDrawable() {
mState = 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 {
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() +
": requires a valid 'src' attribute", null, e);
}
if (!(drawable instanceof AnimatedImageDrawable)) {
throw new XmlPullParserException(a.getPositionDescription() +
": did not decode animated");
}
// 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;
}
}
/**
* @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,
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, cropRect),
inputStream, afd);
// FIXME: Use the right size for the native allocation.
long nativeSize = 200;
NativeAllocationRegistry registry = new NativeAllocationRegistry(
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 == null) {
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);
} 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 == null) {
throw new IllegalStateException("called setAlpha on empty AnimatedImageDrawable");
}
nSetAlpha(mState.mNativePtr, alpha);
invalidateSelf();
}
@Override
public int getAlpha() {
if (mState == null) {
throw new IllegalStateException("called getAlpha on empty AnimatedImageDrawable");
}
return nGetAlpha(mState.mNativePtr);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
if (mState == null) {
throw new IllegalStateException("called setColorFilter on empty AnimatedImageDrawable");
}
long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance();
nSetColorFilter(mState.mNativePtr, nativeFilter);
invalidateSelf();
}
@Override
public @PixelFormat.Opacity int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
if (!super.setVisible(visible, restart)) {
return false;
}
if (!visible) {
nMarkInvisible(mState.mNativePtr);
}
return true;
}
// Animatable overrides
/**
* Return whether the animation is currently running.
*
* When this drawable is created, this will return {@code false}. A client
* needs to call {@link #start} to start the animation.
*/
@Override
public boolean isRunning() {
if (mState == null) {
throw new IllegalStateException("called isRunning on empty AnimatedImageDrawable");
}
return nIsRunning(mState.mNativePtr);
}
/**
* Start the animation.
*
* Does nothing if the animation is already running. If the animation is stopped,
* this will reset it.
*
* If the animation starts, this will call
* {@link Animatable2.AnimationCallback#onAnimationStart}.
*/
@Override
public void start() {
if (mState == null) {
throw new IllegalStateException("called start on empty AnimatedImageDrawable");
}
if (nStart(mState.mNativePtr)) {
mStarting = true;
invalidateSelf();
}
}
/**
* Stop the animation.
*
* If the animation is stopped, it will continue to display the frame
* it was displaying when stopped.
*/
@Override
public void stop() {
if (mState == null) {
throw new IllegalStateException("called stop on empty AnimatedImageDrawable");
}
if (nStop(mState.mNativePtr)) {
postOnAnimationEnd();
}
}
// Animatable2 overrides
private ArrayList mAnimationCallbacks = null;
@Override
public void registerAnimationCallback(@NonNull AnimationCallback callback) {
if (callback == null) {
return;
}
if (mAnimationCallbacks == null) {
mAnimationCallbacks = new ArrayList();
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")
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, 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 void nSetLoopCount(long nativePtr, int loopCount);
// 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 nMarkInvisible(long nativePtr);
}