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

Add support for custom shapes #129

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Shape> = arrayOf(Shape.RECT, Shape.CIRCLE)
var shapes: Array<Shape> = arrayOf(Shape.Square, Shape.Circle)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Shape>) {
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions demo-google-play/src/main/res/drawable/ic_heart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 10 additions & 20 deletions konfetti/src/main/java/nl/dionsegijn/konfetti/Confetti.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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())
DanielMartinus marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Shape> = arrayOf(Shape.Square)
private var confettiConfig = ConfettiConfig()

/**
Expand Down
103 changes: 94 additions & 9 deletions konfetti/src/main/java/nl/dionsegijn/konfetti/models/Shape.kt
Original file line number Diff line number Diff line change
@@ -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
DanielMartinus marked this conversation as resolved.
Show resolved Hide resolved

@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
DanielMartinus marked this conversation as resolved.
Show resolved Hide resolved
) : 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
DanielMartinus marked this conversation as resolved.
Show resolved Hide resolved
) : 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
DanielMartinus marked this conversation as resolved.
Show resolved Hide resolved
}

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

drawable.setBounds(0, top, size.toInt(), top + height)
drawable.draw(canvas)
}
}
}