story_progress_indicator 1.0.0 copy "story_progress_indicator: ^1.0.0" to clipboard
story_progress_indicator: ^1.0.0 copied to clipboard

A zero-dependency Flutter package providing a Stories-style segmented progress indicator. Highly customizable, responsive, and driven by a single progress value for use with PageView, timed stories, o [...]

example/lib/main.dart

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:story_progress_indicator/story_indicator.dart';

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

class _Story {
  const _Story({
    required this.username,
    required this.timeAgo,
    required this.gradientColors,
    required this.headline,
    required this.caption,
    required this.ctaLabel,
    required this.avatarEmoji,
  });

  final String username;
  final String timeAgo;
  final List<Color> gradientColors;
  final String headline;
  final String caption;
  final String ctaLabel;
  final String avatarEmoji;
}

/// Application widget. Public for testing.
class StoryIndicatorExampleApp extends StatelessWidget {
  const StoryIndicatorExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Story Indicator Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(useMaterial3: true),
      home: const _StoriesViewerPage(),
    );
  }
}

class _StoriesViewerPage extends StatefulWidget {
  const _StoriesViewerPage();

  @override
  State<_StoriesViewerPage> createState() => _StoriesViewerPageState();
}

class _StoriesViewerPageState extends State<_StoriesViewerPage>
    with TickerProviderStateMixin {
  static const Duration _segmentDuration = Duration(seconds: 5);

  static final List<_Story> _stories = <_Story>[
    const _Story(
      username: '@aurora',
      timeAgo: 'just now',
      gradientColors: [
        Color(0xFF1a0a2e),
        Color(0xFF16213e),
        Color(0xFF0f3460),
      ],
      headline: 'Northern Lights Tonight',
      caption:
          'Rare aurora borealis visible across the northern hemisphere',
      ctaLabel: 'Set Reminder',
      avatarEmoji: '🌌',
    ),
    const _Story(
      username: '@wavelength',
      timeAgo: '2m ago',
      gradientColors: [
        Color(0xFF0d3b3b),
        Color(0xFF0f4c4c),
        Color(0xFF1a5f5f),
      ],
      headline: 'New Drop: Vol. 12',
      caption: 'Limited vinyl pressing. Only 200 copies worldwide.',
      ctaLabel: 'Shop Now',
      avatarEmoji: '🎵',
    ),
    const _Story(
      username: '@solstice',
      timeAgo: '5m ago',
      gradientColors: [
        Color(0xFF8B4513),
        Color(0xFFCD5C5C),
        Color(0xFF8B0000),
      ],
      headline: 'Golden Hour Hike',
      caption: 'Join our sunrise trail run this Saturday at 5:30am',
      ctaLabel: 'RSVP Free',
      avatarEmoji: '🏔️',
    ),
    const _Story(
      username: '@neoncraft',
      timeAgo: '12m ago',
      gradientColors: [
        Color(0xFF0a0a1a),
        Color(0xFF1a1a3e),
        Color(0xFF4a0a6a),
      ],
      headline: 'Behind the Build',
      caption:
          'How we designed our new studio from scratch in 30 days',
      ctaLabel: 'Watch Now',
      avatarEmoji: '⚡',
    ),
  ];

  late final AnimationController _timerController;
  int _currentStoryIndex = 0;

  @override
  void initState() {
    super.initState();
    _timerController = AnimationController(
      vsync: this,
      duration: _segmentDuration,
    )..addStatusListener(_onTimerStatus);
    _timerController.forward();
  }

  void _onTimerStatus(AnimationStatus status) {
    if (status != AnimationStatus.completed) return;
    _timerController.removeStatusListener(_onTimerStatus);
    _advanceStory();
    _timerController.addStatusListener(_onTimerStatus);
  }

  void _advanceStory() {
    final next = _currentStoryIndex + 1;
    setState(() {
      _currentStoryIndex = next >= _stories.length ? 0 : next;
    });
    _timerController.forward(from: 0.0);
  }

  void _goToStory(int index) {
    if (index == _currentStoryIndex) return;
    setState(() => _currentStoryIndex = index.clamp(0, _stories.length - 1));
    _timerController.forward(from: 0.0);
  }

  @override
  void dispose() {
    _timerController.removeStatusListener(_onTimerStatus);
    _timerController.dispose();
    super.dispose();
  }

  double get _progress =>
      _currentStoryIndex.toDouble() + _timerController.value;

  @override
  Widget build(BuildContext context) {
    final story = _stories[_currentStoryIndex];
    final size = MediaQuery.sizeOf(context);
    const leftTapFraction = 0.35;

    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: <Widget>[
          // [0] Full-screen gradient background
          Positioned.fill(
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 400),
              switchInCurve: Curves.easeOut,
              switchOutCurve: Curves.easeIn,
              transitionBuilder: (Widget child, Animation<double> animation) {
                return FadeTransition(
                  opacity: animation,
                  child: child,
                );
              },
              child: Container(
                key: ValueKey<int>(_currentStoryIndex),
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: story.gradientColors,
                  ),
                ),
              ),
            ),
          ),
          // [1] Decorative blurred background circle
          Positioned(
            top: size.height * 0.25,
            right: -size.width * 0.3,
            child: IgnorePointer(
              child: Container(
                width: size.width * 0.9,
                height: size.width * 0.9,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.white.withValues(alpha: 0.15),
                ),
                child: BackdropFilter(
                  filter: ImageFilter.blur(sigmaX: 60, sigmaY: 60),
                  child: const SizedBox.expand(),
                ),
              ),
            ),
          ),
          // [2] SafeArea + content
          SafeArea(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                const SizedBox(height: 12),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: AnimatedBuilder(
                    animation: _timerController,
                    builder: (BuildContext context, Widget? child) {
                      return StoryIndicator(
                        count: _stories.length,
                        progress: _progress,
                        trackColor: const Color(0x55FFFFFF),
                        fillColor: Colors.white,
                        height: 3,
                        gap: 4,
                        radius: const BorderRadius.all(Radius.circular(100)),
                      );
                    },
                  ),
                ),
                const SizedBox(height: 12),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: Row(
                    children: <Widget>[
                      _FrostedAvatar(emoji: story.avatarEmoji),
                      const SizedBox(width: 10),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            Text(
                              story.username,
                              style: const TextStyle(
                                color: Colors.white,
                                fontWeight: FontWeight.bold,
                                fontSize: 15,
                              ),
                            ),
                            Text(
                              story.timeAgo,
                              style: TextStyle(
                                color: Colors.grey.shade400,
                                fontSize: 13,
                              ),
                            ),
                          ],
                        ),
                      ),
                      IconButton(
                        onPressed: () => Navigator.of(context).pop(),
                        icon: const Icon(Icons.close, color: Colors.white),
                        style: IconButton.styleFrom(
                          backgroundColor: Colors.white24,
                        ),
                      ),
                    ],
                  ),
                ),
                const Spacer(),
                Padding(
                  padding: const EdgeInsets.fromLTRB(20, 0, 20, 32),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        story.headline,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 32,
                          fontWeight: FontWeight.w800,
                          shadows: <Shadow>[
                            Shadow(
                              color: Color(0x80000000),
                              offset: Offset(0, 2),
                              blurRadius: 4,
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        story.caption,
                        style: const TextStyle(
                          color: Colors.white70,
                          fontSize: 15,
                        ),
                      ),
                      const SizedBox(height: 24),
                      Material(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(100),
                        child: InkWell(
                          onTap: () {},
                          borderRadius: BorderRadius.circular(100),
                          child: Padding(
                            padding: const EdgeInsets.symmetric(
                              horizontal: 28,
                              vertical: 14,
                            ),
                            child: Text(
                              story.ctaLabel,
                              style: const TextStyle(
                                color: Color(0xFF1a1a1a),
                                fontWeight: FontWeight.w600,
                                fontSize: 15,
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          // [3] Tap zones
          Positioned(
            left: 0,
            top: 0,
            bottom: 0,
            width: size.width * leftTapFraction,
            child: GestureDetector(
              onTap: () {
                if (_currentStoryIndex > 0) {
                  _goToStory(_currentStoryIndex - 1);
                }
              },
              behavior: HitTestBehavior.opaque,
              child: const SizedBox.expand(),
            ),
          ),
          Positioned(
            left: size.width * leftTapFraction,
            top: 0,
            right: 0,
            bottom: 0,
            child: GestureDetector(
              onTap: () {
                if (_currentStoryIndex < _stories.length - 1) {
                  _goToStory(_currentStoryIndex + 1);
                } else {
                  _advanceStory();
                }
              },
              behavior: HitTestBehavior.opaque,
              child: const SizedBox.expand(),
            ),
          ),
        ],
      ),
    );
  }
}

class _FrostedAvatar extends StatelessWidget {
  const _FrostedAvatar({required this.emoji});

  final String emoji;

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(21),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
        child: Container(
          width: 42,
          height: 42,
          decoration: BoxDecoration(
            color: Colors.white.withValues(alpha: 0.2),
            borderRadius: BorderRadius.circular(21),
            border: Border.all(color: Colors.white24, width: 1),
          ),
          alignment: Alignment.center,
          child: Text(emoji, style: const TextStyle(fontSize: 22)),
        ),
      ),
    );
  }
}
3
likes
0
points
142
downloads

Publisher

verified publisherqasimerror.com

Weekly Downloads

A zero-dependency Flutter package providing a Stories-style segmented progress indicator. Highly customizable, responsive, and driven by a single progress value for use with PageView, timed stories, or swipeable content.

Repository (GitHub)
View/report issues

Topics

#stories #indicator #progress #animation #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on story_progress_indicator