Реалізація анімації Confetti/Celebration у мобільних додатках
Confetti-анімація потрібна там, де додаток повинен емоційно відреагувати на досягнення: перша покупка, виконана мета, завершений урок. Користувачі бачать, як частинки розлітаються — це активує психологію винагороди. Технічно, «багато рухомих об'єктів» становить потенційне навантаження на рендеринг.
iOS: CAEmitterLayer
CAEmitterLayer — правильний інструмент для particle effects на iOS. Рендеринг через Metal, анімація на render thread — головний потік не бере участі.
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"] // або UIImage для кастомних форм
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 // вниз
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 в кінці
return cell
}
}
view.layer.addSublayer(emitter)
// Зупиняємо spawning через 2 секунди, частинки рухаються самі
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
emitter.birthRate = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) {
emitter.removeFromSuperlayer()
}
}
CAEmitterCell.birthRate — частиц на секунду на ячейку. При 5 кольорах × 3 формах × 8 частиц/с = 120 частиц/с всього. При lifetime 4 секунди — до 480 частиц одночасно на екрані. На iPhone 12+ це комфортно. На iPhone SE 2nd gen починає гріватися. Зменшуємо birthRate до 4–5 для середньорівневих пристроїв.
Для кастомних форм confetti (прямокутники, зірки): створюємо через UIGraphicsImageRenderer, малюємо форму, конвертуємо в CGImage для cell.contents.
Android: Canvas-based або бібліотека
Кастомний ConfettiView через 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 // гравітація
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() — це перерисовка Canvas на кожному кадрі. При 80 частицах та кастомному onDraw — прийнятно. При 200+ частицах зі складними формами — переходимо на SurfaceView з окремим render thread.
Готова бібліотека: nl.dionsegijn:konfetti:2.0.4 — непоганий варіант, підтримує кастомні форми та KonfettiView.
Flutter
// Через 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;
}
У Flutter простіше використовувати готовий пакет confetti: ^0.7.0 — там ConfettiController з play(), stop() та параметрами для напрямку, кольорів, форм.
Терміни
Confetti через CAEmitterLayer або готову бібліотеку з базовими параметрами: 4–8 годин. Кастомна реалізація з унікальними формами, фізикою (вітер, гравітація) та оптимізацією під різні пристрої: 1–2 дні. Вартість розраховується індивідуально.







