blob: a385c01ce543ec649967115939d9a98a1b5410ea [file] [log] [blame]
package org.skia.skottie;
import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.graphics.SurfaceTexture;
import android.opengl.GLUtils;
import android.util.Log;
import android.view.Choreographer;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLSurface;
public class SkottieAnimation extends Animator implements Choreographer.FrameCallback,
TextureView.SurfaceTextureListener, SurfaceHolder.Callback {
class Config {
int mSurfaceWidth;
int mSurfaceHeight;
boolean mValidSurface;
}
private final SkottieRunner mRunner = SkottieRunner.getInstance();
private static final String LOG_TAG = "SkottiePlayer";
private boolean mIsRunning = false;
private SurfaceTexture mSurfaceTexture;
private EGLSurface mEglSurface;
private boolean mNewSurface = false;
private SurfaceHolder mSurfaceHolder;
private int mRepeatCount;
private int mRepeatCounter;
private Config config = new Config();
private int mBackgroundColor;
private long mNativeProxy;
private long mDuration; // duration in ms of the animation
private float mProgress; // animation progress in the range of 0.0f to 1.0f
private long mAnimationStartTime; // time in System.nanoTime units, when started
SkottieAnimation(SurfaceTexture surfaceTexture, InputStream is) {
if (init(is)) {
mSurfaceTexture = surfaceTexture;
}
}
SkottieAnimation(TextureView view, InputStream is, int backgroundColor, int repeatCount) {
if (init(is)) {
mSurfaceTexture = view.getSurfaceTexture();
}
view.setSurfaceTextureListener(this);
mBackgroundColor = backgroundColor;
mRepeatCount = repeatCount;
mRepeatCounter = mRepeatCount;
}
SkottieAnimation(SurfaceView view, InputStream is, int backgroundColor, int repeatCount) {
if (init(is)) {
mSurfaceHolder = view.getHolder();
}
mSurfaceHolder.addCallback(this);
mBackgroundColor = backgroundColor;
mRepeatCount = repeatCount;
mRepeatCounter = mRepeatCount;
}
void setSurfaceTexture(SurfaceTexture s) {
mSurfaceTexture = s;
}
private ByteBuffer convertToByteBuffer(InputStream is) throws IOException {
if (is instanceof FileInputStream) {
FileChannel fileChannel = ((FileInputStream)is).getChannel();
return fileChannel.map(FileChannel.MapMode.READ_ONLY,
fileChannel.position(), fileChannel.size());
}
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
byte[] tmpStorage = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(tmpStorage, 0, tmpStorage.length)) != -1) {
byteStream.write(tmpStorage, 0, bytesRead);
}
byteStream.flush();
tmpStorage = byteStream.toByteArray();
ByteBuffer buffer = ByteBuffer.allocateDirect(tmpStorage.length);
buffer.order(ByteOrder.nativeOrder());
buffer.put(tmpStorage, 0, tmpStorage.length);
return buffer.asReadOnlyBuffer();
}
private boolean init(InputStream is) {
ByteBuffer byteBuffer;
try {
byteBuffer = convertToByteBuffer(is);
} catch (IOException e) {
Log.e(LOG_TAG, "failed to read input stream", e);
return false;
}
long proxy = mRunner.getNativeProxy();
mNativeProxy = nCreateProxy(proxy, byteBuffer);
mDuration = nGetDuration(mNativeProxy);
mProgress = 0f;
return true;
}
private void notifyAnimationEnd() {
if (this.getListeners() != null) {
for (AnimatorListener l : this.getListeners()) {
l.onAnimationEnd(this);
}
}
}
@Override
protected void finalize() throws Throwable {
try {
end();
nDeleteProxy(mNativeProxy);
mNativeProxy = 0;
} finally {
super.finalize();
}
}
// Always call this on GL thread
public void updateSurface(int width, int height) {
config.mSurfaceWidth = width;
config.mSurfaceHeight = height;
mNewSurface = true;
drawFrame();
}
@Override
public void start() {
try {
mRunner.runOnGLThread(() -> {
if (!mIsRunning) {
long currentTime = System.nanoTime();
mAnimationStartTime = currentTime - (long)(1000000 * mDuration * mProgress);
mIsRunning = true;
mNewSurface = true;
mRepeatCounter = mRepeatCount;
doFrame(currentTime);
}
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "start failed", t);
throw new RuntimeException(t);
}
if (this.getListeners() != null) {
for (AnimatorListener l : this.getListeners()) {
l.onAnimationStart(this);
}
}
}
@Override
public void end() {
try {
mRunner.runOnGLThread(() -> {
mIsRunning = false;
if (mEglSurface != null) {
// Ensure we always have a valid surface & context.
mRunner.mEgl.eglMakeCurrent(mRunner.mEglDisplay, mRunner.mPBufferSurface,
mRunner.mPBufferSurface, mRunner.mEglContext);
mRunner.mEgl.eglDestroySurface(mRunner.mEglDisplay, mEglSurface);
mEglSurface = null;
}
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "stop failed", t);
throw new RuntimeException(t);
}
notifyAnimationEnd();
}
@Override
public void pause() {
try {
mRunner.runOnGLThread(() -> {
mIsRunning = false;
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "pause failed", t);
throw new RuntimeException(t);
}
}
@Override
public void resume() {
try {
mRunner.runOnGLThread(() -> {
if (!mIsRunning) {
long currentTime = System.nanoTime();
mAnimationStartTime = currentTime - (long)(1000000 * mDuration * mProgress);
mIsRunning = true;
mNewSurface = true;
doFrame(currentTime);
}
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "resume failed", t);
throw new RuntimeException(t);
}
}
// TODO: add support for start delay
@Override
public long getStartDelay() {
return 0;
}
// TODO: add support for start delay
@Override
public void setStartDelay(long startDelay) {
}
@Override
public Animator setDuration(long duration) {
return null;
}
@Override
public boolean isRunning() {
return mIsRunning;
}
@Override
public long getDuration() {
return mDuration;
}
@Override
public long getTotalDuration() {
if (mRepeatCount == -1) {
return DURATION_INFINITE;
}
// TODO: add start delay when implemented
return mDuration * (1 + mRepeatCount);
}
// TODO: support TimeInterpolators
@Override
public void setInterpolator(TimeInterpolator value) {
}
public void setProgress(float progress) {
try {
mRunner.runOnGLThread(() -> {
mProgress = progress;
if (mIsRunning) {
mAnimationStartTime = System.nanoTime()
- (long)(1000000 * mDuration * mProgress);
}
drawFrame();
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "setProgress failed", t);
throw new RuntimeException(t);
}
}
public float getProgress() {
return mProgress;
}
private void drawFrame() {
try {
boolean forceDraw = false;
if (mNewSurface) {
forceDraw = true;
// if there is a new SurfaceTexture, we need to recreate the EGL surface.
if (mEglSurface != null) {
mRunner.mEgl.eglDestroySurface(mRunner.mEglDisplay, mEglSurface);
mEglSurface = null;
}
mNewSurface = false;
}
if (mEglSurface == null) {
// block for Texture Views
if (mSurfaceTexture != null) {
mEglSurface = mRunner.mEgl.eglCreateWindowSurface(mRunner.mEglDisplay,
mRunner.mEglConfig, mSurfaceTexture, null);
checkSurface();
// block for Surface Views
} else if (mSurfaceHolder != null) {
mEglSurface = mRunner.mEgl.eglCreateWindowSurface(mRunner.mEglDisplay,
mRunner.mEglConfig, mSurfaceHolder, null);
checkSurface();
}
}
if (mEglSurface != null) {
if (!mRunner.mEgl.eglMakeCurrent(mRunner.mEglDisplay, mEglSurface, mEglSurface,
mRunner.mEglContext)) {
// If eglMakeCurrent failed, recreate EGL surface on next frame.
Log.w(LOG_TAG, "eglMakeCurrent failed "
+ GLUtils.getEGLErrorString(mRunner.mEgl.eglGetError()));
mNewSurface = true;
return;
}
// only if nDrawFrames() returns true do we need to swap buffers
if(nDrawFrame(mNativeProxy, config.mSurfaceWidth, config.mSurfaceHeight, false,
mProgress, mBackgroundColor, forceDraw)) {
if (!mRunner.mEgl.eglSwapBuffers(mRunner.mEglDisplay, mEglSurface)) {
int error = mRunner.mEgl.eglGetError();
if (error == EGL10.EGL_BAD_SURFACE
|| error == EGL10.EGL_BAD_NATIVE_WINDOW) {
// For some reason our surface was destroyed. Recreate EGL surface
// on next frame.
mNewSurface = true;
// This really shouldn't happen, but if it does we can recover
// easily by just not trying to use the surface anymore
Log.w(LOG_TAG, "swapBuffers failed "
+ GLUtils.getEGLErrorString(error));
return;
}
// Some other fatal EGL error happened, log an error and stop the
// animation.
throw new RuntimeException("Cannot swap buffers "
+ GLUtils.getEGLErrorString(error));
}
}
// If animation stopped, release EGL surface.
if (!mIsRunning) {
// Ensure we always have a valid surface & context.
mRunner.mEgl.eglMakeCurrent(mRunner.mEglDisplay, mRunner.mPBufferSurface,
mRunner.mPBufferSurface, mRunner.mEglContext);
mRunner.mEgl.eglDestroySurface(mRunner.mEglDisplay, mEglSurface);
mEglSurface = null;
}
}
} catch (Throwable t) {
Log.e(LOG_TAG, "drawFrame failed", t);
mIsRunning = false;
}
}
private void checkSurface() throws RuntimeException {
// ensure eglSurface was created
if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
// If failed to create a surface, log an error and stop the animation
int error = mRunner.mEgl.eglGetError();
throw new RuntimeException("createWindowSurface failed "
+ GLUtils.getEGLErrorString(error));
}
}
@Override
public void doFrame(long frameTimeNanos) {
if (mIsRunning) {
// Schedule next frame.
Choreographer.getInstance().postFrameCallback(this);
// Advance animation.
long durationNS = mDuration * 1000000;
long timeSinceAnimationStartNS = frameTimeNanos - mAnimationStartTime;
long animationProgressNS = timeSinceAnimationStartNS % durationNS;
mProgress = animationProgressNS / (float)durationNS;
if (timeSinceAnimationStartNS > durationNS) {
mAnimationStartTime += durationNS; // prevents overflow
}
if (timeSinceAnimationStartNS > durationNS) {
if (mRepeatCounter > 0) {
mRepeatCounter--;
} else if (mRepeatCounter == 0) {
mIsRunning = false;
mProgress = 1;
notifyAnimationEnd();
}
}
}
if (config.mValidSurface) {
drawFrame();
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// will be called on UI thread
try {
mRunner.runOnGLThread(() -> {
mSurfaceTexture = surface;
updateSurface(width, height);
config.mValidSurface = true;
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "updateSurface failed", t);
throw new RuntimeException(t);
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// will be called on UI thread
onSurfaceTextureAvailable(surface, width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
// will be called on UI thread
onSurfaceTextureAvailable(null, 0, 0);
config.mValidSurface = false;
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
// Inherited from SurfaceHolder
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
try {
mRunner.runOnGLThread(() -> {
mSurfaceHolder = holder;
updateSurface(width, height);
config.mValidSurface = true;
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "updateSurface failed", t);
throw new RuntimeException(t);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
config.mValidSurface = false;
surfaceChanged(null, 0, 0, 0);
}
Config getBackingViewConfig() {
return config;
}
void setBackingViewConfig(Config config) {
this.config.mSurfaceHeight = config.mSurfaceHeight;
this.config.mSurfaceWidth = config.mSurfaceWidth;
this.config.mValidSurface = config.mValidSurface;
}
private native long nCreateProxy(long runner, ByteBuffer data);
private native void nDeleteProxy(long nativeProxy);
private native boolean nDrawFrame(long nativeProxy, int width, int height,
boolean wideColorGamut, float progress,
int backgroundColor, boolean forceDraw);
private native long nGetDuration(long nativeProxy);
}