From b2708277304ffddd1db7c8db06efe04ecdd75348 Mon Sep 17 00:00:00 2001 From: Matt Precious Date: Wed, 4 Mar 2020 11:42:56 -0500 Subject: [PATCH 1/2] Add support for custom shapes --- .../dionsegijn/konfettidemo/MainActivity.kt | 2 +- .../configurations/settings/Configuration.kt | 2 +- .../views/ShapeSelectionView.kt | 15 ++- .../src/main/res/drawable/ic_heart.xml | 9 ++ .../dionsegijn/simple_demo/MainActivity.java | 2 +- .../java/nl/dionsegijn/konfetti/Confetti.kt | 30 ++--- .../nl/dionsegijn/konfetti/ParticleSystem.kt | 2 +- .../nl/dionsegijn/konfetti/models/Shape.kt | 103 ++++++++++++++++-- 8 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 demo-google-play/src/main/res/drawable/ic_heart.xml diff --git a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/MainActivity.kt b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/MainActivity.kt index 8b51a351..cd0bee64 100644 --- a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/MainActivity.kt +++ b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/MainActivity.kt @@ -135,7 +135,7 @@ class MainActivity : AppCompatActivity(), OnConfigurationChangedListener { .addColors(*colors) .setDirection(degrees - 50, degrees + 50) .setSpeed(0f, speed + 5f) - .addShapes(Shape.RECT, Shape.CIRCLE) + .addShapes(Shape.Square, Shape.Circle) .addSizes(Size(12), Size(16, 6f)) .setPosition(startX, startY) .setTimeToLive(10000) diff --git a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/settings/Configuration.kt b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/settings/Configuration.kt index 540a0a0d..73937177 100644 --- a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/settings/Configuration.kt +++ b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/settings/Configuration.kt @@ -27,5 +27,5 @@ open class Configuration( var minSpeed: Float = 4f var maxSpeed: Float = 7f var colors = intArrayOf(R.color.lt_yellow, R.color.lt_orange, R.color.lt_purple, R.color.lt_pink) - var shapes: Array = arrayOf(Shape.RECT, Shape.CIRCLE) + var shapes: Array = arrayOf(Shape.Square, Shape.Circle) } diff --git a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/views/ShapeSelectionView.kt b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/views/ShapeSelectionView.kt index b505f995..3ca2d7d3 100644 --- a/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/views/ShapeSelectionView.kt +++ b/demo-google-play/src/main/java/nl/dionsegijn/konfettidemo/configurations/views/ShapeSelectionView.kt @@ -33,13 +33,17 @@ class ShapeSelectionView( return dp * resources.displayMetrics.density } - private val availableShapes = arrayOf(Shape.CIRCLE, Shape.RECT) + private val availableShapes = arrayOf( + Shape.Circle, + Shape.Square, + Shape.DrawableShape(ContextCompat.getDrawable(context, R.drawable.ic_heart)!!) + ) init { inflate(context, R.layout.view_section_shape_selection, this) orientation = HORIZONTAL gravity = Gravity.CENTER - displayShapeConfigOptions(availableShapes) + displayShapeConfigOptions(configurationManager.active.shapes) } private fun displayShapeConfigOptions(selectedShapes: Array) { @@ -50,7 +54,12 @@ class ShapeSelectionView( button.elevation = 6f } - val drawable = if (shape == Shape.RECT) R.drawable.ic_rectangle else R.drawable.ic_circle + val drawable = when (shape) { + Shape.Square -> R.drawable.ic_rectangle + Shape.Circle -> R.drawable.ic_circle + is Shape.DrawableShape -> R.drawable.ic_heart + else -> throw IllegalArgumentException("Unexpected shape: $shape") + } /** Set width, height and margins of the button */ val params = LinearLayout.LayoutParams(buttonWidth, buttonHeight) diff --git a/demo-google-play/src/main/res/drawable/ic_heart.xml b/demo-google-play/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..cfba5d84 --- /dev/null +++ b/demo-google-play/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/demo-simple-java/src/main/java/nl/dionsegijn/simple_demo/MainActivity.java b/demo-simple-java/src/main/java/nl/dionsegijn/simple_demo/MainActivity.java index e32635c4..334b6e5b 100644 --- a/demo-simple-java/src/main/java/nl/dionsegijn/simple_demo/MainActivity.java +++ b/demo-simple-java/src/main/java/nl/dionsegijn/simple_demo/MainActivity.java @@ -26,7 +26,7 @@ public void onClick(final View view) { .setSpeed(1f, 5f) .setFadeOutEnabled(true) .setTimeToLive(2000L) - .addShapes(Shape.RECT, Shape.CIRCLE) + .addShapes(Shape.Square.INSTANCE, Shape.Circle.INSTANCE) .addSizes(new Size(12, 5f)) .setPosition(-50f, konfettiView.getWidth() + 50f, -50f, -50f) .streamFor(300, 5000L); diff --git a/konfetti/src/main/java/nl/dionsegijn/konfetti/Confetti.kt b/konfetti/src/main/java/nl/dionsegijn/konfetti/Confetti.kt index 0a990247..d248fc1f 100644 --- a/konfetti/src/main/java/nl/dionsegijn/konfetti/Confetti.kt +++ b/konfetti/src/main/java/nl/dionsegijn/konfetti/Confetti.kt @@ -3,11 +3,11 @@ package nl.dionsegijn.konfetti import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint -import android.graphics.RectF import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size import nl.dionsegijn.konfetti.models.Vector import kotlin.random.Random +import kotlin.math.abs class Confetti( var location: Vector, @@ -27,7 +27,6 @@ class Confetti( private var rotationSpeed = 1f private var rotation = 0f private var rotationWidth = width - private var rectF = RectF() // Expected frame rate private var speedF = 60f @@ -96,26 +95,17 @@ class Confetti( return } - var left = location.x + (width - rotationWidth) - var right = location.x + rotationWidth - /** Switch values. Left or right may not be negative due to how Android Api < 25 - * draws Rect and RectF, negative values won't be drawn resulting in flickering confetti */ - if (left > right) { - left += right - right = left - right - left -= right - } - paint.alpha = alpha - rectF.set(left, location.y, right, location.y + getSize()) + val scaleX = abs(rotationWidth / width - 0.5f) * 2 + val centerX = scaleX * width / 2 - canvas.save() - canvas.rotate(rotation, rectF.centerX(), rectF.centerY()) - when (shape) { - Shape.CIRCLE -> canvas.drawOval(rectF, paint) - Shape.RECT -> canvas.drawRect(rectF, paint) - } - canvas.restore() + val saveCount = canvas.save() + canvas.translate(location.x - centerX, location.y) + canvas.rotate(rotation, centerX, width / 2) + canvas.scale(scaleX, 1f) + + shape.draw(canvas, paint, width) + canvas.restoreToCount(saveCount) } } diff --git a/konfetti/src/main/java/nl/dionsegijn/konfetti/ParticleSystem.kt b/konfetti/src/main/java/nl/dionsegijn/konfetti/ParticleSystem.kt index 00218894..04f1df77 100644 --- a/konfetti/src/main/java/nl/dionsegijn/konfetti/ParticleSystem.kt +++ b/konfetti/src/main/java/nl/dionsegijn/konfetti/ParticleSystem.kt @@ -26,7 +26,7 @@ class ParticleSystem(private val konfettiView: KonfettiView) { /** Default values */ private var colors = intArrayOf(Color.RED) private var sizes = arrayOf(Size(16)) - private var shapes = arrayOf(Shape.RECT) + private var shapes: Array = arrayOf(Shape.Square) private var confettiConfig = ConfettiConfig() /** diff --git a/konfetti/src/main/java/nl/dionsegijn/konfetti/models/Shape.kt b/konfetti/src/main/java/nl/dionsegijn/konfetti/models/Shape.kt index 1a365506..2c771207 100644 --- a/konfetti/src/main/java/nl/dionsegijn/konfetti/models/Shape.kt +++ b/konfetti/src/main/java/nl/dionsegijn/konfetti/models/Shape.kt @@ -1,12 +1,97 @@ package nl.dionsegijn.konfetti.models -/** - * Created by dionsegijn on 3/26/17. - * Available shapes - * RECT (rectangle) - * Circle - */ -enum class Shape { - RECT(), - CIRCLE() +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.RectF +import android.graphics.drawable.Drawable + +interface Shape { + /** + * Draw a shape to `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 draw(canvas: Canvas, paint: Paint, size: Float) + + companion object { + // Maintain binary and backwards compatibility with previous enum API. + @JvmField + @Deprecated("Use Square class, instead.", replaceWith = ReplaceWith("Shape.Square")) + val RECT = Square + + @JvmField + @Deprecated("Use Circle class, instead.", replaceWith = ReplaceWith("Shape.Circle")) + val CIRCLE = Circle + } + + object Square : Shape { + override fun draw( + canvas: Canvas, + paint: Paint, + size: Float + ) { + canvas.drawRect(0f, 0f, size, size, paint) + } + } + + class Rectangle( + /** The ratio of height to width. Must be within range [0, 1] */ + private val heightRatio: Float + ) : Shape { + init { + require(heightRatio in 0f..1f) + } + + override fun draw(canvas: Canvas, paint: Paint, size: Float) { + val height = size * heightRatio + val top = (size - height) / 2f + canvas.drawRect(0f, top, size, top + height, paint) + } + } + + object Circle : Shape { + private val rect = RectF() + + override fun draw( + canvas: Canvas, + paint: Paint, + size: Float + ) { + rect.set(0f, 0f, size, size) + canvas.drawOval(rect, paint) + } + } + + class DrawableShape( + drawable: Drawable, + /** Set to `false` to opt out of tinting the drawable, keeping its original colors. */ + private val tint: Boolean = true + ) : Shape { + private val drawable = drawable.mutate() + private val heightRatio = + if (drawable.intrinsicHeight == -1 && drawable.intrinsicWidth == -1) { + // If the drawable has no intrinsic size, fill the available space. + 1f + } else if (drawable.intrinsicHeight == -1 || drawable.intrinsicWidth == -1) { + // Currently cannot handle a drawable with only one intrinsic dimension. + 0f + } else { + drawable.intrinsicHeight.toFloat() / drawable.intrinsicWidth + } + + override fun draw(canvas: Canvas, paint: Paint, size: Float) { + if (tint) { + drawable.setColorFilter(paint.color, PorterDuff.Mode.SRC_IN) + } else { + drawable.alpha = paint.alpha + } + + val height = (size * heightRatio).toInt() + val top = ((size - height) / 2f).toInt() + + drawable.setBounds(0, top, size.toInt(), top + height) + drawable.draw(canvas) + } + } } From 427d0e6c190bb616c1c6d5eba9c0253eadf71b78 Mon Sep 17 00:00:00 2001 From: Dion Segijn Date: Sat, 7 Mar 2020 11:04:45 +0100 Subject: [PATCH 2/2] Update readme about DrawableShape --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 78741d22..b50b2c6d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ viewKonfetti.build() .setSpeed(1f, 5f) .setFadeOutEnabled(true) .setTimeToLive(2000L) - .addShapes(Shape.RECT, Shape.CIRCLE) + .addShapes(Shape.Square, Shape.Circle) .addSizes(Size(12)) .setPosition(-50f, viewKonfetti.width + 50f, -50f, -50f) .streamFor(300, 5000L) @@ -62,21 +62,25 @@ viewKonfetti.build() .setSpeed(1f, 5f) .setFadeOutEnabled(true) .setTimeToLive(2000L) - .addShapes(Shape.RECT, Shape.CIRCLE) + .addShapes(Shape.Square, Shape.Circle) .addSizes(new Size(12, 5)) .setPosition(-50f, viewKonfetti.getWidth() + 50f, -50f, -50f) .streamFor(300, 5000L) ``` -If you haven't configured Kotlin for your Java only project, add the following to your project: +### Custom shapes -`implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:$latest_version'` +Add a custom shape by using: -Read more about the latest version and kotlin via gradle here: https://kotlinlang.org/docs/reference/using-gradle.html +```Kotlin +Shape.DrawableShape(drawable: Drawable) +``` + +The 3D flip effect works best for symmetrical shapes, for example a drawable with a width and a height of 24x24. ## Download -Just add the following dependency in your app's build.gradle +Add the following dependency in your app's build.gradle ```groovy dependencies { @@ -85,6 +89,14 @@ dependencies { ``` [ ![Download](https://api.bintray.com/packages/danielmartinus/maven/Konfetti/images/download.svg) ](https://bintray.com/danielmartinus/maven/Konfetti/_latestVersion) +### Java project + +If you haven't configured Kotlin for your Java only project, add the following to your project: + +`implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:$latest_version'` + +Read more about the latest version and kotlin via gradle here: https://kotlinlang.org/docs/reference/using-gradle.html + ## Contribute There is always room for improvement. @@ -104,6 +116,6 @@ In line with the previous contribute section there are some already known issues - ~~Determining the size of the particles in the current implementation is not ideal. More here: [#7 Confetti size system](https://github.com/DanielMartinus/Konfetti/issues/7)~~ - A performance improvement to the library could for one be to implement a shared object pool amongst all particle systems instead of having them to handle confetti instances themselves. -## License +## License Konfetti is released under the ISC license. See [LICENSE](https://github.com/DanielMartinus/Konfetti/blob/master/LICENSE) for details.