Creating a Rainy Street Scene Animation in Flutter Using CustomPainter

Rainy street Animation in Flutter
Rainy street Animation in Flutter

Animations add life to your Flutter apps, and using CustomPainter allows you to design beautiful, layered visuals with control and efficiency.

In this tutorial, we will build a Rainy Street Scene animation in Flutter using CustomPainter, featuring:

✅ A twilight background with mountain reflections
✅ Animated raindrops
✅ Glowing streetlights with pulsing glow
✅ Random lightning flashes

This is an excellent project to practice advanced Flutter UI animations for your portfolio or YouTube channel.

Complete Source Code

Copy, paste, and run this in your Flutter environment:

import 'dart:async';
import 'dart:math' as math;
import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const RainyStreetScene(),
    );
  }
}

class RainyStreetScene extends StatefulWidget {
  const RainyStreetScene({super.key});
  @override
  State<RainyStreetScene> createState() => _RainyStreetSceneState();
}

class _RainyStreetSceneState extends State<RainyStreetScene>
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late AnimationController _bulbGlowController;

  late Timer _thunderTimer;
  late Animation<double> _glowRadius;

  bool _showLightning = false;

  @override
  void initState() {
    super.initState();
    _bulbGlowController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    )..repeat(reverse: true);
    _glowRadius = Tween<double>(begin: 20, end: 40).animate(
      CurvedAnimation(parent: _bulbGlowController, curve: Curves.easeInOut),
    );
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();
    _startThunderTimer();
  }

  void _startThunderTimer() {
    _thunderTimer = Timer.periodic(const Duration(seconds: 5), (_) {
      setState(() => _showLightning = true);
      Future.delayed(const Duration(milliseconds: 200), () {
        setState(() => _showLightning = false);
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _bulbGlowController.dispose();
    _thunderTimer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final random = Random();
    final double randomTop =
        random.nextDouble() * 300; // Adjust 300 as per screen height
    final double randomLeft = random.nextDouble() * 200; // Adjust 200 as per
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          CustomPaint(
            size: Size.infinite,
            painter: MountainReflectionPainter(),
          ),
          AnimatedBuilder(
            animation: Listenable.merge([_controller, _glowRadius]),
            builder: (_, __) {
              return CustomPaint(
                painter: StreetScenePainter(
                  rainProgress: _controller.value,
                  showLightning: _showLightning,
                  glowRadius: _glowRadius.value,
                ),
                child: Container(),
              );
            },
          ),
          // Thunder lightning flash with bolt
          if (_showLightning) ...[
            Positioned(
              top: randomTop,
              left: randomLeft,
              child: CustomPaint(
                size: Size(60, 120),
                painter: LightningPainter(),
              ),
            ),
          ],
        ],
      ),
    );
  }
}

class MountainReflectionPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint();

    // Draw background gradient (sunset)
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final gradient = LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: [
        // Ground dark
        Color(0xFF1E3C72), // Twilight blue
        Color(0xFF2A5298), // Cool night blue
        Color(0xFF000000),
      ],
    );
    paint.shader = gradient.createShader(rect);
    canvas.drawRect(rect, paint);

    // Mountain silhouette
    final mountainPath =
        Path()
          ..moveTo(0, size.height * 0.4)
          ..lineTo(size.width * 0.3, size.height * 0.25)
          ..lineTo(size.width * 0.5, size.height * 0.3)
          ..lineTo(size.width * 0.7, size.height * 0.2)
          ..lineTo(size.width, size.height * 0.35)
          ..lineTo(size.width, size.height * 0.5)
          ..lineTo(0, size.height * 0.5)
          ..close();

    paint.color = Colors.black87;
    canvas.drawPath(mountainPath, paint);

    // Reflection (flip the mountain)
    canvas.save();
    canvas.translate(0, size.height); // move to bottom
    canvas.scale(1, -1); // flip vertically

    paint.color = Colors.black38; // faded reflection
    canvas.drawPath(mountainPath.shift(Offset(0, size.height * 0.1)), paint);
    canvas.restore();

    // Add water effect (blue overlay)
    final waterRect = Rect.fromLTWH(
      0,
      size.height * 0.5,
      size.width,
      size.height * 0.5,
    );
    paint.shader = null;
    paint.color = Colors.blue.withOpacity(0.2);
    canvas.drawRect(waterRect, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

class LightningPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final path = Path();
    path.moveTo(size.width * 0.3, 0);
    path.lineTo(size.width * 0.6, size.height * 0.3);
    path.lineTo(size.width * 0.4, size.height * 0.3);
    path.lineTo(size.width * 0.7, size.height * 0.6);
    path.lineTo(size.width * 0.5, size.height * 0.6);
    path.lineTo(size.width * 0.8, size.height);

    // Draw glow by painting the same path multiple times with blur effect
    for (int i = 6; i >= 1; i--) {
      final glowPaint =
          Paint()
            ..color = Colors.white.withOpacity(0.02 * i)
            ..strokeWidth = 4.0 * i
            ..style = PaintingStyle.stroke
            ..maskFilter = MaskFilter.blur(BlurStyle.normal, 4.0 * i);
      canvas.drawPath(path, glowPaint);
    }

    // Draw main lightning stroke
    final paint =
        Paint()
          ..color = Colors.white
          ..strokeWidth = 4
          ..style = PaintingStyle.stroke
          ..strokeCap = StrokeCap.round;

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

class StreetScenePainter extends CustomPainter {
  final double rainProgress;
  final bool showLightning;
  final Random _rand = Random();
  final double glowRadius;

  StreetScenePainter({
    required this.rainProgress,
    required this.showLightning,
    required this.glowRadius,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint();

    // Background sky
    paint.color = showLightning ? Colors.white54 : Colors.transparent;
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);

    // Road
    paint.color = Colors.black87;
    canvas.drawRect(
      Rect.fromLTWH(0, size.height, size.width, size.height * 0.25),
      paint,
    );

    // Streetlights
    for (int i = 0; i < 5; i++) {
      final scale = 1.2 - i * 0.2;
      final dx = size.width * 0.15 + i * size.width * 0.17;
      _drawStreetLight(canvas, dx, size.height * 0.8, scale);
    }

    // Raindrops
    for (int i = 0; i < 100; i++) {
      final x = _rand.nextDouble() * size.width;
      final y = (_rand.nextDouble() + rainProgress) % 1 * size.height;
      paint.color = Colors.blueGrey.shade200.withOpacity(0.4);
      paint.strokeWidth = 1;
      canvas.drawLine(Offset(x, y), Offset(x, y + 10), paint);
    }
  }

  void _drawStreetLight(Canvas canvas, double x, double y, double scale) {
    final paint = Paint()..color = Colors.black;
    final path = Path();

    // Pole
    final poleHeight = 250.0 * scale;
    final poleWidth = 4.0 * scale;
    canvas.drawRect(
      Rect.fromLTWH(x, y - poleHeight, poleWidth, poleHeight),
      paint,
    );

    // Arc arm
    final arcPath = Path();
    arcPath.moveTo(x + poleWidth / 2, y - poleHeight);
    arcPath.relativeQuadraticBezierTo(0, -30 * scale, 20 * scale, -30 * scale);
    arcPath.relativeLineTo(0, 20 * scale);
    canvas.drawPath(arcPath, paint);

    // Bulb box
    final bulbX = x + poleWidth / 2 + 18 * scale;
    final bulbY = y - poleHeight - 10 * scale;
    paint.color = Colors.grey.shade800;
    canvas.drawRect(Rect.fromLTWH(bulbX, bulbY, 15 * scale, 15 * scale), paint);

    // Glow
    final glowPaint =
        Paint()
          ..shader = RadialGradient(
            colors: [Colors.yellow.withOpacity(0.6), Colors.transparent],
          ).createShader(
            Rect.fromCircle(
              center: Offset(bulbX + 7.5 * scale, bulbY + 7.5 * scale),
              radius: glowRadius * scale,
            ),
          );
    canvas.drawCircle(
      Offset(bulbX + 7.5 * scale, bulbY + 7.5 * scale),
      glowRadius * scale,
      glowPaint,
    );

    // Reflection on road
    final reflectionPaint =
        Paint()
          ..shader = LinearGradient(
            colors: [Colors.yellow.withOpacity(0.3), Colors.transparent],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ).createShader(
            Rect.fromLTWH(bulbX + 5 * scale, y, 5 * scale, 50 * scale),
          );
    canvas.drawRect(
      Rect.fromLTWH(bulbX + 5 * scale, y, 5 * scale, 50 * scale),
      reflectionPaint,
    );
  }

  @override
  bool shouldRepaint(covariant StreetScenePainter oldDelegate) => true;
}

Explanation of Key Components

We will now break down each part for clear understanding:

1️⃣ Main Structure

void main() => runApp(const MyApp());

2️⃣ RainyStreetScene Widget

A StatefulWidget that:
✅ Initializes animation controllers for rain and glow
✅ Starts a timer for periodic lightning flashes
✅ Uses a Stack to layer:

  • The mountain background
  • The animated rain and streetlights
  • Random lightning strikes

The AnimationController runs continuously to animate raindrops, while another controller animates the pulsing glow of the streetlights.

A timer triggers the _showLightning flag every 5 seconds, briefly displaying a lightning flash.

3️⃣ MountainReflectionPainter

Uses CustomPainter to:
✅ Draw a gradient twilight sky using LinearGradient.
✅ Create black mountain silhouettes.
✅ Flip and fade the mountains below to simulate water reflection.
✅ Add a subtle blue overlay to mimic water.

This painter lays the static background of your scene.

4️⃣ StreetScenePainter

This painter adds:
Glowing streetlights using radial gradients for glow.
Streetlight reflections on the wet road using a linear gradient.
Continuous raindrops drawn with canvas.drawLine at random positions, animated with the rain progress.
✅ A lightning flash overlay (a translucent white overlay) when lightning occurs.

It uses:

  • AnimationController for smooth rain animation.
  • The glowRadius animation for the pulsing glow effect on streetlights.

5️⃣ LightningPainter

  • Creates a zigzag lightning bolt using Path:
  • Uses layered blurred strokes for glow.
  • Uses a white stroke for the main bolt, making it pop during flashes.

6️⃣ Animation Details

  • Rain Animation: Uses a 2-second repeating AnimationController to move raindrops from top to bottom, creating a seamless rain effect.
  • Glow Animation: Pulses the glow of streetlights between 20 and 40 radius smoothly using CurvedAnimation.
  • Lightning Timer: Every 5 seconds, _showLightning becomes true for 200ms, displaying a lightning flash and bolt at a random position.

Final Output

The animation will showcase:

  • A night sky with mountains reflected in water.
  • Continuous rain falling smoothly.
  • Streetlights glowing gently.
  • Lightning flashes randomly, illuminating the scene.

Why Use CustomPainter?

✅ Allows low-level drawing control for custom, layered visuals.
✅ Offers better performance for animations compared to stacking many widgets.
✅ Enables creative, unique UIs for your Flutter apps.

Customization Ideas

✅ Change the background colors for different moods (sunset, dawn, midnight rain).
✅ Add thunder and rain sound for realism.
✅ Adjust lightning frequency for heavy storms.
✅ Replace streetlights with lanterns or traffic lights for city scenes.

Final Thoughts

This project will help you learn:
✅ Layering animations and custom painters
✅ Using AnimationController with CustomPainter
✅ Adding glow and reflection for realism
✅ Building complex animations efficiently

Once you understand this structure, you can create advanced, aesthetic Flutter animations that make your apps stand out.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *