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:
parent
3526cfaf05
commit
93151726ed
17
BUILD.gn
17
BUILD.gn
@ -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 = [
|
||||
|
@ -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'
|
35
platform_tools/android/apps/skottie/build.gradle
Normal file
35
platform_tools/android/apps/skottie/build.gradle
Normal 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")
|
||||
}
|
@ -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) -->
|
@ -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;
|
||||
}
|
@ -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);
|
211
platform_tools/android/apps/skottie/src/main/cpp/native-lib.cpp
Normal file
211
platform_tools/android/apps/skottie/src/main/cpp/native-lib.cpp
Normal 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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user