[androidkit] Add SurfaceRenderer utility

Most samples use the same render thread boilerplate -- consolidate as
a utility base class.

Also add Canvas.getWidth/getHeight.

Change-Id: I27c9f51b4fd9d228a39593fbd4650a66d10240c2
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/409896
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Jorge Betancourt <jmbetancourt@google.com>
diff --git a/modules/androidkit/src/Canvas.cpp b/modules/androidkit/src/Canvas.cpp
index 8552f2e..038437e 100644
--- a/modules/androidkit/src/Canvas.cpp
+++ b/modules/androidkit/src/Canvas.cpp
@@ -13,6 +13,16 @@
 
 namespace {
 
+jint Canvas_GetWidth(JNIEnv* env, jobject, jlong native_instance) {
+    const auto* canvas = reinterpret_cast<const SkCanvas*>(native_instance);
+    return canvas ? canvas->imageInfo().width() : 0;
+}
+
+jint Canvas_GetHeight(JNIEnv* env, jobject, jlong native_instance) {
+    const auto* canvas = reinterpret_cast<const SkCanvas*>(native_instance);
+    return canvas ? canvas->imageInfo().height() : 0;
+}
+
 void Canvas_Save(JNIEnv* env, jobject, jlong native_instance) {
     if (auto* canvas = reinterpret_cast<SkCanvas*>(native_instance)) {
         canvas->save();
@@ -73,6 +83,8 @@
 
 int register_androidkit_Canvas(JNIEnv* env) {
     static const JNINativeMethod methods[] = {
+        {"nGetWidth"        , "(J)I"     , reinterpret_cast<void*>(Canvas_GetWidth)      },
+        {"nGetHeight"       , "(J)I"     , reinterpret_cast<void*>(Canvas_GetHeight)     },
         {"nSave"            , "(J)V"     , reinterpret_cast<void*>(Canvas_Save)          },
         {"nRestore"         , "(J)V"     , reinterpret_cast<void*>(Canvas_Restore)       },
         {"nGetLocalToDevice", "(J)J"     , reinterpret_cast<void*>(Canvas_LocalToDevice) },
diff --git a/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/Canvas.java b/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/Canvas.java
index febf4be..f804126 100644
--- a/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/Canvas.java
+++ b/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/Canvas.java
@@ -16,6 +16,14 @@
     private long mNativeInstance;
     private Surface mSurface;
 
+    public int getWidth() {
+        return nGetWidth(mNativeInstance);
+    }
+
+    public int getHeight() {
+        return nGetHeight(mNativeInstance);
+    }
+
     public void save() {
         nSave(mNativeInstance);
     }
@@ -71,6 +79,8 @@
     // package private
     long getNativeInstance() { return mNativeInstance; }
 
+    private static native int  nGetWidth(long nativeInstance);
+    private static native int  nGetHeight(long nativeInstance);
     private static native void nSave(long nativeInstance);
     private static native void nRestore(long nativeInstance);
     private static native long nGetLocalToDevice(long mNativeInstance);
diff --git a/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/util/SurfaceRenderer.java b/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/util/SurfaceRenderer.java
new file mode 100644
index 0000000..343d694
--- /dev/null
+++ b/platform_tools/android/apps/AndroidKit/src/main/java/org/skia/androidkit/util/SurfaceRenderer.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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.androidkit.util;
+
+import android.util.Log;
+import android.view.SurfaceHolder;
+import org.skia.androidkit.Canvas;
+import org.skia.androidkit.Surface;
+
+/**
+  * Utility base class facilitating the implementation of Surface-bound animations.
+  *
+  * Provides a dedicated render thread and user content callbacks.
+  */
+public abstract class SurfaceRenderer implements SurfaceHolder.Callback, Runnable {
+    private android.view.Surface mAndroidSurface;
+    private Thread               mRenderThread;
+    private boolean              mThreadRunning;
+
+    /**
+      * Initialization callback.
+      *
+      * This can be invoked multiple times if the underlying surface changes.
+      */
+    protected abstract void onSurfaceInitialized(Surface surface);
+
+    /**
+      * Callback for frame content.
+      *
+      * Invoked once per (vsync'ed) frame.
+      */
+    protected abstract void onRenderFrame(Canvas canvas, long ms);
+
+    @Override
+    public void run() {
+        Log.d("SurfaceRenderer", "Render thread started.");
+
+        // TODO: Vulkan support?
+        Surface surface = Surface.CreateGL(mAndroidSurface);
+        onSurfaceInitialized(surface);
+
+        long time_base = java.lang.System.currentTimeMillis();
+
+        while (mThreadRunning) {
+            long timestamp = java.lang.System.currentTimeMillis() - time_base;
+            onRenderFrame(surface.getCanvas(), timestamp);
+            surface.flushAndSubmit();
+        }
+
+        Log.d("SurfaceRenderer", "Render thread finished.");
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        // Initialization handled in surfaceChanged().
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        mAndroidSurface = holder.getSurface();
+
+        stopRenderThread();
+        startRenderThread();
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        stopRenderThread();
+    }
+
+    private void startRenderThread() {
+        if (!mThreadRunning) {
+            mThreadRunning = true;
+            mRenderThread = new Thread(this);
+            mRenderThread.start();
+        }
+    }
+
+    private void stopRenderThread() {
+        if (mThreadRunning) {
+            mThreadRunning = false;
+            try {
+                mRenderThread.join();
+            } catch (InterruptedException e) {}
+        }
+    }
+}
diff --git a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/AnimationActivity.java b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/AnimationActivity.java
index 3a89840..31c62be 100644
--- a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/AnimationActivity.java
+++ b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/AnimationActivity.java
@@ -9,61 +9,17 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import org.skia.androidkit.*;
+import org.skia.androidkit.util.*;
 
-class RenderThread extends Thread {
-    private android.view.Surface mAndroidSurface;
-    private Surface              mSurface;
-    private boolean              mRunning;
-
-    private static final String TAG = "*** AK RenderThread";
-
-    public RenderThread(android.view.Surface surface) {
-        mAndroidSurface = surface;
-    }
-
-    public void finish() {
-        mRunning = false;
-    }
+class AnimationRenderer extends SurfaceRenderer {
+    @Override
+    protected void onSurfaceInitialized(Surface surface) {}
 
     @Override
-    public void run() {
-        mRunning = true;
-
-        Log.d(TAG, "start");
-
-        long time_base = java.lang.System.currentTimeMillis();
-
-        // TODO: convert to native AK surface.
-        while (mRunning) {
-            android.graphics.Canvas android_canvas = mAndroidSurface.lockHardwareCanvas();
-
-            int w = android_canvas.getWidth(),
-                h = android_canvas.getHeight();
-
-            android.graphics.Bitmap bm =
-                    android.graphics.Bitmap.createBitmap(w, h,
-                                                         android.graphics.Bitmap.Config.ARGB_8888,
-                                                         true);
-            Surface surface = new Surface(bm);
-            renderFrame(surface.getCanvas(),
-                        (double)(java.lang.System.currentTimeMillis() - time_base) / 1000,
-                        w, h);
-            surface.flushAndSubmit();
-            surface.release();
-
-            android_canvas.drawBitmap(bm, 0, 0, new android.graphics.Paint());
-
-            mAndroidSurface.unlockCanvasAndPost(android_canvas);
-        }
-
-        Log.d(TAG, "finish");
-    }
-
-    private void renderFrame(Canvas canvas, double t, int canvas_width, int canvas_height) {
+    protected void onRenderFrame(Canvas canvas, long ms) {
         final float kWidth  = 400,
                     kHeight = 200,
                     kSpeed  = 4;
@@ -73,52 +29,23 @@
         Paint p = new Paint();
         p.setColor(new Color(0, 1, 0, 1));
 
-        float x = (float)(java.lang.Math.cos(t * kSpeed) + 1) * canvas_width/2;
-        canvas.drawRect(x - kWidth/2, (canvas_height - kHeight)/2,
-                        x + kWidth/2, (canvas_height + kHeight)/2, p);
+        float x = (float)(java.lang.Math.cos(ms * kSpeed / 1000) + 1) * canvas.getWidth()/2;
+        canvas.drawRect(x - kWidth/2, (canvas.getHeight() - kHeight)/2,
+                        x + kWidth/2, (canvas.getHeight() + kHeight)/2, p);
     }
 }
 
-public class AnimationActivity extends Activity implements SurfaceHolder.Callback {
+public class AnimationActivity extends Activity {
     static {
         System.loadLibrary("androidkit");
     }
 
-    private RenderThread mRenderThread;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_animation);
 
         SurfaceView sv = findViewById(R.id.surfaceView);
-        sv.getHolder().addCallback(this);
-    }
-
-    @Override
-    public void surfaceCreated(SurfaceHolder holder) {
-    }
-
-    @Override
-    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
-
-        mRenderThread = new RenderThread(holder.getSurface());;
-        mRenderThread.start();
-    }
-
-    @Override
-    public void surfaceDestroyed(SurfaceHolder holder) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
+        sv.getHolder().addCallback(new AnimationRenderer());
     }
 }
diff --git a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/CubeActivity.java b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/CubeActivity.java
index 03f78a5..953afac 100644
--- a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/CubeActivity.java
+++ b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/CubeActivity.java
@@ -18,6 +18,7 @@
 import org.skia.androidkit.Matrix;
 import org.skia.androidkit.Paint;
 import org.skia.androidkit.Surface;
+import org.skia.androidkit.util.SurfaceRenderer;
 
 import static java.lang.Math.tan;
 
@@ -37,9 +38,7 @@
     }
 }
 
-class CubeRenderThread extends Thread {
-    private android.view.Surface mASurface;
-    private boolean mRunning;
+class CubeRenderer extends SurfaceRenderer {
     // TODO: make these relative to surface size
     private float mCubeSideLength = 500;
     private int DX = 200;
@@ -52,7 +51,7 @@
     private Matrix perspective = Matrix.makePerspective(0.05f, 4, fAngle);
     private Matrix viewport;
 
-    private Paint mPaint;
+    private Paint mPaint = new Paint();
 
     private final float rot = (float) Math.PI;
     private Face[] faces = {new Face(0, 0, new Color(1, 0, 0, 1)),
@@ -62,54 +61,39 @@
                             new Face(0, rot/2, new Color(0, 1, 1, 1)),
                             new Face(0, -rot/2, new Color(0, 0, 0, 1))};
 
-    private static final String TAG = "*** AK CubeRenderThread";
-
-    public CubeRenderThread(android.view.Surface surface) {
-        mASurface = surface;
-        mPaint = new Paint();
+    public CubeRenderer() {
         mPaint.setColor(new Color(0, 1, 1, 1));
         mPaint.setStroke(false);
         mPaint.setStrokeWidth(10);
-
-    }
-
-    public void finish(){
-        mRunning = false;
     }
 
     @Override
-    public void run() {
-        mRunning = true;
-        long time_base = java.lang.System.currentTimeMillis();
-
-        Surface surface = Surface.CreateGL(mASurface);
+    protected void onSurfaceInitialized(Surface surface) {
         viewport = new Matrix().translate(mCubeSideLength/2, mCubeSideLength/2, 0)
                                .scale(mCubeSideLength/2, mCubeSideLength/2, surface.getWidth());
-        while (mRunning) {
-            float t = (float)(java.lang.System.currentTimeMillis() - time_base) / 1000;
-            float speed = 0.5f;
-            float rads = t * speed % (float)(2 * Math.PI);
-            renderFrame(surface, rads);
-            surface.flushAndSubmit();
-        }
-
-        surface.release();
     }
 
-    private void renderFrame(Surface surface, float rads) {
-        Canvas canvas = surface.getCanvas();
+    @Override
+    protected void onRenderFrame(Canvas canvas, long ms) {
+        float speed = 0.5f;
+        float rads = ms / 1000.f * speed % (float)(2 * Math.PI);
+
         // clear canvas
         canvas.drawColor(0xffffffff);
 
         canvas.save();
         canvas.concat(new Matrix().translate(DX, DY, 0));
-        canvas.concat(viewport.preConcat(perspective).preConcat(cam).preConcat(Matrix.makeInverse(viewport)));
+        canvas.concat(viewport.preConcat(perspective)
+                              .preConcat(cam)
+                              .preConcat(Matrix.makeInverse(viewport)));
 
         for (Face f : faces) {
             //TODO: auto restore
             canvas.save();
             Matrix trans = new Matrix().translate(mCubeSideLength/2, mCubeSideLength/2, 0);
-            Matrix m = new Matrix().rotateY(rads).rotateZ(rads).preConcat(f.asMatrix(mCubeSideLength/2));
+            Matrix m = new Matrix().rotateY(rads)
+                                   .rotateZ(rads)
+                                   .preConcat(f.asMatrix(mCubeSideLength/2));
 
             canvas.concat(trans);
             Matrix localToWorld = m.preConcat(Matrix.makeInverse(trans));
@@ -134,8 +118,8 @@
         return m2.getAtRowCol(2, 2) > 0;
     }
 }
-public class CubeActivity extends Activity implements SurfaceHolder.Callback {
-    private CubeRenderThread mRenderThread;
+
+public class CubeActivity extends Activity {
     static {
         System.loadLibrary("androidkit");
     }
@@ -146,34 +130,6 @@
         setContentView(R.layout.activity_cube);
 
         SurfaceView sv = findViewById(R.id.surfaceView);
-        sv.getHolder().addCallback(this);
-    }
-
-    @Override
-    public void surfaceCreated(@NonNull SurfaceHolder holder) {
-
-    }
-
-    @Override
-    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
-
-        mRenderThread = new CubeRenderThread(holder.getSurface());;
-        mRenderThread.start();
-    }
-
-    @Override
-    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
+        sv.getHolder().addCallback(new CubeRenderer());
     }
 }
diff --git a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/RuntimeShaderActivity.java b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/RuntimeShaderActivity.java
index 44eb579..0aa6568 100644
--- a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/RuntimeShaderActivity.java
+++ b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/RuntimeShaderActivity.java
@@ -9,19 +9,14 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import org.skia.androidkit.*;
+import org.skia.androidkit.util.*;
 
-// TODO: refactor to share w/ other activities
-class RuntimeShaderRenderThread extends Thread {
-    private android.view.Surface mAndroidSurface;
-    private Surface              mSurface;
-    private RuntimeShaderBuilder mBuilder;
-    private boolean              mRunning;
+class RuntimeShaderRenderer extends SurfaceRenderer {
+    private RuntimeShaderBuilder mBuilder = new RuntimeShaderBuilder(SkSLShader);
 
-    private static final String TAG = "*** AK RenderThread";
     private static final String SkSLShader =
         "uniform half u_time;                                  " +
         "uniform half u_w;                                     " +
@@ -48,106 +43,50 @@
 
         "half4 main(vec2 fragcoord) {                     " +
         "   vec3 c;" +
-	    "   float l;" +
+        "   float l;" +
         "   float z=u_time;" +
-	    "   for(int i=0;i<3;i++) {" +
-		"       vec2 p=fragcoord.xy/vec2(u_w,u_h);" +
-		"       vec2 uv=p;" +
-		"       p-=.5;" +
-		"       p.x*=u_w/u_h;" +
-		"       z+=.07;" +
-		"       l=length(p);" +
-		"       uv+=p/l*(sin(z)+1.)*abs(sin(l*9.-z*2.));" +
-		"       c[i]=.01/length(abs(mod(uv,1.)-.5));" +
-	    "   }" +
-	    "   return half4(c/l,u_time);" +
+        "   for(int i=0;i<3;i++) {" +
+        "       vec2 p=fragcoord.xy/vec2(u_w,u_h);" +
+        "       vec2 uv=p;" +
+        "       p-=.5;" +
+        "       p.x*=u_w/u_h;" +
+        "       z+=.07;" +
+        "       l=length(p);" +
+        "       uv+=p/l*(sin(z)+1.)*abs(sin(l*9.-z*2.));" +
+        "       c[i]=.01/length(abs(mod(uv,1.)-.5));" +
+        "   }" +
+        "   return half4(c/l,u_time);" +
         "}";
 
-    public RuntimeShaderRenderThread(android.view.Surface surface) {
-        mAndroidSurface = surface;
-        mBuilder = new RuntimeShaderBuilder(SkSLShader);
-    }
-
-    public void finish() {
-        mRunning = false;
-    }
+    @Override
+    protected void onSurfaceInitialized(Surface surface) {}
 
     @Override
-    public void run() {
-        mRunning = true;
-
-        Log.d(TAG, "start");
-
-        long time_base = java.lang.System.currentTimeMillis();
-
-        Surface surface = Surface.CreateGL(mAndroidSurface);
-
-        while (mRunning) {
-            renderFrame(surface.getCanvas(),
-                        (double)(java.lang.System.currentTimeMillis() - time_base) / 1000,
-                        surface.getWidth(), surface.getHeight());
-            surface.flushAndSubmit();
-        }
-
-        surface.release();
-        Log.d(TAG, "finish");
-    }
-
-    private void renderFrame(Canvas canvas, double t, int canvas_width, int canvas_height) {
-        final float kWidth  = 1000,
-                    kHeight = 1000,
-                    kSpeed  = 40;
+    protected void onRenderFrame(Canvas canvas, long ms) {
+        final int w = canvas.getWidth();
+        final int h = canvas.getHeight();
 
         Paint p = new Paint();
-        p.setShader(mBuilder.setUniform("u_time", (float)t)
-                            .setUniform("u_w", canvas_width)
-                            .setUniform("u_h", canvas_height)
+        p.setShader(mBuilder.setUniform("u_time", ms/1000.0f)
+                            .setUniform("u_w", w)
+                            .setUniform("u_h", h)
                             .makeShader());
 
-        canvas.drawRect(0, 0, canvas_width, canvas_height, p);
+        canvas.drawRect(0, 0, w, h, p);
     }
 }
 
-public class RuntimeShaderActivity extends Activity implements SurfaceHolder.Callback {
+public class RuntimeShaderActivity extends Activity {
     static {
         System.loadLibrary("androidkit");
     }
 
-    private RuntimeShaderRenderThread mRenderThread;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_animation);
 
         SurfaceView sv = findViewById(R.id.surfaceView);
-        sv.getHolder().addCallback(this);
-    }
-
-    @Override
-    public void surfaceCreated(SurfaceHolder holder) {
-    }
-
-    @Override
-    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
-
-        mRenderThread = new RuntimeShaderRenderThread(holder.getSurface());;
-        mRenderThread.start();
-    }
-
-    @Override
-    public void surfaceDestroyed(SurfaceHolder holder) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
+        sv.getHolder().addCallback(new RuntimeShaderRenderer());
     }
 }
diff --git a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/SkottieAnimationActivity.java b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/SkottieAnimationActivity.java
index 501c271..0491d04 100644
--- a/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/SkottieAnimationActivity.java
+++ b/platform_tools/android/apps/androidkitdemo/src/main/java/org/skia/androidkitdemo1/SkottieAnimationActivity.java
@@ -14,36 +14,18 @@
 import android.view.SurfaceView;
 import java.io.InputStream;
 import org.skia.androidkit.*;
+import org.skia.androidkit.util.*;
 
-// TODO: refactor to share w/ other activities
-class SkottieAnimationRenderThread extends Thread {
-    private android.view.Surface mAndroidSurface;
-    private SkottieAnimation     mAnimation;
-    private Matrix               mAnimationMatrix;
-    private Surface              mSurface;
-    private boolean              mRunning;
+class SkottieAnimationRenderer extends SurfaceRenderer {
+    private SkottieAnimation mAnimation;
+    private Matrix           mAnimationMatrix;
 
-    private static final String TAG = "*** AK RenderThread";
-
-    public SkottieAnimationRenderThread(android.view.Surface surface, SkottieAnimation animation) {
-        mAndroidSurface = surface;
+    SkottieAnimationRenderer(SkottieAnimation animation) {
         mAnimation = animation;
     }
 
-    public void finish() {
-        mRunning = false;
-    }
-
     @Override
-    public void run() {
-        mRunning = true;
-
-        Log.d(TAG, "start");
-
-        long time_base = java.lang.System.currentTimeMillis();
-
-        Surface surface = Surface.CreateGL(mAndroidSurface);
-
+    protected void onSurfaceInitialized(Surface surface) {
         // Scale to fit/center.
         float sx = surface.getWidth()  / mAnimation.getWidth();
         float sy = surface.getHeight() / mAnimation.getHeight();
@@ -52,20 +34,11 @@
             .translate((surface.getWidth()  - s * mAnimation.getWidth())  / 2,
                        (surface.getHeight() - s * mAnimation.getHeight()) / 2)
             .scale(s, s);
-
-        while (mRunning) {
-            renderFrame(surface.getCanvas(),
-                        (double)(java.lang.System.currentTimeMillis() - time_base) / 1000,
-                        surface.getWidth(), surface.getHeight());
-            surface.flushAndSubmit();
-        }
-
-        surface.release();
-        Log.d(TAG, "finish");
     }
 
-    private void renderFrame(Canvas canvas, double t, int canvas_width, int canvas_height) {
-        t = t % mAnimation.getDuration();
+    @Override
+    protected void onRenderFrame(Canvas canvas, long ms) {
+        double t = (double)ms / 1000 % mAnimation.getDuration();
         mAnimation.seekTime(t);
 
         canvas.save();
@@ -78,56 +51,27 @@
     }
 }
 
-public class SkottieAnimationActivity extends Activity implements SurfaceHolder.Callback {
+public class SkottieAnimationActivity extends Activity {
     static {
         System.loadLibrary("androidkit");
     }
 
-    private SkottieAnimationRenderThread mRenderThread;
-    private SkottieAnimation             mAnimation;
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_animation);
 
         SurfaceView sv = findViewById(R.id.surfaceView);
-        sv.getHolder().addCallback(this);
 
         try {
             InputStream is = getResources().openRawResource(R.raw.im_thirsty);
             byte[] data = new byte[is.available()];
             is.read(data);
-            mAnimation = new SkottieAnimation(new String(data));
+
+            SkottieAnimation animation = new SkottieAnimation(new String(data));
+            sv.getHolder().addCallback(new SkottieAnimationRenderer(animation));
         } catch (Exception e) {
-            Log.e("AndroidKit", "Could not load resource: " + R.raw.im_thirsty);
-        }
-    }
-
-    @Override
-    public void surfaceCreated(SurfaceHolder holder) {
-    }
-
-    @Override
-    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
-        }
-
-        mRenderThread = new SkottieAnimationRenderThread(holder.getSurface(), mAnimation);;
-        mRenderThread.start();
-    }
-
-    @Override
-    public void surfaceDestroyed(SurfaceHolder holder) {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            try {
-                mRenderThread.join();
-            } catch (InterruptedException e) {}
+            Log.e("AndroidKit", "Could not load animation resource: " + R.raw.im_thirsty);
         }
     }
 }