Prototype an Android lottie player

Create a new Skottie test app, which plays lottie files using
a TextureView.
Implement SkottieView, which takes a JSON input stream and
plays the animation.

Bug: skia:
Change-Id: Ic62688b91692c28f35b13356d1e99b4d15d3e30f
Reviewed-on: https://skia-review.googlesource.com/130125
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Reviewed-by: Florin Malita <fmalita@chromium.org>
Commit-Queue: Stan Iliev <stani@google.com>
This commit is contained in:
Stan Iliev 2018-08-02 11:10:52 -04:00 committed by Skia Commit-Bot
parent 3526cfaf05
commit 93151726ed
16 changed files with 1419 additions and 1 deletions

View File

@ -1893,6 +1893,23 @@ if (skia_enable_tools) {
libs = [ "android" ]
}
}
if (is_android && skia_enable_gpu) {
test_app("skottie_android") {
is_shared_library = true
sources = [
"platform_tools/android/apps/skottie/src/main/cpp/JavaInputStreamAdaptor.cpp",
"platform_tools/android/apps/skottie/src/main/cpp/native-lib.cpp",
]
libs = []
include_dirs = []
deps = [
":skia",
"modules/skottie",
]
}
}
test_app("list_gms") {
sources = [

View File

@ -1,4 +1,5 @@
include ':viewer'
include ':skqp'
include ':arcore' //must build out directory first: bin/gn gen out/arm64 --args='ndk="NDKPATH" target_cpu="ABI" is_component_build=true'
include ':skar_java'
include ':skar_java'
include ':skottie'

View File

@ -0,0 +1,35 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
apply plugin: 'com.android.application'
dependencies {
implementation 'com.android.support:support-v13:23.3.0'
implementation 'com.android.support:appcompat-v7:23.3.0'
}
android {
compileSdkVersion 23
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
flavorDimensions "tier"
defaultConfig {
applicationId "org.skia.skottie"
minSdkVersion 23
targetSdkVersion 23
versionCode 1
versionName "1.0"
signingConfig signingConfigs.debug
}
flavorDimensions "base"
sourceSets.main.jni.srcDirs = [] //disable automatic ndk-build call
sourceSets.main.jniLibs.srcDir "src/main/libs"
productFlavors { universal{}; arm {}; arm64 {}; x86 {}; x64 {}; arm64vulkan{}; }
setupSkiaLibraryBuild(project, applicationVariants, "libskottie_android")
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- BEGIN_INCLUDE(manifest) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.skia.skottie"
android:versionCode="1"
android:versionName="1.0">
<application
android:allowBackup="false"
android:theme="@android:style/Theme.Holo.Light"
android:name=".SkottieApplication"
android:label="Skottie">
<activity android:name=".SkottieActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<!-- END_INCLUDE(manifest) -->

View File

@ -0,0 +1,256 @@
/*
* 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.
*/
#include "JavaInputStreamAdaptor.h"
#include "SkPostConfig.h"
static jclass findClassCheck(JNIEnv* env, const char classname[]) {
jclass clazz = env->FindClass(classname);
SkASSERT(!env->ExceptionCheck());
return clazz;
}
static jmethodID getMethodIDCheck(JNIEnv* env, jclass clazz,
const char methodname[], const char type[]) {
jmethodID id = env->GetMethodID(clazz, methodname, type);
SkASSERT(!env->ExceptionCheck());
return id;
}
static jmethodID gInputStream_readMethodID;
static jmethodID gInputStream_skipMethodID;
static JNIEnv* get_env_or_die(JavaVM* jvm) {
JNIEnv* env;
if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
char errorMessage[256];
sprintf(errorMessage, "Failed to get JNIEnv for JavaVM: %p", jvm);
SK_ABORT(errorMessage);
}
return env;
}
/**
* Wrapper for a Java InputStream.
*/
class JavaInputStreamAdaptor : public SkStream {
JavaInputStreamAdaptor(JavaVM* jvm, jobject js, jbyteArray ar, jint capacity,
bool swallowExceptions)
: fJvm(jvm)
, fJavaInputStream(js)
, fJavaByteArray(ar)
, fCapacity(capacity)
, fBytesRead(0)
, fIsAtEnd(false)
, fSwallowExceptions(swallowExceptions) {}
public:
static JavaInputStreamAdaptor* Create(JNIEnv* env, jobject js, jbyteArray ar,
bool swallowExceptions) {
JavaVM* jvm;
if (env->GetJavaVM(&jvm) != JNI_OK) {
SK_ABORT("Failed to get JavaVM");
}
js = env->NewGlobalRef(js);
if (!js) {
return nullptr;
}
ar = (jbyteArray) env->NewGlobalRef(ar);
if (!ar) {
env->DeleteGlobalRef(js);
return nullptr;
}
jint capacity = env->GetArrayLength(ar);
return new JavaInputStreamAdaptor(jvm, js, ar, capacity, swallowExceptions);
}
~JavaInputStreamAdaptor() override {
auto* env = get_env_or_die(fJvm);
env->DeleteGlobalRef(fJavaInputStream);
env->DeleteGlobalRef(fJavaByteArray);
}
size_t read(void* buffer, size_t size) override {
auto* env = get_env_or_die(fJvm);
if (!fSwallowExceptions && checkException(env)) {
// Just in case the caller did not clear from a previous exception.
return 0;
}
if (NULL == buffer) {
if (0 == size) {
return 0;
} else {
/* InputStream.skip(n) can return <=0 but still not be at EOF
If we see that value, we need to call read(), which will
block if waiting for more data, or return -1 at EOF
*/
size_t amountSkipped = 0;
do {
size_t amount = this->doSkip(size - amountSkipped, env);
if (0 == amount) {
char tmp;
amount = this->doRead(&tmp, 1, env);
if (0 == amount) {
// if read returned 0, we're at EOF
fIsAtEnd = true;
break;
}
}
amountSkipped += amount;
} while (amountSkipped < size);
return amountSkipped;
}
}
return this->doRead(buffer, size, env);
}
bool isAtEnd() const override { return fIsAtEnd; }
private:
size_t doRead(void* buffer, size_t size, JNIEnv* env) {
size_t bytesRead = 0;
// read the bytes
do {
jint requested = 0;
if (size > static_cast<size_t>(fCapacity)) {
requested = fCapacity;
} else {
// This is safe because requested is clamped to (jint)
// fCapacity.
requested = static_cast<jint>(size);
}
jint n = env->CallIntMethod(fJavaInputStream,
gInputStream_readMethodID, fJavaByteArray, 0, requested);
if (checkException(env)) {
SkDebugf("---- read threw an exception\n");
return bytesRead;
}
if (n < 0) { // n == 0 should not be possible, see InputStream read() specifications.
fIsAtEnd = true;
break; // eof
}
env->GetByteArrayRegion(fJavaByteArray, 0, n,
reinterpret_cast<jbyte*>(buffer));
if (checkException(env)) {
SkDebugf("---- read:GetByteArrayRegion threw an exception\n");
return bytesRead;
}
buffer = (void*)((char*)buffer + n);
bytesRead += n;
size -= n;
fBytesRead += n;
} while (size != 0);
return bytesRead;
}
size_t doSkip(size_t size, JNIEnv* env) {
jlong skipped = env->CallLongMethod(fJavaInputStream,
gInputStream_skipMethodID, (jlong)size);
if (checkException(env)) {
SkDebugf("------- skip threw an exception\n");
return 0;
}
if (skipped < 0) {
skipped = 0;
}
return (size_t)skipped;
}
bool checkException(JNIEnv* env) {
if (!env->ExceptionCheck()) {
return false;
}
env->ExceptionDescribe();
if (fSwallowExceptions) {
env->ExceptionClear();
}
// There is no way to recover from the error, so consider the stream
// to be at the end.
fIsAtEnd = true;
return true;
}
JavaVM* fJvm;
jobject fJavaInputStream;
jbyteArray fJavaByteArray;
const jint fCapacity;
size_t fBytesRead;
bool fIsAtEnd;
const bool fSwallowExceptions;
};
static SkStream* CreateJavaInputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage,
bool swallowExceptions = true) {
return JavaInputStreamAdaptor::Create(env, stream, storage, swallowExceptions);
}
static SkMemoryStream* adaptor_to_mem_stream(SkStream* stream) {
SkASSERT(stream != NULL);
size_t bufferSize = 4096;
size_t streamLen = 0;
size_t len;
char* data = (char*)sk_malloc_throw(bufferSize);
while ((len = stream->read(data + streamLen,
bufferSize - streamLen)) != 0) {
streamLen += len;
if (streamLen == bufferSize) {
bufferSize *= 2;
data = (char*)sk_realloc_throw(data, bufferSize);
}
}
data = (char*)sk_realloc_throw(data, streamLen);
SkMemoryStream* streamMem = new SkMemoryStream();
streamMem->setMemoryOwned(data, streamLen);
return streamMem;
}
SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream,
jbyteArray storage) {
std::unique_ptr<SkStream> adaptor(CreateJavaInputStreamAdaptor(env, stream, storage));
if (NULL == adaptor.get()) {
return NULL;
}
return adaptor_to_mem_stream(adaptor.get());
}
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
jclass inputStream_Clazz = findClassCheck(env, "java/io/InputStream");
gInputStream_readMethodID = getMethodIDCheck(env, inputStream_Clazz, "read", "([BII)I");
gInputStream_skipMethodID = getMethodIDCheck(env, inputStream_Clazz, "skip", "(J)J");
return JNI_VERSION_1_6;
}

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
#pragma once
#include <jni.h>
#include <SkStream.h>
#include <SkMalloc.h>
SkStreamRewindable* CopyJavaInputStream(JNIEnv* env, jobject stream,
jbyteArray storage);

View File

@ -0,0 +1,211 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <jni.h>
#include <math.h>
#include <string>
#include <utility>
#include <SkColor.h>
#include <SkCanvas.h>
#include <SkBitmap.h>
#include <SkSurface.h>
#include <SkTime.h>
#include <GrContextOptions.h>
#include <GrContext.h>
#include <gl/GrGLInterface.h>
#include <GrBackendSurface.h>
#include <gl/GrGLTypes.h>
#include <Skottie.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <GLES3/gl3.h>
#include <android/trace.h>
#include "JavaInputStreamAdaptor.h"
#define STENCIL_BUFFER_SIZE 8
/*#define ATRACE_NAME(name) ScopedTrace ___tracer(name)
// ATRACE_CALL is an ATRACE_NAME that uses the current function name.
#define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
namespace {
class ScopedTrace {
public:
inline ScopedTrace(const char *name) {
ATrace_beginSection(name);
}
inline ~ScopedTrace() {
ATrace_endSection();
}
};
}*/
//disable atrace
#define ATRACE_NAME(name)
#define ATRACE_CALL()
struct SkottieRunner {
sk_sp<GrContext> mGrContext;
};
extern "C" JNIEXPORT jlong
JNICALL
Java_org_skia_skottie_SkottieRunner_nCreateProxy(JNIEnv *env, jclass clazz) {
sk_sp<const GrGLInterface> glInterface(GrGLCreateNativeInterface());
if (!glInterface.get()) {
return 0;
}
GrContextOptions options;
options.fDisableDistanceFieldPaths = true;
options.fDisableCoverageCountingPaths = true;
sk_sp<GrContext> grContext = GrContext::MakeGL(std::move(glInterface), options);
if (!grContext.get()) {
return 0;
}
SkottieRunner* skottie = new SkottieRunner();
skottie->mGrContext = grContext;
return (jlong) skottie;
}
extern "C" JNIEXPORT void
JNICALL
Java_org_skia_skottie_SkottieRunner_nDeleteProxy(JNIEnv *env, jclass clazz, jlong nativeProxy) {
if (!nativeProxy) {
return;
}
SkottieRunner* skottie = reinterpret_cast<SkottieRunner*>(nativeProxy);
if (skottie->mGrContext) {
skottie->mGrContext->releaseResourcesAndAbandonContext();
skottie->mGrContext.reset();
}
delete skottie;
}
struct SkottieAnimation {
SkottieRunner *mRunner;
std::unique_ptr<SkStream> mStream;
sk_sp<skottie::Animation> mAnimation;
long mTimeBase;
float mDuration; //in milliseconds
};
extern "C" JNIEXPORT jlong
JNICALL
Java_org_skia_skottie_SkottieRunner_00024SkottieAnimation_nCreateProxy(JNIEnv *env, jobject clazz,
jlong runner, jobject is,
jbyteArray storage) {
if (!runner) {
return 0;
}
SkottieRunner *skottieRunner = reinterpret_cast<SkottieRunner*>(runner);
std::unique_ptr<SkStream> stream(CopyJavaInputStream(env, is, storage));
if (!stream.get()) {
// Cannot create a stream
return 0;
}
SkottieAnimation* skottieAnimation = new SkottieAnimation();
skottieAnimation->mRunner = skottieRunner;
skottieAnimation->mStream = std::move(stream);
skottieAnimation->mAnimation = skottie::Animation::Make(skottieAnimation->mStream.get(),
nullptr, nullptr);
skottieAnimation->mTimeBase = 0.0f; // force a time reset
skottieAnimation->mDuration = 1000 * skottieAnimation->mAnimation->duration();
if (!skottieAnimation->mAnimation) {
//failed to load Bodymovin animation
delete skottieAnimation;
return 0;
}
return (jlong) skottieAnimation;
}
extern "C" JNIEXPORT void
JNICALL
Java_org_skia_skottie_SkottieRunner_00024SkottieAnimation_nDeleteProxy(JNIEnv *env, jclass clazz,
jlong nativeProxy) {
if (!nativeProxy) {
return;
}
SkottieAnimation* skottieAnimation = reinterpret_cast<SkottieAnimation*>(nativeProxy);
delete skottieAnimation;
}
extern "C" JNIEXPORT void
JNICALL
Java_org_skia_skottie_SkottieRunner_00024SkottieAnimation_nDrawFrame(JNIEnv *env, jclass clazz,
jlong nativeProxy, jint width,
jint height,
jboolean wideColorGamut,
jlong frameTimeNanos) {
ATRACE_NAME("SkottieDrawFrame");
if (!nativeProxy) {
return;
}
SkottieAnimation* skottieAnimation = reinterpret_cast<SkottieAnimation*>(nativeProxy);
auto grContext = skottieAnimation->mRunner->mGrContext;
if (!grContext) {
return;
}
SkColorType colorType;
// setup surface for fbo0
GrGLFramebufferInfo fboInfo;
fboInfo.fFBOID = 0;
if (wideColorGamut) {
fboInfo.fFormat = GL_RGBA16F;
colorType = kRGBA_F16_SkColorType;
} else {
fboInfo.fFormat = GL_RGBA8;
colorType = kN32_SkColorType;
}
GrBackendRenderTarget backendRT(width, height, 0, STENCIL_BUFFER_SIZE, fboInfo);
SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
sk_sp<SkSurface> renderTarget(SkSurface::MakeFromBackendRenderTarget(
grContext.get(), backendRT, kBottomLeft_GrSurfaceOrigin, colorType,
nullptr, &props));
auto canvas = renderTarget->getCanvas();
canvas->clear(SK_ColorTRANSPARENT);
if (skottieAnimation->mAnimation) {
SkMSec t = 0;
if (skottieAnimation->mTimeBase == 0.0f) {
// Reset the animation time.
skottieAnimation->mTimeBase = frameTimeNanos;
} else {
//convert from nanoseconds to milliseconds
t = (frameTimeNanos - skottieAnimation->mTimeBase) / 1000000;
}
//TODO: control repeat count
float intpart;
float animState = modff(t / skottieAnimation->mDuration, &intpart);
skottieAnimation->mAnimation->seek(animState);
SkAutoCanvasRestore acr(canvas, true);
SkRect bounds = SkRect::MakeWH(width, height);
skottieAnimation->mAnimation->render(canvas, &bounds);
}
canvas->flush();
}

View File

@ -0,0 +1,270 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
package org.skia.skottie;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.GridLayout;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static java.lang.Math.ceil;
import static java.lang.Math.sqrt;
public class SkottieActivity extends Activity implements View.OnClickListener {
private final static long TIME_OUT_MS = 10000;
private SkottieApplication mApplication;
private CountDownLatch mEnterAnimationFence = new CountDownLatch(1);
private GridLayout mGrid;
private int mRowCount = 0;
private int mColumnCount = 0;
private int mCellWidth = 0;
private int mCellHeight = 0;
private List<SkottieView> mAnimations;
static private List<Uri> mAnimationFiles = new ArrayList<Uri>();
private void populateGrid() {
mRowCount = 0;
mColumnCount = 0;
mAnimations = new ArrayList<SkottieView>();
mCellWidth = 0;
mCellHeight = 0;
int rawAssets[] = {
R.raw.star, R.raw.movie_loading, R.raw.uk, R.raw.white_material_wave_loading
};
for (int resId : rawAssets) {
SkottieView view = new SkottieView(this, getResources().openRawResource(resId));
mAnimations.add(view);
}
for (Uri uri : mAnimationFiles) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
SkottieView view = new SkottieView(this, inputStream);
mAnimations.add(view);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
int screenWidth = size.x;
int screenHeight = (int)(size.y / 1.3f);
double unit = sqrt(mAnimations.size() / 6.0f);
mRowCount = (int)ceil(3 * unit);
mColumnCount = (int)ceil(2 * unit);
mGrid.setColumnCount(mColumnCount);
mGrid.setRowCount(mRowCount);
mCellWidth = screenWidth / mColumnCount;
mCellHeight = screenHeight / mRowCount;
refreshGrid();
startAnimation();
for (SkottieView view : mAnimations) {
view.setOnClickListener(new View.OnClickListener(){
public void onClick(View view){
inflateView((SkottieView)view);
}
});
}
if (mInflatedIndex >= 0) {
SkottieView view = mAnimations.get(mInflatedIndex);
mInflatedIndex = -1;
inflateView(view);
}
}
static int mInflatedIndex = -1;
private void inflateView(SkottieView view) {
if (mInflatedIndex >= 0) {
//deflate active view
SkottieView oldView = mAnimations.get(mInflatedIndex);
if (oldView != null) {
int row = mInflatedIndex / mColumnCount, column = mInflatedIndex % mColumnCount;
addView(oldView, row, column, false);
}
mInflatedIndex = -1;
//start and show animations that were in the background
for (SkottieView anyView : mAnimations) {
if (anyView != oldView) {
anyView.start();
anyView.setVisibility(View.VISIBLE);
}
}
return;
}
//stop and hide animations in the background
for (SkottieView anyView : mAnimations) {
if (anyView != view) {
anyView.stop();
anyView.setVisibility(View.INVISIBLE);
}
}
mInflatedIndex = mAnimations.indexOf(view);
GridLayout.Spec rowSpec = GridLayout.spec(0, mRowCount, GridLayout.CENTER);
GridLayout.Spec colSpec = GridLayout.spec(0, mColumnCount, GridLayout.CENTER);
GridLayout.LayoutParams params = new GridLayout.LayoutParams(rowSpec, colSpec);
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
mGrid.updateViewLayout(view, params);
}
private void refreshGrid() {
mGrid.removeAllViews();
int currentRaw = 0;
int row = 0, column = 0;
for (SkottieView view : mAnimations) {
addView(view, row, column, true);
column++;
if (column >= mColumnCount) {
column = 0;
row++;
}
}
}
private void addView(SkottieView view, int row , int column, boolean addView) {
GridLayout.Spec rowSpec = GridLayout.spec(row, 1, GridLayout.CENTER);
GridLayout.Spec colSpec = GridLayout.spec(column, 1, GridLayout.CENTER);
GridLayout.LayoutParams params = new GridLayout.LayoutParams(rowSpec, colSpec);
params.width = mCellWidth;
params.height = mCellHeight;
if (addView) {
mGrid.addView(view, params);
} else {
mGrid.updateViewLayout(view, params);
}
}
private void startAnimation() {
for (SkottieView view : mAnimations) {
view.start();
}
}
private void stopAnimation() {
for (SkottieView view : mAnimations) {
view.stop();
}
}
private void addLottie(Uri uri) throws FileNotFoundException {
InputStream inputStream = getContentResolver().openInputStream(uri);
int animations = mAnimations.size();
if (animations < mRowCount * mColumnCount) {
SkottieView view = new SkottieView(this, inputStream);
int row = animations / mColumnCount, column = animations % mColumnCount;
mAnimations.add(view);
mAnimationFiles.add(uri);
view.setOnClickListener(new View.OnClickListener(){
public void onClick(View view){
inflateView((SkottieView)view);
}
});
addView(view, row, column, true);
view.start();
} else {
stopAnimation();
mAnimationFiles.add(uri);
populateGrid();
startAnimation();
}
}
@Override
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();
mEnterAnimationFence.countDown();
}
public void waitForEnterAnimationComplete() throws TimeoutException, InterruptedException {
if (!mEnterAnimationFence.await(TIME_OUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
}
private void createLayout() {
setContentView(R.layout.main_layout);
Button button1 = (Button)findViewById(R.id.open_lottie);
button1.setOnClickListener(this);
mGrid = (GridLayout)findViewById(R.id.grid_lotties);
mGrid.setBackgroundColor(Color.LTGRAY);
populateGrid();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
createLayout();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
static final int PICK_FILE_REQUEST = 2;
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setType("application/json");
Intent i = Intent.createChooser(intent, "View Default File Manager");
startActivityForResult(i, PICK_FILE_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_FILE_REQUEST) if (data != null) {
//no data present
Uri uri = data.getData();
try {
addLottie(uri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
package org.skia.skottie;
import android.app.Application;
public class SkottieApplication extends Application {
static {
System.loadLibrary("skottie_android");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onTerminate() {
super.onTerminate();
}
}

View File

@ -0,0 +1,461 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
package org.skia.skottie;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.Animatable;
import android.opengl.GLUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Choreographer;
import android.view.TextureView;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
public class SkottieRunner {
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
private static final int EGL_OPENGL_ES2_BIT = 4;
private static final int STENCIL_BUFFER_SIZE = 8;
private static final long TIME_OUT_MS = 10000;
private static final String LOG_TAG = "SkottiePlayer";
private static SkottieRunner sInstance;
private HandlerThread mGLThreadLooper;
private Handler mGLThread;
private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLConfig mEglConfig;
private EGLContext mEglContext;
private EGLSurface mPBufferSurface;
private long mNativeProxy;
/**
* Gets SkottieRunner singleton instance.
*/
public static synchronized SkottieRunner getInstance() {
if (sInstance == null) {
sInstance = new SkottieRunner();
}
return sInstance;
}
/**
* Create a new animation by feeding data from "is" and replaying in a TextureView.
* TextureView is tracked internally for SurfaceTexture state.
*/
public Animatable createAnimation(TextureView view, InputStream is) {
return new SkottieAnimation(view, is);
}
/**
* Create a new animation by feeding data from "is" and replaying in a SurfaceTexture.
* SurfaceTexture is possibly taken from a TextureView and can be updated with
* updateAnimationSurface.
*/
public Animatable createAnimation(SurfaceTexture surfaceTexture, InputStream is) {
return new SkottieAnimation(surfaceTexture, is);
}
/**
* Pass a new SurfaceTexture: use this method only if managing TextureView outside
* SkottieRunner.
*/
public void updateAnimationSurface(Animatable animation, SurfaceTexture surfaceTexture,
int width, int height) {
((SkottieAnimation) animation).updateSurface(surfaceTexture, width, height);
}
private SkottieRunner()
{
mGLThreadLooper = new HandlerThread("SkottieAnimator");
mGLThreadLooper.start();
mGLThread = new Handler(mGLThreadLooper.getLooper());
initGl();
}
@Override
protected void finalize() throws Throwable {
try {
runOnGLThread(this::doFinishGL);
} finally {
super.finalize();
}
}
private long getNativeProxy() { return mNativeProxy; }
private class RunSignalAndCatch implements Runnable {
public Throwable error;
private Runnable mRunnable;
private CountDownLatch mFence;
RunSignalAndCatch(Runnable run, CountDownLatch fence) {
mRunnable = run;
mFence = fence;
}
@Override
public void run() {
try {
mRunnable.run();
} catch (Throwable t) {
error = t;
} finally {
mFence.countDown();
}
}
}
private void runOnGLThread(Runnable r) throws Throwable {
runOnGLThread(r, false);
}
private void runOnGLThread(Runnable r, boolean postAtFront) throws Throwable {
CountDownLatch fence = new CountDownLatch(1);
RunSignalAndCatch wrapper = new RunSignalAndCatch(r, fence);
if (postAtFront) {
mGLThread.postAtFrontOfQueue(wrapper);
} else {
mGLThread.post(wrapper);
}
if (!fence.await(TIME_OUT_MS, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
if (wrapper.error != null) {
throw wrapper.error;
}
}
private void initGl()
{
try {
runOnGLThread(mDoInitGL);
}
catch (Throwable t) {
Log.e(LOG_TAG, "initGl failed", t);
throw new RuntimeException(t);
}
}
private Runnable mDoInitGL = new Runnable() {
@Override
public void run() {
mEgl = (EGL10) EGLContext.getEGL();
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed "
+ GLUtils.getEGLErrorString(mEgl.eglGetError()));
}
int[] version = new int[2];
if (!mEgl.eglInitialize(mEglDisplay, version)) {
throw new RuntimeException("eglInitialize failed " +
GLUtils.getEGLErrorString(mEgl.eglGetError()));
}
mEglConfig = chooseEglConfig();
if (mEglConfig == null) {
throw new RuntimeException("eglConfig not initialized");
}
mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
int[] attribs = new int[] {
EGL10.EGL_WIDTH, 1,
EGL10.EGL_HEIGHT, 1,
EGL10.EGL_NONE
};
mPBufferSurface = mEgl.eglCreatePbufferSurface(mEglDisplay, mEglConfig, attribs);
if (mPBufferSurface == null || mPBufferSurface == EGL10.EGL_NO_SURFACE) {
int error = mEgl.eglGetError();
throw new RuntimeException("createPbufferSurface failed "
+ GLUtils.getEGLErrorString(error));
}
if (!mEgl.eglMakeCurrent(mEglDisplay, mPBufferSurface, mPBufferSurface, mEglContext)) {
throw new RuntimeException("eglMakeCurrent failed "
+ GLUtils.getEGLErrorString(mEgl.eglGetError()));
}
mNativeProxy = nCreateProxy();
}
};
EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
}
private EGLConfig chooseEglConfig() {
int[] configsCount = new int[1];
EGLConfig[] configs = new EGLConfig[1];
int[] configSpec = getConfig();
if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
throw new IllegalArgumentException("eglChooseConfig failed " +
GLUtils.getEGLErrorString(mEgl.eglGetError()));
} else if (configsCount[0] > 0) {
return configs[0];
}
return null;
}
private int[] getConfig() {
return new int[] {
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 0,
EGL10.EGL_STENCIL_SIZE, STENCIL_BUFFER_SIZE,
EGL10.EGL_NONE
};
}
private void doFinishGL() {
nDeleteProxy(mNativeProxy);
mNativeProxy = 0;
if (mEglDisplay != null) {
if (mEglContext != null) {
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
mEglContext = null;
}
if (mPBufferSurface != null) {
mEgl.eglDestroySurface(mEglDisplay, mPBufferSurface);
mPBufferSurface = null;
}
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_CONTEXT);
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
}
}
private class SkottieAnimation implements Animatable, Choreographer.FrameCallback,
TextureView.SurfaceTextureListener {
boolean mIsRunning = false;
SurfaceTexture mSurfaceTexture;
EGLSurface mEglSurface;
boolean mNewSurface = false;
private int mSurfaceWidth = 0;
private int mSurfaceHeight = 0;
private long mNativeProxy;
private InputStream mInputStream;
private byte[] mTempStorage;
SkottieAnimation(SurfaceTexture surfaceTexture, InputStream is) {
init(surfaceTexture, is);
}
SkottieAnimation(TextureView view, InputStream is) {
init(view.getSurfaceTexture(), is);
view.setSurfaceTextureListener(this);
}
private void init(SurfaceTexture surfaceTexture, InputStream is) {
mTempStorage = new byte[16 * 1024];
mInputStream = is;
long proxy = SkottieRunner.getInstance().getNativeProxy();
mNativeProxy = nCreateProxy(proxy, mInputStream, mTempStorage);
mSurfaceTexture = surfaceTexture;
}
@Override
protected void finalize() throws Throwable {
try {
stop();
nDeleteProxy(mNativeProxy);
mNativeProxy = 0;
} finally {
super.finalize();
}
}
public void updateSurface(SurfaceTexture surfaceTexture, int width, int height) {
try {
runOnGLThread(() -> {
mSurfaceTexture = surfaceTexture;
mSurfaceWidth = width;
mSurfaceHeight = height;
mNewSurface = true;
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "updateSurface failed", t);
throw new RuntimeException(t);
}
}
@Override
public void start() {
try {
runOnGLThread(() -> {
if (!mIsRunning) {
mIsRunning = true;
mNewSurface = true;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "start failed", t);
throw new RuntimeException(t);
}
}
@Override
public void stop() {
try {
runOnGLThread(() -> {
mIsRunning = false;
if (mEglSurface != null) {
// Ensure we always have a valid surface & context.
mEgl.eglMakeCurrent(mEglDisplay, mPBufferSurface, mPBufferSurface,
mEglContext);
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
}
});
}
catch (Throwable t) {
Log.e(LOG_TAG, "stop failed", t);
throw new RuntimeException(t);
}
}
@Override
public boolean isRunning() {
return mIsRunning;
}
@Override
public void doFrame(long frameTimeNanos) {
try {
if (mIsRunning) {
// Schedule next frame.
Choreographer.getInstance().postFrameCallback(this);
} else {
// If animation stopped, release EGL surface.
if (mEglSurface != null) {
// Ensure we always have a valid surface & context.
mEgl.eglMakeCurrent(mEglDisplay, mPBufferSurface, mPBufferSurface,
mEglContext);
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
}
return;
}
if (mNewSurface) {
// if there is a new SurfaceTexture, we need to recreate the EGL surface.
if (mEglSurface != null) {
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
}
mNewSurface = false;
}
if (mEglSurface == null) {
if (mSurfaceTexture != null) {
mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig,
mSurfaceTexture, null);
if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
// If failed to create a surface, log an error and stop the animation
int error = mEgl.eglGetError();
throw new RuntimeException("createWindowSurface failed "
+ GLUtils.getEGLErrorString(error));
}
}
}
if (mEglSurface != null) {
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
// If eglMakeCurrent failed, recreate EGL surface on next frame.
Log.w(LOG_TAG, "eglMakeCurrent failed "
+ GLUtils.getEGLErrorString(mEgl.eglGetError()));
mNewSurface = true;
return;
}
nDrawFrame(mNativeProxy, mSurfaceWidth, mSurfaceHeight, false,
frameTimeNanos);
if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
int error = 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));
}
}
} catch (Throwable t) {
Log.e(LOG_TAG, "doFrame failed", t);
mIsRunning = false;
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// will be called on UI thread
updateSurface(surface, width, height);
}
@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);
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
private native long nCreateProxy(long runner, InputStream is, byte[] storage);
private native void nDeleteProxy(long nativeProxy);
private native void nDrawFrame(long nativeProxy, int width, int height,
boolean wideColorGamut, long frameTimeNanos);
}
private static native long nCreateProxy();
private static native void nDeleteProxy(long nativeProxy);
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
package org.skia.skottie;
import android.content.Context;
import android.graphics.drawable.Animatable;
import android.view.TextureView;
import android.view.ViewGroup;
import java.io.InputStream;
public class SkottieView extends ViewGroup implements Animatable {
private TextureView mTextureView;
private Animatable mAnimation;
public SkottieView(Context context, InputStream is) {
super(context);
mTextureView = new TextureView(context);
mTextureView.setOpaque(false);
mAnimation = SkottieRunner.getInstance().createAnimation(mTextureView, is);
addView(mTextureView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
/**
* Starts the animation.
*/
@Override
public void start() {
mAnimation.start();
}
/**
* Stops the animation.
*/
@Override
public void stop() {
mAnimation.stop();
}
@Override
public boolean isRunning() {
return mAnimation.isRunning();
}
/**
* Ask all children to measure themselves and compute the measurement of this
* layout based on the children.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mTextureView.measure(widthMeasureSpec, heightMeasureSpec);
int width = mTextureView.getMeasuredWidth();
int height = mTextureView.getMeasuredHeight();
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) { // This is a new size or position for this view
mTextureView.layout(0, 0, right - left, bottom - top);
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/open_lottie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Open lottie file"/>
<GridLayout
android:id="@+id/grid_lotties"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_gravity="center"
android:columnCount="1"
>
</GridLayout>
</LinearLayout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long