ソースを参照

Finish `DisableRendering` sample and add `isRendering` flag to `SpineView`

Denis Andrasec 1 年間 前
コミット
ebc7bc9cfc

+ 196 - 0
spine-android/app/src/main/java/com/esotericsoftware/spine/DisableRendering.kt

@@ -0,0 +1,196 @@
+package com.esotericsoftware.spine
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.navigation.NavHostController
+import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
+import com.esotericsoftware.spine.android.AndroidTextureAtlas
+import com.esotericsoftware.spine.android.SpineController
+import com.esotericsoftware.spine.android.SpineView
+import com.esotericsoftware.spine.android.utils.SkeletonDataUtils
+import kotlin.random.Random
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DisableRendering(nav: NavHostController) {
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(text = Destination.DisableRendering.title) },
+                navigationIcon = {
+                    IconButton({ nav.navigateUp() }) {
+                        Icon(
+                            Icons.Rounded.ArrowBack,
+                            null,
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        Column(
+            modifier = Modifier
+                .padding(paddingValues)
+                .padding()
+                .onGloballyPositioned { coordinates ->
+                    print(coordinates.size.toSize())
+                }
+        ) {
+            Column(
+                modifier = Modifier
+                    .padding(8.dp)
+            ) {
+                Text("Scroll spine boys out of the viewport")
+                Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.")
+            }
+            SpineBoys()
+        }
+    }
+}
+
+@Composable
+fun SpineBoys() {
+    var boxSize by remember { mutableStateOf(Size.Zero) }
+    val offsetX = remember { mutableFloatStateOf(0f) }
+    val offsetY = remember { mutableFloatStateOf(0f) }
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .clipToBounds()
+            .onGloballyPositioned { coordinates ->
+                boxSize = coordinates.size.toSize()
+            }
+            .pointerInput(Unit) {
+                detectDragGestures { change, dragAmount ->
+                    change.consume()
+                    offsetX.floatValue += dragAmount.x
+                    offsetY.floatValue += dragAmount.y
+                }
+            }
+    ) {
+        if (boxSize != Size.Zero) {
+            val contentSize = boxSize * 4f
+
+            val context = LocalContext.current
+            val cachedAtlas =
+                remember { AndroidTextureAtlas.fromAsset("spineboy.atlas", context) }
+            val cachedSkeletonData = remember {
+                SkeletonDataUtils.fromAsset(
+                    cachedAtlas,
+                    "spineboy-pro.json",
+                    context
+                )
+            }
+
+            val spineboys = remember {
+                val rng = Random(System.currentTimeMillis())
+                List(100) { index ->
+                    val scale = 0.1f + rng.nextFloat() * 0.2f
+                    val position = Offset(
+                        rng.nextFloat() * contentSize.width,
+                        rng.nextFloat() * contentSize.height
+                    )
+                    SpineBoyData(
+                        index,
+                        scale,
+                        position,
+                        if (index == 99) "hoverboard" else "walk"
+                    )
+                }
+            }
+
+            spineboys.forEach { spineBoyData ->
+
+                val isSpineBoyVisible = remember { mutableStateOf(false) }
+
+                Box(modifier = Modifier
+                    .offset {
+                        IntOffset(
+                            (-(contentSize.width / 2) + spineBoyData.position.x + offsetX.floatValue.toInt()).toInt(),
+                            (-(contentSize.height / 2) + spineBoyData.position.y + offsetY.floatValue.toInt()).toInt(),
+                        )
+                    }
+                    .size(
+                        (boxSize.width * spineBoyData.scale).dp,
+                        (boxSize.height * spineBoyData.scale).dp
+                    )
+                    .onGloballyPositioned { coordinates ->
+                        val positionInRoot = coordinates.positionInParent()
+                        val size = coordinates.size.toSize()
+
+                        val isInViewport = positionInRoot.x < boxSize.width &&
+                            positionInRoot.x + size.width > 0 &&
+                            positionInRoot.y < boxSize.height &&
+                            positionInRoot.y + size.height > 0
+
+                        isSpineBoyVisible.value = isInViewport
+                    }
+                ) {
+                    AndroidView(
+                        factory = { ctx ->
+                            SpineView.loadFromDrawable(
+                                AndroidSkeletonDrawable(cachedAtlas, cachedSkeletonData),
+                                ctx,
+                                SpineController {
+                                    it.animationState.setAnimation(
+                                        0,
+                                        spineBoyData.animation,
+                                        true
+                                    )
+                                }
+                            ).apply {
+                                isRendering = false
+                            }
+                        },
+                        update = { view ->
+                            view.isRendering = isSpineBoyVisible.value
+                        }
+                    )
+                }
+            }
+        }
+    }
+}
+
+data class SpineBoyData(
+    val id: Int,
+    val scale: Float,
+    val position: Offset,
+    val animation: String
+)
+

+ 4 - 5
spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt

@@ -20,7 +20,6 @@ import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBar
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.NavHost
@@ -67,7 +66,7 @@ fun AppContent() {
                                 Destination.DressUp,
                                 Destination.IKFollowing,
                                 Destination.Physics,
-                                Destination.TheBoys
+                                Destination.DisableRendering
                             ),
                             paddingValues
                         )
@@ -117,9 +116,9 @@ fun AppContent() {
                 }
 
                 composable(
-                    Destination.TheBoys.route
+                    Destination.DisableRendering.route
                 ) {
-                    TheBoys(navController)
+                    DisableRendering(navController)
                 }
             }
         }
@@ -188,5 +187,5 @@ sealed class Destination(val route: String, val title: String) {
     data object DressUp : Destination("dressUp", "Dress Up")
     data object IKFollowing : Destination("ikFollowing", "IK Following")
     data object Physics: Destination("physics", "Physics (drag anywhere)")
-    data object TheBoys: Destination("theBoys", "100 Spine Boys")
+    data object DisableRendering: Destination("disableRendering", "Disable Rendering")
 }

+ 0 - 133
spine-android/app/src/main/java/com/esotericsoftware/spine/TheBoys.kt

@@ -1,133 +0,0 @@
-package com.esotericsoftware.spine
-
-import android.view.View
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.navigation.NavHostController
-import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
-import com.esotericsoftware.spine.android.AndroidTextureAtlas
-import com.esotericsoftware.spine.android.SpineController
-import com.esotericsoftware.spine.android.SpineView
-import com.esotericsoftware.spine.android.utils.SkeletonDataUtils
-import kotlin.random.Random
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun TheBoys(nav: NavHostController) {
-    Scaffold(
-        topBar = {
-            TopAppBar(
-                title = { Text(text = Destination.TheBoys.title) },
-                navigationIcon = {
-                    IconButton({ nav.navigateUp() }) {
-                        Icon(
-                            Icons.Rounded.ArrowBack,
-                            null,
-                        )
-                    }
-                }
-            )
-        }
-    ) { paddingValues ->
-        var viewportSize by remember { mutableStateOf(Size.Zero) }
-        val offsetX = remember { mutableFloatStateOf(0f) }
-        val offsetY = remember { mutableFloatStateOf(0f) }
-
-        Box(
-            modifier = Modifier
-                .padding(paddingValues)
-                .fillMaxSize()
-                .clipToBounds()
-                .onGloballyPositioned { coordinates ->
-                    viewportSize = coordinates.size.toSize()
-                }
-                .pointerInput(Unit) {
-                    detectDragGestures { change, dragAmount ->
-                        change.consume()
-                        offsetX.floatValue += dragAmount.x
-                        offsetY.floatValue += dragAmount.y
-                    }
-                }
-        ) {
-            if (viewportSize != Size.Zero) {
-                val contentSize = viewportSize * 4f
-
-                val context = LocalContext.current
-                val cachedAtlas = remember { AndroidTextureAtlas.fromAsset("spineboy.atlas", context) }
-                val cachedSkeletonData = remember { SkeletonDataUtils.fromAsset(cachedAtlas, "spineboy-pro.json", context) }
-
-                val spineboys = remember {
-                    val rng = Random(System.currentTimeMillis())
-                    List(100) { index ->
-                        val scale = 0.1f + rng.nextFloat() * 0.2f
-                        val position = Offset(rng.nextFloat() * contentSize.width, rng.nextFloat() * contentSize.height)
-                        SpineBoyData(scale, position, if (index == 99) "hoverboard" else "walk")
-                    }
-                }
-
-                spineboys.forEach { spineBoyData ->
-                    Box(modifier = Modifier
-                        .offset {
-                            IntOffset(
-                                (-(contentSize.width / 2) + spineBoyData.position.x + offsetX.floatValue.toInt()).toInt(),
-                                (-(contentSize.height / 2) + spineBoyData.position.y + offsetY.floatValue.toInt()).toInt(),
-                            )
-                        }
-                        .scale(spineBoyData.scale)
-                    ) {
-                        AndroidView(
-                            factory = { ctx ->
-                                SpineView.loadFromDrawable(
-                                    AndroidSkeletonDrawable(cachedAtlas, cachedSkeletonData),
-                                    ctx,
-                                    SpineController {
-                                        it.animationState.setAnimation(0, spineBoyData.animation, true)
-                                    }
-                                )
-                            },
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
-
-data class SpineBoyData(
-    val scale: Float,
-    val position: Offset,
-    val animation: String
-)
-

+ 22 - 2
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineController.java

@@ -119,11 +119,15 @@ public class SpineController {
     }
 
     public void pause() {
-        playing = false;
+        if (playing) {
+            playing = false;
+        }
     }
 
     public void resume() {
-        playing = true;
+        if (!playing) {
+            playing = true;
+        }
     }
 
     public Point toSkeletonCoordinates(Point position) {
@@ -132,6 +136,22 @@ public class SpineController {
         return new Point((int) (x / scaleX - offsetX), (int) (y / scaleY - offsetY));
     }
 
+    public void setOnBeforeUpdateWorldTransforms(@Nullable SpineControllerCallback onBeforeUpdateWorldTransforms) {
+        this.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms;
+    }
+
+    public void setOnAfterUpdateWorldTransforms(@Nullable SpineControllerCallback onAfterUpdateWorldTransforms) {
+        this.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms;
+    }
+
+    public void setOnBeforePaint(@Nullable SpineControllerBeforePaintCallback onBeforePaint) {
+        this.onBeforePaint = onBeforePaint;
+    }
+
+    public void setOnAfterPaint(@Nullable SpineControllerAfterPaintCallback onAfterPaint) {
+        this.onAfterPaint = onAfterPaint;
+    }
+
     protected void setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) {
         this.offsetX = offsetX;
         this.offsetY = offsetY;

+ 17 - 1
spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java

@@ -42,13 +42,20 @@ import android.graphics.Canvas;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.Choreographer;
 import android.view.View;
 
 import androidx.annotation.NonNull;
 
+import java.io.Console;
 import java.io.File;
 import java.net.URL;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
 
 public class SpineView extends View implements Choreographer.FrameCallback {
 
@@ -138,6 +145,7 @@ public class SpineView extends View implements Choreographer.FrameCallback {
 	private float x = 0;
 	private float y = 0;
 	private final SkeletonRenderer renderer = new SkeletonRenderer();
+	private Boolean rendering = true;
 	private Bounds computedBounds = new Bounds();
 
 	private SpineController controller;
@@ -235,6 +243,14 @@ public class SpineView extends View implements Choreographer.FrameCallback {
 		updateCanvasTransform();
 	}
 
+	public Boolean isRendering() {
+		return rendering;
+	}
+
+	public void setRendering(Boolean rendering) {
+		this.rendering = rendering;
+	}
+
 	private void loadFrom(AndroidSkeletonDrawableLoader loader) {
 		Handler mainHandler = new Handler(Looper.getMainLooper());
 		Thread backgroundThread = new Thread(() -> {
@@ -253,7 +269,7 @@ public class SpineView extends View implements Choreographer.FrameCallback {
 	@Override
 	public void onDraw (@NonNull Canvas canvas) {
 		super.onDraw(canvas);
-		if (controller == null || !controller.isInitialized()) {
+		if (controller == null || !controller.isInitialized() || !rendering) {
 			return;
 		}