All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.graphics.drawable.AnimatedImageDrawable Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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.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).
 *
 * 

The framework handles decoding subsequent frames in another thread and * updating when necessary. The drawable will only animate while it is being * displayed.

* *

Created by {@link ImageDecoder#decodeDrawable}. A user needs to call * {@link #start} to start the animation.

* *

It can also be defined in XML using the <animated-image> * element.

* * @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. * *

{@link Animatable2.AnimationCallback#onAnimationEnd} will never be * called unless there is an error.

*/ 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. * *

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.

* *

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}.

* *

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.

*/ 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. * *

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.

* *

Calling {@link #setRepeatCount} will make future calls to this method * return the value passed to {@link #setRepeatCount}.

*/ 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() + ": requires a valid 'src' attribute", null, e); } if (!(drawable instanceof AnimatedImageDrawable)) { throw new XmlPullParserException(a.getPositionDescription() + ": 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() + ": 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. * *

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.mNativePtr == 0) { 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.

* *

When the drawable is drawn, starting the animation, * {@link Animatable2.AnimationCallback#onAnimationStart} will be called.

*/ @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. * *

If the animation is stopped, it will continue to display the frame * it was displaying when stopped.

*/ @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 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") @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); }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy