Files
frameworks_base/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java
Leon Scroggins III 4afdd1caa4 AnimatedImageDrawable: Eliminate unnecessary calls to redraw
Bug: 78866720
Test: Manual + systrace; existing CTS

Previously, we set hasAnimations to true when the AnimatedImageDrawable,
so that we would get a call to redraw. But if the image does not need to
show its next frame yet, the redraw was unnecessary.

Instead, add a new field to TreeInfo::Out, representing the delay time
until the image will need to be redrawn - i.e. when the duration of the
current frame has passed. Each call to prepareTree will post at most one
message to redraw, in time for the earliest animated image to be
redrawn. Post the message for one rendered frame ahead of time, so that
when it is time to show the next frame, the image has already gotten the
message to update.

On a screen with a single animated image, this drops the number of calls
to dispatchFrameCallbacks to as infrequent as possible. It is called
only when we need to draw a new frame of the image. On a screen with
multiple animated images, the calls may be redundant, but they will not
be more frequent than they would be without this change.

Switch to nsecs_t and systemTime internally, matching the rest of HWUI.

Remove mDidDraw and related. Its purpose was to prevent advancing the
animation while the image is not being drawn. But it isn't really
necessary. If it's not drawn, onDraw is not called, which is where we
trigger decoding. And onDraw already has a defense against getting too
far ahead - if its timer indicates that it should skip a frame or show
it very briefly, it will back up its timer. More importantly, mDidDraw
caused a bug, when combined with less frequent redraws. If the display
list containing the drawable doesn't need to be redrawn for other
reasons, the drawable's timer never advanced, so its animation stopped.

Fix software drawing. Compute the milliseconds in the future to draw the
next frame, and add that to SystemClock.uptimeMillis() to compute the
time to pass to scheduleSelf.

Change-Id: I13aab49922fa300f73b327be25561d7120c09ec4
2018-05-16 18:51:07 -04:00

605 lines
21 KiB
Java

/*
* 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.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,
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);
final long nativeSize = nNativeByteSize(mState.mNativePtr);
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.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")
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 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);
}