Finish DisableRendering sample and add isRendering flag to SpineView

This commit is contained in:
Denis Andrasec 2024-07-18 12:27:30 +02:00
parent 2e5c1c9ff0
commit ebc7bc9cfc
5 changed files with 239 additions and 141 deletions

View File

@ -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
)

View File

@ -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")
}

View File

@ -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
)

View File

@ -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;

View File

@ -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;
}