Last active
February 16, 2025 11:41
-
-
Save pbk20191/77c3800f36205cbdd157324a0f414e91 to your computer and use it in GitHub Desktop.
Spine Android Native Drawable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.esotericsoftware.spine.android; | |
import android.graphics.Canvas | |
import com.badlogic.gdx.utils.Array | |
import com.esotericsoftware.spine.android.SkeletonRenderer.RenderCommand | |
// this is the hack for accessing package-private while in ART & Kotlin | |
// ART does not have module.info.java so we can safely access it at runtime | |
// we can access java-package scope at compile time thanks to kotlin | |
// kotlin + ART(no module.info.java) -> below class can access everything! | |
data object KTExtensionSpineCompat { | |
fun getPaint( | |
command: RenderCommand | |
) = command.texture.getPaint(command.blendMode) | |
fun commandUV(command: RenderCommand) = command.uvs | |
fun commandVertice(command: RenderCommand) = command.vertices | |
fun commandColors(command: RenderCommand) = command.colors | |
fun commandIndices(command: RenderCommand) = command.indices | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.TimeAnimator | |
import android.graphics.Canvas | |
import android.graphics.ColorFilter | |
import android.graphics.PixelFormat.TRANSPARENT | |
import android.graphics.Rect | |
import android.graphics.Region | |
import android.graphics.drawable.Animatable | |
import android.graphics.drawable.Animatable2 | |
import android.graphics.drawable.Drawable | |
import android.os.Build | |
import androidx.core.animation.doOnEnd | |
import androidx.core.animation.doOnStart | |
import com.badlogic.gdx.utils.Array | |
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable | |
import com.esotericsoftware.spine.android.KTExtensionSpineCompat | |
import com.esotericsoftware.spine.android.SkeletonRenderer | |
import com.esotericsoftware.spine.android.SkeletonRenderer.RenderCommand | |
import com.esotericsoftware.spine.android.SpineController | |
import com.esotericsoftware.spine.android.bounds.Alignment | |
import com.esotericsoftware.spine.android.bounds.Bounds | |
import com.esotericsoftware.spine.android.bounds.BoundsProvider | |
import com.esotericsoftware.spine.android.bounds.ContentMode | |
import com.esotericsoftware.spine.android.bounds.SetupPoseBounds | |
import kotlin.properties.Delegates | |
class SpineDrawable( | |
drawable: AndroidSkeletonDrawable, | |
): Drawable(), Animatable, Animatable2, TimeAnimator.TimeListener { | |
private val animationCb = ArrayList<Animatable2.AnimationCallback>() | |
private var offsetX = 0.0f; | |
private var offsetY = 0.0f; | |
private var scaleX = 1.0f | |
private var scaleY = 1.0f | |
private var x = 0.0f | |
private var y = 0.0f | |
private val computedBounds = Bounds(); | |
private val renderer = SkeletonRenderer() | |
private var colorFilter: ColorFilter? = null | |
private var alpha = 255; | |
private val _controller:SpineDrawableController = SpineDrawableController(drawable) | |
private val clock = _controller.clock | |
val controller: SpineController get() = this._controller | |
public var alignment by Delegates.observable(Alignment.CENTER) { prop, old, new -> | |
updateCanvasTransform() | |
} | |
public var contentMode by Delegates.observable(ContentMode.FIT) { prop, old, new -> | |
updateCanvasTransform() | |
} | |
public var boundProvider: BoundsProvider by Delegates.observable(SetupPoseBounds()) { prop, old, new -> | |
val other = new.computeBounds(this.controller.drawable) | |
this.computedBounds.x = other.x | |
this.computedBounds.y = other.y | |
this.computedBounds.width = other.width | |
this.computedBounds.height = other.height | |
updateCanvasTransform() | |
} | |
init { | |
val other = boundProvider.computeBounds(controller.drawable) | |
this.computedBounds.x = other.x | |
this.computedBounds.y = other.y | |
this.computedBounds.width = other.width | |
this.computedBounds.height = other.height | |
clock.setTimeListener(this) | |
clock.doOnStart { | |
animationCb.forEach { | |
it.onAnimationStart(this) | |
} | |
} | |
clock.doOnEnd { | |
animationCb.forEach{ | |
it.onAnimationEnd(this) | |
} | |
} | |
} | |
override fun draw(canvas: Canvas) { | |
val saved = if (alpha < 255) { | |
canvas.saveLayerAlpha(bounds.left.toFloat(),bounds.top.toFloat(), bounds.right.toFloat(), bounds.bottom.toFloat(), alpha) | |
} else { | |
canvas.save() | |
} | |
canvas.translate(offsetX, offsetY); | |
canvas.scale(scaleX, scaleY * -1); | |
canvas.translate(x, y); | |
_controller.callOnBeforePaint(canvas) | |
val commands = renderer.render(controller.getSkeleton()); | |
// if color filter support is not needed, this commented single line of code is enough | |
// renderer.renderToCanvas(canvas, commands); | |
val myFilter = this.colorFilter | |
commands.forEach { command -> | |
// TODO: better way to inject Color Filter | |
val paint = KTExtensionSpineCompat.getPaint(command) | |
val oldFilter = paint.colorFilter | |
if (myFilter!= null) { | |
paint.colorFilter = myFilter | |
} | |
// TODO: contribute to Spine-runtime directly rather than using hacks | |
val vertices = KTExtensionSpineCompat.commandVertice(command) | |
val uvs = KTExtensionSpineCompat.commandUV(command) | |
val indices = KTExtensionSpineCompat.commandIndices(command) | |
val colors = KTExtensionSpineCompat.commandColors(command) | |
if (Build.VERSION.SDK_INT >= 29) { | |
canvas.drawVertices( | |
Canvas.VertexMode.TRIANGLES, vertices.size, vertices.items, 0, uvs.items, | |
0, colors.items, 0, indices.items, 0, indices.size, | |
paint); | |
} else { | |
// See https://github.com/EsotericSoftware/spine-runtimes/issues/2638 | |
val colors = colors.items; | |
val colorsCopy = IntArray(size = vertices.size) //new int[command.vertices.size]; | |
System.arraycopy(colors, 0, colorsCopy, 0, colors.size); | |
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.size, vertices.items, 0, uvs.items, | |
0, colorsCopy, 0, indices.items, 0, indices.size, paint); | |
} | |
if (myFilter != null) { | |
paint.colorFilter = oldFilter | |
} | |
} | |
_controller.callOnAfterPaint(canvas, commands) | |
canvas.restoreToCount(saved) | |
} | |
override fun setAlpha(alpha: Int) { | |
this.alpha = alpha | |
} | |
override fun getAlpha(): Int { | |
return this.alpha | |
} | |
override fun onBoundsChange(bounds: Rect) { | |
super.onBoundsChange(bounds) | |
updateCanvasTransform() | |
} | |
override fun getIntrinsicHeight(): Int { | |
return this.computedBounds.height.toInt() | |
} | |
override fun getIntrinsicWidth(): Int { | |
return this.computedBounds.width.toInt() | |
} | |
private fun updateCanvasTransform () { | |
x = (-computedBounds.x.toFloat() - computedBounds.width.toFloat() / 2.0f | |
- (alignment.x.toFloat() * computedBounds.width.toFloat() / 2.0f)); | |
y = (-computedBounds.y - computedBounds.height / 2.0f | |
- (alignment.y * computedBounds.height / 2.0f)).toFloat(); | |
when (contentMode) { | |
ContentMode.FIT -> { | |
scaleX = (bounds.width() / computedBounds.width).coerceAtMost(bounds.height() / computedBounds.height) | |
.toFloat() | |
scaleY = scaleX | |
} | |
ContentMode.FILL -> { | |
scaleX = (bounds.width() / computedBounds.width).coerceAtLeast(bounds.height() / computedBounds.height) | |
.toFloat() | |
scaleY = scaleX | |
} | |
} | |
offsetX = (bounds.width() / 2.0 + (alignment.x * bounds.width() / 2.0)).toFloat() | |
offsetY = (bounds.height() / 2.0 + (alignment.y * bounds.height() / 2.0)).toFloat() | |
_controller.setCoordinateTransform( | |
x + offsetX / scaleX.toDouble(), y + offsetY / scaleY.toDouble(), scaleX.toDouble(), scaleY.toDouble() | |
) | |
} | |
override fun start() { | |
_controller.superResume() | |
clock.start() | |
} | |
override fun stop() { | |
controller.pause() | |
clock.cancel() | |
} | |
public fun resume() { | |
_controller.superResume() | |
clock.resume() | |
} | |
public fun pause() { | |
_controller.superPause() | |
clock.pause() | |
} | |
override fun isRunning(): Boolean { | |
return clock.isRunning | |
} | |
override fun registerAnimationCallback(callback: Animatable2.AnimationCallback) { | |
animationCb.add(callback) | |
} | |
override fun unregisterAnimationCallback(callback: Animatable2.AnimationCallback): Boolean { | |
return animationCb.remove(callback) | |
} | |
override fun clearAnimationCallbacks() { | |
animationCb.clear() | |
} | |
override fun setColorFilter(colorFilter: ColorFilter?) { | |
this.colorFilter = colorFilter | |
} | |
override fun getColorFilter(): ColorFilter? { | |
return this.colorFilter | |
} | |
override fun getOpacity(): Int { | |
return TRANSPARENT | |
} | |
// TODO: return transparent region | |
override fun getTransparentRegion(): Region? { | |
return super.getTransparentRegion() | |
} | |
override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) { | |
_controller.worldTransform { | |
controller.getDrawable().update(deltaTime.toFloat() / 1e3f); | |
} | |
invalidateSelf() | |
if (callback == null || !controller.isPlaying) { | |
this.clock.pause() | |
} | |
} | |
override fun setVisible(visible: Boolean, restart: Boolean): Boolean { | |
val changed = super.setVisible(visible, restart) | |
if (changed) { | |
if (visible) { | |
_controller.superResume() | |
if (restart) { | |
clock.start() | |
} else { | |
if (clock.isStarted) { | |
clock.resume() | |
} else { | |
clock.start() | |
} | |
} | |
} else { | |
clock.pause() | |
} | |
} | |
return changed | |
} | |
private open class SpineDrawableController(drawable: AndroidSkeletonDrawable): SpineController({}) { | |
init { | |
this.init(drawable) | |
} | |
public val clock = TimeAnimator() | |
public override fun callOnBeforePaint(canvas: Canvas) { | |
super.callOnBeforePaint(canvas) | |
} | |
public override fun callOnAfterPaint(canvas: Canvas, commands: Array<RenderCommand>) { | |
super.callOnAfterPaint(canvas, commands) | |
} | |
fun worldTransform( block: () -> Unit) { | |
this.callOnBeforeUpdateWorldTransforms() | |
block() | |
this.callOnAfterUpdateWorldTransforms() | |
} | |
public override fun setCoordinateTransform( | |
offsetX: Double, | |
offsetY: Double, | |
scaleX: Double, | |
scaleY : Double) { | |
super.setCoordinateTransform(offsetX, offsetY, scaleX, scaleY) | |
} | |
public fun superResume() { | |
super.resume() | |
} | |
public fun superPause() { | |
super.pause() | |
} | |
override fun resume() { | |
super.resume() | |
if (clock.isStarted) { | |
clock.resume() | |
} else { | |
clock.start() | |
} | |
} | |
override fun pause() { | |
super.pause() | |
clock.pause() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment