package pl.droidsonroids.gif;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.os.Parcelable;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Surface;
import android.view.TextureView;
import android.widget.ImageView.ScaleType;
import java.io.IOException;
import java.lang.ref.WeakReference;
import me.xiaopan.sketch.gif.R;
import pl.droidsonroids.gif.annotations.Beta;
/**
* <p>{@link TextureView} which can display animated GIFs. Available on API level 14
* ({@link Build.VERSION_CODES#ICE_CREAM_SANDWICH}) and above. GifTextureView can only be used in a
* hardware accelerated window. When rendered in software, GifTextureView will draw nothing.</p>
* <p>GIF source can be specified in XML or by calling {@link #setInputSource(InputSource)}</p>
* <pre> {@code
* <pl.droidsonroids.gif.GifTextureView
* xmlns:app="http://schemas.android.com/apk/res-auto"
* android:id="@+id/gif_texture_view"
* android:scaleType="fitEnd"
* app:gifSource="@drawable/animation"
* android:layout_width="match_parent"
* android:layout_height="match_parent"> }
* </pre>
* Note that <b>src</b> attribute comes from app namespace (you can call it whatever you want) not from
* android one. Drawable, raw, mipmap resources and assets can be specified through XML. If value is a string
* (referenced from resources or entered directly) it will be treated as an asset.
* <p>Unlike {@link TextureView} GifTextureView is transparent by default, but it can be changed by
* {@link #setOpaque(boolean)}.
* You can use scale types the same way as in {@link android.widget.ImageView}.</p>
*/
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class GifTextureView extends TextureView {
private static final ScaleType[] sScaleTypeArray = {
ScaleType.MATRIX,
ScaleType.FIT_XY,
ScaleType.FIT_START,
ScaleType.FIT_CENTER,
ScaleType.FIT_END,
ScaleType.CENTER,
ScaleType.CENTER_CROP,
ScaleType.CENTER_INSIDE
};
private ScaleType mScaleType = ScaleType.FIT_CENTER;
private final Matrix mTransform = new Matrix();
private InputSource mInputSource;
private boolean mFreezesAnimation;
private RenderThread mRenderThread;
private float mSpeedFactor = 1f;
public GifTextureView(Context context) {
super(context);
init(null, 0, 0);
}
public GifTextureView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0, 0);
}
public GifTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0);
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public GifTextureView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs != null) {
final int scaleTypeIndex = attrs.getAttributeIntValue(GifViewUtils.ANDROID_NS, "scaleType", -1);
if (scaleTypeIndex >= 0 && scaleTypeIndex < sScaleTypeArray.length) {
mScaleType = sScaleTypeArray[scaleTypeIndex];
}
final TypedArray textureViewAttributes = getContext().obtainStyledAttributes(attrs, R.styleable
.GifTextureView, defStyleAttr, defStyleRes);
mInputSource = findSource(textureViewAttributes);
super.setOpaque(textureViewAttributes.getBoolean(R.styleable.GifTextureView_isOpaque, false));
textureViewAttributes.recycle();
mFreezesAnimation = GifViewUtils.isFreezingAnimation(this, attrs, defStyleAttr, defStyleRes);
} else {
super.setOpaque(false);
}
if (!isInEditMode()) {
mRenderThread = new RenderThread(this);
if (mInputSource != null) {
mRenderThread.start();
}
}
}
/**
* Always throws {@link UnsupportedOperationException}. Changing {@link SurfaceTextureListener}
* is not supported.
*
* @param listener ignored
*/
@Override
public void setSurfaceTextureListener(SurfaceTextureListener listener) {
throw new UnsupportedOperationException("Changing SurfaceTextureListener is not supported");
}
/**
* Always returns null since changing {@link SurfaceTextureListener} is not supported.
*
* @return always null
*/
@Override
public SurfaceTextureListener getSurfaceTextureListener() {
return null;
}
/**
* Always throws {@link UnsupportedOperationException}. Changing {@link SurfaceTexture} is not
* supported.
*
* @param surfaceTexture ignored
*/
@Override
public void setSurfaceTexture(SurfaceTexture surfaceTexture) {
throw new UnsupportedOperationException("Changing SurfaceTexture is not supported");
}
private static InputSource findSource(final TypedArray textureViewAttributes) {
final TypedValue value = new TypedValue();
if (!textureViewAttributes.getValue(R.styleable.GifTextureView_gifSource, value)) {
return null;
}
if (value.resourceId != 0) {
final String resourceTypeName = textureViewAttributes.getResources().getResourceTypeName(value.resourceId);
if (GifViewUtils.SUPPORTED_RESOURCE_TYPE_NAMES.contains(resourceTypeName)) {
return new InputSource.ResourcesSource(textureViewAttributes.getResources(), value.resourceId);
} else if (!"string".equals(resourceTypeName)) {
throw new IllegalArgumentException("Expected string, drawable, mipmap or raw resource type. '" + resourceTypeName + "' is not supported");
}
}
return new InputSource.AssetSource(textureViewAttributes.getResources().getAssets(), value.string.toString());
}
private static class RenderThread extends Thread implements SurfaceTextureListener {
final ConditionVariable isSurfaceValid = new ConditionVariable();
private GifInfoHandle mGifInfoHandle = new GifInfoHandle();
private IOException mIOException;
long[] mSavedState;
private final WeakReference<GifTextureView> mGifTextureViewReference;
RenderThread(final GifTextureView gifTextureView) {
super("GifRenderThread");
mGifTextureViewReference = new WeakReference<>(gifTextureView);
}
@Override
public void run() {
try {
final GifTextureView gifTextureView = mGifTextureViewReference.get();
if (gifTextureView == null) {
return;
}
mGifInfoHandle = gifTextureView.mInputSource.open();
mGifInfoHandle.setOptions((char) 1, gifTextureView.isOpaque());
} catch (IOException ex) {
mIOException = ex;
return;
}
final GifTextureView gifTextureView = mGifTextureViewReference.get();
if (gifTextureView == null) {
mGifInfoHandle.recycle();
return;
}
gifTextureView.setSuperSurfaceTextureListener(this);
final boolean isSurfaceAvailable = gifTextureView.isAvailable();
isSurfaceValid.set(isSurfaceAvailable);
if (isSurfaceAvailable) {
gifTextureView.post(new Runnable() {
@Override
public void run() {
gifTextureView.updateTextureViewSize(mGifInfoHandle);
}
});
}
mGifInfoHandle.setSpeedFactor(gifTextureView.mSpeedFactor);
while (!isInterrupted()) {
try {
isSurfaceValid.block();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
final SurfaceTexture surfaceTexture = gifTextureView.getSurfaceTexture();
if (surfaceTexture == null) {
continue;
}
final Surface surface = new Surface(surfaceTexture);
try {
mGifInfoHandle.bindSurface(surface, mSavedState);
} finally {
surface.release();
surfaceTexture.release();
}
}
mGifInfoHandle.recycle();
mGifInfoHandle = new GifInfoHandle();
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
final GifTextureView gifTextureView = mGifTextureViewReference.get();
if (gifTextureView != null)
gifTextureView.updateTextureViewSize(mGifInfoHandle);
isSurfaceValid.open();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//no-op
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
isSurfaceValid.close();
mGifInfoHandle.postUnbindSurface();
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
//no-op
}
void dispose(@NonNull final GifTextureView gifTextureView, @Nullable final PlaceholderDrawListener drawer) {
isSurfaceValid.close();
final SurfaceTextureListener listener = drawer != null ? new PlaceholderDrawingSurfaceTextureListener(drawer) : null;
gifTextureView.setSuperSurfaceTextureListener(listener);
mGifInfoHandle.postUnbindSurface();
interrupt();
}
}
private void setSuperSurfaceTextureListener(SurfaceTextureListener listener) {
super.setSurfaceTextureListener(listener);
}
/**
* Indicates whether the content of this GifTextureView is opaque. The
* content is assumed to be <b>non-opaque</b> by default (unlike {@link TextureView}.
* View that is known to be opaque can take a faster drawing case than non-opaque one.<br>
* Opacity change will cause animation to restart.
*
* @param opaque True if the content of this GifTextureView is opaque,
* false otherwise
*/
@Override
public void setOpaque(boolean opaque) {
if (opaque != isOpaque()) {
super.setOpaque(opaque);
setInputSource(mInputSource);
}
}
@Override
protected void onDetachedFromWindow() {
mRenderThread.dispose(this, null);
super.onDetachedFromWindow();
final SurfaceTexture surfaceTexture = getSurfaceTexture();
if (surfaceTexture != null) {
surfaceTexture.release();
}
}
/**
* Sets the source of the animation. Pass {@code null} to remove current source.
* Equivalent of {@code setInputSource(inputSource, null)}.
*
* @param inputSource new animation source, may be null
*/
public synchronized void setInputSource(@Nullable InputSource inputSource) {
setInputSource(inputSource, null);
}
/**
* Sets the source of the animation and optionally placeholder drawer. Pass {@code null inputSource} to remove current source.
* {@code placeholderDrawListener} is overwritten on {@code setInputSource(inputSource)} call.
*
* @param inputSource new animation source, may be null
* @param placeholderDrawListener placeholder draw listener, may be null
*/
@Beta
public synchronized void setInputSource(@Nullable InputSource inputSource, @Nullable PlaceholderDrawListener placeholderDrawListener) {
mRenderThread.dispose(this, placeholderDrawListener);
mInputSource = inputSource;
mRenderThread = new RenderThread(this);
if (inputSource != null) {
mRenderThread.start();
}
}
/**
* Equivalent of {@link GifDrawable#setSpeed(float)}.
*
* @param factor new speed factor, eg. 0.5f means half speed, 1.0f - normal, 2.0f - double speed
* @throws IllegalArgumentException if {@code factor <= 0}
* @see GifDrawable#setSpeed(float)
*/
public void setSpeed(@FloatRange(from = 0, fromInclusive = false) float factor) {
mSpeedFactor = factor;
mRenderThread.mGifInfoHandle.setSpeedFactor(factor);
}
/**
* Returns last {@link IOException} occurred during loading or playing GIF (in such case only {@link GifIOException}
* can be returned. Null is returned when source is not set, surface was not yet created or no error
* occurred.
*
* @return exception occurred during loading or playing GIF or null
*/
@Nullable
public IOException getIOException() {
if (mRenderThread.mIOException != null) {
return mRenderThread.mIOException;
} else {
return GifIOException.fromCode(mRenderThread.mGifInfoHandle.getNativeErrorCode());
}
}
/**
* Controls how the image should be resized or moved to match the size
* of this GifTextureView.
*
* @param scaleType The desired scaling mode.
*/
public void setScaleType(@NonNull ScaleType scaleType) {
mScaleType = scaleType;
updateTextureViewSize(mRenderThread.mGifInfoHandle);
}
/**
* @return the current scale type in use by this View.
* @see ScaleType
*/
public ScaleType getScaleType() {
return mScaleType;
}
private void updateTextureViewSize(final GifInfoHandle gifInfoHandle) {
final Matrix transform = new Matrix();
final float viewWidth = getWidth();
final float viewHeight = getHeight();
final float scaleRef;
final float scaleX = gifInfoHandle.getWidth() / viewWidth;
final float scaleY = gifInfoHandle.getHeight() / viewHeight;
RectF src = new RectF(0, 0, gifInfoHandle.getWidth(), gifInfoHandle.getHeight());
RectF dst = new RectF(0, 0, viewWidth, viewHeight);
switch (mScaleType) {
case CENTER:
transform.setScale(scaleX, scaleY, viewWidth / 2, viewHeight / 2);
break;
case CENTER_CROP:
scaleRef = 1 / Math.min(scaleX, scaleY);
transform.setScale(scaleRef * scaleX, scaleRef * scaleY, viewWidth / 2, viewHeight / 2);
break;
case CENTER_INSIDE:
if (gifInfoHandle.getWidth() <= viewWidth && gifInfoHandle.getHeight() <= viewHeight) {
scaleRef = 1.0f;
} else {
scaleRef = Math.min(1 / scaleX, 1 / scaleY);
}
transform.setScale(scaleRef * scaleX, scaleRef * scaleY, viewWidth / 2, viewHeight / 2);
break;
case FIT_CENTER:
transform.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
transform.preScale(scaleX, scaleY);
break;
case FIT_END:
transform.setRectToRect(src, dst, Matrix.ScaleToFit.END);
transform.preScale(scaleX, scaleY);
break;
case FIT_START:
transform.setRectToRect(src, dst, Matrix.ScaleToFit.START);
transform.preScale(scaleX, scaleY);
break;
case FIT_XY:
return;
case MATRIX:
transform.set(mTransform);
transform.preScale(scaleX, scaleY);
break;
}
super.setTransform(transform);
}
/**
* Wrapper of {@link #setTransform(Matrix)}. Introduced to preserve the same API as in
* {@link GifImageView}.
*
* @param matrix The transform to apply to the content of this view.
*/
public void setImageMatrix(Matrix matrix) {
setTransform(matrix);
}
/**
* Works like {@link TextureView#setTransform(Matrix)} but transform will take effect only if
* scale type is set to {@link ScaleType#MATRIX} through XML attribute or via {@link #setScaleType(ScaleType)}
*
* @param transform The transform to apply to the content of this view.
*/
@Override
public void setTransform(Matrix transform) {
mTransform.set(transform);
updateTextureViewSize(mRenderThread.mGifInfoHandle);
}
/**
* Returns the transform associated with this texture view, either set explicitly by {@link #setTransform(Matrix)}
* or computed according to the current scale type.
*
* @param transform The {@link Matrix} in which to copy the current transform. Can be null.
* @return The specified matrix if not null or a new {@link Matrix} instance otherwise.
* @see #setTransform(android.graphics.Matrix)
* @see #setScaleType(ScaleType)
*/
@Override
public Matrix getTransform(Matrix transform) {
if (transform == null) {
transform = new Matrix();
}
transform.set(mTransform);
return transform;
}
@Override
public Parcelable onSaveInstanceState() {
mRenderThread.mSavedState = mRenderThread.mGifInfoHandle.getSavedState();
return new GifViewSavedState(super.onSaveInstanceState(), mFreezesAnimation ? mRenderThread.mSavedState : null);
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof GifViewSavedState)) {
super.onRestoreInstanceState(state);
return;
}
GifViewSavedState ss = (GifViewSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mRenderThread.mSavedState = ss.mStates[0];
}
/**
* Sets whether animation position is saved in {@link #onSaveInstanceState()} and restored
* in {@link #onRestoreInstanceState(Parcelable)}
*
* @param freezesAnimation whether animation position is saved
*/
public void setFreezesAnimation(boolean freezesAnimation) {
mFreezesAnimation = freezesAnimation;
}
/**
* This listener can be used to be notified when the {@link GifTextureView} content placeholder can be drawn.
* Placeholder is displayed before proper input source is loaded and remains visible when input source loading fails.
*/
@Beta
public interface PlaceholderDrawListener {
/**
* Called when surface is ready and placeholder has to be drawn.
* It may occur more than once (eg. if {@code View} visibility is toggled before input source is loaded)
* or never (eg. when {@code View} is never visible).<br>
* Note that it is an error to use {@code canvas} after this method return.
* @param canvas canvas to draw into
*/
void onDrawPlaceholder(Canvas canvas);
}
}