Confetti/Celebration Animation Implementation in Mobile Apps
Confetti animation is needed when the app should emotionally respond to achievements: first purchase, completed goal, finished lesson. Users see particles flying apart—this activates reward psychology. Technically, "many moving objects" poses potential rendering load.
iOS: CAEmitterLayer
CAEmitterLayer is the right tool for particle effects on iOS. Rendering through Metal, animation on render thread—main thread doesn't participate.
func startConfetti(in view: UIView) {
let emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: view.bounds.midX, y: -10)
emitter.emitterShape = .line
emitter.emitterSize = CGSize(width: view.bounds.width, height: 0)
let colors: [UIColor] = [.systemRed, .systemBlue, .systemYellow, .systemGreen, .systemPurple]
let shapes = ["square", "circle", "triangle"] // or UIImage for custom shapes
emitter.emitterCells = colors.flatMap { color in
shapes.map { _ in
let cell = CAEmitterCell()
cell.contents = UIImage(systemName: "circle.fill")?.withTintColor(color).cgImage
cell.birthRate = 8
cell.lifetime = 4.0
cell.velocity = 200
cell.velocityRange = 100
cell.emissionLongitude = .pi // downward
cell.emissionRange = .pi / 4
cell.spin = 3.5
cell.spinRange = 1.0
cell.scaleRange = 0.5
cell.scale = 0.4
cell.color = color.cgColor
cell.alphaSpeed = -0.15 // fade out at end
return cell
}
}
view.layer.addSublayer(emitter)
// Stop spawning after 2 seconds; particles drift on their own
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
emitter.birthRate = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) {
emitter.removeFromSuperlayer()
}
}
CAEmitterCell.birthRate is particles per second per cell. With 5 colors × 3 shapes × 8 particles/s = 120 particles/s total. At 4 seconds lifetime, up to 480 particles on screen simultaneously. On iPhone 12+ this is comfortable. On iPhone SE 2nd gen it starts to heat up. Lower birthRate to 4–5 for mid-range devices.
For custom confetti shapes (rectangles, stars): create via UIGraphicsImageRenderer, draw the shape, convert to CGImage for cell.contents.
Android: Canvas-based or Library
Custom ConfettiView via Canvas:
class ConfettiView(context: Context) : View(context) {
private val particles = mutableListOf<ConfettiParticle>()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var animator: ValueAnimator? = null
data class ConfettiParticle(
var x: Float, var y: Float,
var vx: Float, var vy: Float,
val color: Int,
val size: Float,
var rotation: Float,
val rotationSpeed: Float,
var alpha: Float = 1f
)
fun start() {
repeat(80) {
particles.add(ConfettiParticle(
x = Random.nextFloat() * width,
y = -Random.nextFloat() * 100,
vx = Random.nextFloat() * 6 - 3,
vy = Random.nextFloat() * 4 + 3,
color = listOf(Color.RED, Color.BLUE, Color.YELLOW, Color.GREEN).random(),
size = Random.nextFloat() * 12 + 6,
rotation = Random.nextFloat() * 360,
rotationSpeed = Random.nextFloat() * 6 - 3
))
}
animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 5000
addUpdateListener {
updateParticles()
invalidate()
}
start()
}
}
private fun updateParticles() {
particles.forEach { p ->
p.x += p.vx
p.vy += 0.1f // gravity
p.y += p.vy
p.rotation += p.rotationSpeed
if (p.y > height * 0.7f) p.alpha -= 0.02f
}
particles.removeAll { it.alpha <= 0 || it.y > height + 50 }
}
override fun onDraw(canvas: Canvas) {
particles.forEach { p ->
paint.color = p.color
paint.alpha = (p.alpha * 255).toInt()
canvas.save()
canvas.translate(p.x, p.y)
canvas.rotate(p.rotation)
canvas.drawRect(-p.size/2, -p.size/2, p.size/2, p.size/2, paint)
canvas.restore()
}
}
}
ValueAnimator + invalidate() redraws Canvas each frame. With 80 particles and custom onDraw, this is acceptable. With 200+ particles with complex shapes, switch to SurfaceView with separate render thread.
Ready-made library: nl.dionsegijn:konfetti:2.0.4—decent, supports custom shapes and KonfettiView.
Flutter
// Via CustomPainter
class ConfettiPainter extends CustomPainter {
final List<ConfettiParticle> particles;
ConfettiPainter(this.particles);
@override
void paint(Canvas canvas, Size size) {
for (final p in particles) {
final paint = Paint()..color = p.color.withOpacity(p.alpha);
canvas.save();
canvas.translate(p.x, p.y);
canvas.rotate(p.rotation);
canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size, height: p.size * 0.5), paint);
canvas.restore();
}
}
@override
bool shouldRepaint(ConfettiPainter old) => true;
}
In Flutter, using the ready-made package confetti: ^0.7.0 is simpler—it has ConfettiController with play(), stop(), and parameters for direction, colors, shapes.
Timeline
Confetti via CAEmitterLayer or ready library with basic parameters: 4–8 hours. Custom implementation with unique shapes, physics (wind, gravity), and optimization for different devices: 1–2 days. Cost is calculated individually.







