Реализация анимации Confetti/Celebration в мобильном приложении
Confetti-анимация нужна там, где приложение должно эмоционально отреагировать на достижение: первая покупка, выполненная цель, завершённый урок. Пользователь видит разлетающиеся частицы — и это работает на уровне психологии вознаграждения. Но технически «много движущихся объектов» — потенциальная нагрузка на рендеринг.
iOS: CAEmitterLayer
CAEmitterLayer — правильный инструмент для particle effects на iOS. Рендеринг через Metal, анимация на render thread — main 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 частиц/с суммарно. На 4 секунды lifetime — до 480 частиц одновременно на экране. На iPhone 12+ — комфортно. На iPhone SE 2nd gen — начинает греться. Снижаем birthRate до 4–5 для mid-range устройств.
Для кастомных форм 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 дня. Стоимость рассчитывается индивидуально.







