Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate core module to jvm only module #321

Merged
merged 11 commits into from
Dec 29, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ captures/
/.DS_Store
jarRepositories.xml
androidTestResultsUserPreferences.xml
migrations.xml
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
id("org.jetbrains.dokka") version "1.7.0"
}

buildscript {
Expand All @@ -14,6 +15,7 @@ buildscript {
classpath("com.github.dcendents:android-maven-gradle-plugin:2.1")
classpath("com.diffplug.spotless:spotless-plugin-gradle:5.14.2")
classpath("io.github.gradle-nexus:publish-plugin:1.3.0")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.7.0")
}
}

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ object buildVersions {

object Constants {
const val composeVersion = "1.4.3"
const val konfettiVersion = "2.0.4"
const val konfettiVersion = "2.1.0-beta01"
const val kotlinVersion = "1.8.10"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import nl.dionsegijn.konfetti.core.Particle
import nl.dionsegijn.konfetti.core.models.ReferenceImage
import nl.dionsegijn.konfetti.core.models.Shape
import nl.dionsegijn.konfetti.core.models.Shape.Circle
import nl.dionsegijn.konfetti.core.models.Shape.DrawableShape
import nl.dionsegijn.konfetti.core.models.Shape.Rectangle
import nl.dionsegijn.konfetti.core.models.Shape.Square
import nl.dionsegijn.konfetti.xml.image.ImageStore

/**
* Draw a shape to `compose canvas`. Implementations are expected to draw within a square of size
* `size` and must vertically/horizontally center their asset if it does not have an equal width
* and height.
*/
fun Shape.draw(drawScope: DrawScope, particle: Particle, imageResource: ImageBitmap? = null) {
fun Shape.draw(drawScope: DrawScope, particle: Particle, imageResource: ImageBitmap? = null, imageStore: ImageStore) {
when (this) {
Circle -> {
val offsetMiddle = particle.width / 2
Expand All @@ -50,25 +52,31 @@ fun Shape.draw(drawScope: DrawScope, particle: Particle, imageResource: ImageBit
)
}
is DrawableShape -> {
drawScope.drawIntoCanvas {
if (tint) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.colorFilter = BlendModeColorFilter(particle.color, BlendMode.SRC_IN)
} else {
drawable.setColorFilter(particle.color, PorterDuff.Mode.SRC_IN)
val referenceImage = image
if (referenceImage is ReferenceImage) {
val drawable = imageStore.getImage(referenceImage.reference) ?: return

drawScope.drawIntoCanvas {
// Making use of the ImageStore for performance reasons, see ImageStore for more info
if (tint) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
drawable.colorFilter = BlendModeColorFilter(particle.color, BlendMode.SRC_IN)
} else {
drawable.setColorFilter(particle.color, PorterDuff.Mode.SRC_IN)
}
} else if (applyAlpha) {
drawable.alpha = particle.alpha
}
} else if (applyAlpha) {
drawable.alpha = particle.alpha
}

val size = particle.width
val height = (size * heightRatio).toInt()
val top = ((size - height) / 2f).toInt()
val size = particle.width
val height = (size * heightRatio).toInt()
val top = ((size - height) / 2f).toInt()

val x = particle.y.toInt()
val y = particle.x.toInt()
drawable.setBounds(y, top + x, size.toInt() + y, top + height + x)
drawable.draw(it.nativeCanvas)
val x = particle.y.toInt()
val y = particle.x.toInt()
drawable.setBounds(y, top + x, size.toInt() + y, top + height + x)
drawable.draw(it.nativeCanvas)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nl.dionsegijn.konfetti.compose

import android.graphics.Rect
import android.content.res.Resources
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -14,6 +15,11 @@ import androidx.compose.ui.layout.onGloballyPositioned
import nl.dionsegijn.konfetti.core.Particle
import nl.dionsegijn.konfetti.core.Party
import nl.dionsegijn.konfetti.core.PartySystem
import nl.dionsegijn.konfetti.core.models.CoreRectImpl
import nl.dionsegijn.konfetti.core.models.ReferenceImage
import nl.dionsegijn.konfetti.core.models.Shape
import nl.dionsegijn.konfetti.xml.image.DrawableImage
import nl.dionsegijn.konfetti.xml.image.ImageStore

@Composable
fun KonfettiView(
Expand All @@ -37,10 +43,20 @@ fun KonfettiView(
/**
* Area in which the particles are being drawn
*/
val drawArea = remember { mutableStateOf(Rect()) }
val drawArea = remember { mutableStateOf(CoreRectImpl()) }

/**
* Store for drawable images
*/
val imageStore = remember { ImageStore() }

LaunchedEffect(Unit) {
partySystems = parties.map { PartySystem(it) }
partySystems = parties.map {
PartySystem(
party = storeImages(it, imageStore),
pixelDensity = Resources.getSystem().displayMetrics.density
)
}
while (true) {
withFrameMillis { frameMs ->
// Calculate time between frames, fallback to 0 when previous frame doesn't exist
Expand Down Expand Up @@ -69,7 +85,8 @@ fun KonfettiView(
Canvas(
modifier = modifier
.onGloballyPositioned {
drawArea.value = Rect(0, 0, it.size.width, it.size.height)
drawArea.value =
CoreRectImpl(0f, 0f, it.size.width.toFloat(), it.size.height.toFloat())
},
onDraw = {
particles.value.forEach { particle ->
Expand All @@ -87,13 +104,44 @@ fun KonfettiView(
pivot = Offset(particle.x + (particle.width / 2), particle.y)
)
}) {
particle.shape.draw(this, particle)
particle.shape.draw(drawScope = this, particle = particle, imageStore = imageStore)
}
}
}
)
}

/**
* Transforms the shapes in the given [Party] object. If a shape is a [Shape.DrawableShape],
* it replaces the [DrawableImage] with a [ReferenceImage] and stores the [Drawable] in the [ImageStore].
*
* @param party The Party object containing the shapes to be transformed.
* @return A new Party object with the transformed shapes.
*/
fun storeImages(party: Party, imageStore: ImageStore): Party {
val transformedShapes = party.shapes.map { shape ->
when (shape) {
is Shape.DrawableShape -> {
val referenceImage = drawableToReferenceImage(shape.image as DrawableImage, imageStore)
shape.copy(image = referenceImage)
}
else -> shape
}
}
return party.copy(shapes = transformedShapes)
}

/**
* Converts a [DrawableImage] to a [ReferenceImage] and stores the [Drawable] in the [ImageStore].
*
* @param drawableImage The DrawableImage to be converted.
* @return A ReferenceImage with the same dimensions as the DrawableImage and a reference to the stored Drawable.
*/
fun drawableToReferenceImage(drawableImage: DrawableImage, imageStore: ImageStore): ReferenceImage {
val id = imageStore.storeImage(drawableImage.drawable)
return ReferenceImage(id, drawableImage.width, drawableImage.height)
}

fun getTotalTimeRunning(startTime: Long): Long {
val currentTime = System.currentTimeMillis()
return (currentTime - startTime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nl.dionsegijn.konfetti.xml.image

import android.graphics.drawable.Drawable
import nl.dionsegijn.konfetti.core.models.CoreImage

data class DrawableImage(
val drawable: Drawable,
override val width: Int,
override val height: Int
) : CoreImage
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package nl.dionsegijn.konfetti.xml.image

import android.graphics.drawable.Drawable
import nl.dionsegijn.konfetti.core.models.CoreImageStore

/**
* The ImageStore class is used to store Drawable objects and provide a way to reference them.
* This is done for performance reasons and to allow the core library, which can't use Android Drawables,
* to work with images.
*
* Instead of converting a Drawable to a ByteBuffer, then to a Bitmap, and then back to a Drawable,
* which is inefficient for the render code, the Drawable is stored in the ImageStore.
* The rest of the application can then work with a simple integer reference to the Drawable.
*
* The ImageStore provides methods to store a Drawable and retrieve it using its reference.
*/
class ImageStore : CoreImageStore<Drawable> {
private val images = mutableMapOf<Int, Drawable>()

override fun storeImage(image: Drawable): Int {
val id = image.hashCode()
images[id] = image
return id
}

override fun getImage(id: Int): Drawable? {
return images[id]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nl.dionsegijn.konfetti.xml.image

import android.graphics.drawable.Drawable
import nl.dionsegijn.konfetti.core.models.Shape

object ImageUtil {

@JvmStatic
fun loadDrawable(
drawable: Drawable,
tint: Boolean = true,
applyAlpha: Boolean = true
): Shape.DrawableShape {
val width = drawable.intrinsicWidth
val height = drawable.intrinsicHeight
val drawableImage = DrawableImage(drawable, width, height)
return Shape.DrawableShape(drawableImage, tint, applyAlpha)
}
}
37 changes: 4 additions & 33 deletions konfetti/core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("java-library")
id("kotlin")
id("com.diffplug.spotless")
}

Expand All @@ -9,47 +9,18 @@ apply(from = "../../scripts/publish-module.gradle.kts")

spotless {
kotlin {
ktlint("0.37.2")
ktlint("1.1.0")
target("src/**/*.kt")
}
java {
removeUnusedImports()
googleJavaFormat("1.5")
googleJavaFormat("1.15.0")
target("**/*.java")
}
}

android {
compileSdk = buildVersions.compileSdk

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = "1.8"
}

defaultConfig {
minSdk = 15
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
namespace = "nl.dionsegijn.konfetti.core"
lint {
abortOnError = true
baseline = file("lint-baseline.xml")
}
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${Constants.kotlinVersion}")
testImplementation(libs.test.junit)
testImplementation(libs.test.mockito)

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package nl.dionsegijn.konfetti.core

import android.content.res.Resources
import android.graphics.Rect
import nl.dionsegijn.konfetti.core.emitter.BaseEmitter
import nl.dionsegijn.konfetti.core.emitter.Confetti
import nl.dionsegijn.konfetti.core.emitter.PartyEmitter
import nl.dionsegijn.konfetti.core.models.CoreRect

/**
* PartySystem is responsible for requesting particles from the emitter and updating the particles
Expand All @@ -16,7 +15,7 @@ import nl.dionsegijn.konfetti.core.emitter.PartyEmitter
class PartySystem(
val party: Party,
val createdAt: Long = System.currentTimeMillis(),
pixelDensity: Float = Resources.getSystem().displayMetrics.density
pixelDensity: Float
) {

var enabled = true
Expand All @@ -27,7 +26,7 @@ class PartySystem(

// Called every frame to create and update the particles state
// returns a list of particles that are ready to be rendered
fun render(deltaTime: Float, drawArea: Rect): List<Particle> {
fun render(deltaTime: Float, drawArea: CoreRect): List<Particle> {
if (enabled) {
activeParticles.addAll(emitter.createConfetti(deltaTime, party, drawArea))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package nl.dionsegijn.konfetti.core.emitter

import android.graphics.Rect
import nl.dionsegijn.konfetti.core.Party
import nl.dionsegijn.konfetti.core.models.CoreRect

/**
* An abstract class for creating a custom emitter
Expand All @@ -16,7 +16,7 @@ abstract class BaseEmitter {
abstract fun createConfetti(
deltaTime: Float,
party: Party,
drawArea: Rect,
drawArea: CoreRect,
): List<Confetti>

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package nl.dionsegijn.konfetti.core.emitter

import android.graphics.Paint
import android.graphics.Rect
import nl.dionsegijn.konfetti.core.models.CoreRect
import nl.dionsegijn.konfetti.core.models.Shape
import nl.dionsegijn.konfetti.core.models.Vector
import kotlin.math.abs
Expand Down Expand Up @@ -90,7 +89,7 @@ class Confetti(
/**
* Updates the state of the particle for each frame of the animation.
*/
fun render(deltaTime: Float, drawArea: Rect) {
fun render(deltaTime: Float, drawArea: CoreRect) {
applyForce(gravity)
update(deltaTime, drawArea)
}
Expand All @@ -99,11 +98,11 @@ class Confetti(
* Updates the state of the particle based on its current acceleration, velocity, and location.
* Also handles the fading out of the particle when its lifespan is over.
*/
private fun update(deltaTime: Float, drawArea: Rect) {
private fun update(deltaTime: Float, drawArea: CoreRect) {
// Calculate frameRate dynamically, fallback to 60fps in case deltaTime is 0
frameRate = if (deltaTime > 0) 1f / deltaTime else DEFAULT_FRAME_RATE

if (location.y > drawArea.height()) {
if (location.y > drawArea.height) {
alpha = 0
return
}
Expand Down
Loading
Loading