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

A customizable bottom sheet with Hero animation support, implementing all features of showModalBottomSheet.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:dupe_modal_bottom_sheet/dupe_modal_bottom_sheet.dart';

void main() {
  runApp(const MyApp());
}

/// Root application widget.
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

/// Home page demonstrating the usage of DupeModalBottomSheet.
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Example')),
      body: Center(
        child: Column(
          spacing: 12,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                showDupeModalBottomSheet(
                  context: context,
                  useSafeArea: true,
                  isScrollControlled: true,
                  // Configure rounded corners and clipping
                  shape: const RoundedRectangleBorder(
                    borderRadius: BorderRadius.vertical(
                      top: Radius.circular(20),
                    ),
                  ),
                  clipBehavior: Clip.antiAlias,
                  builder: (context) => const GalleryPage(),
                );
              },
              child: const Text('showDupeModalBottomSheet'),
            ),
            ElevatedButton(
              onPressed: () {
                showModalBottomSheet(
                  context: context,
                  useSafeArea: true,
                  isScrollControlled: true,
                  // Configure rounded corners and clipping
                  shape: const RoundedRectangleBorder(
                    borderRadius: BorderRadius.vertical(
                      top: Radius.circular(20),
                    ),
                  ),
                  clipBehavior: Clip.antiAlias,
                  builder: (context) => const GalleryPage(),
                );
              },
              child: const Text('showModalBottomSheet'),
            ),
          ],
        ),
      ),
    );
  }
}

/// Gallery page displaying a grid of images with Hero animation support.
class GalleryPage extends StatelessWidget {
  const GalleryPage({super.key});

  /// Sample image URLs for demonstration.
  static const List<String> images = [
    'https://ak-d.tripcdn.com/images/1lo6h12000cjjzfat9B44_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/01066120008ro8p3n4406_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/1lo0g12000cjsyzga0E7B_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/0106p120008ro8sd3B5B2_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/1lo5912000cjjyzw4303D_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/350r190000016fjbi68C7_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/1lo0y12000cjjzdxn9694_C_1200_800_Q70.webp',
    'https://ak-d.tripcdn.com/images/0100p120008rn6wly74FD_C_1200_800_Q70.webp',
  ];

  @override
  Widget build(BuildContext context) {
    return FractionallySizedBox(
      heightFactor: 0.9,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Gallery'),
          leading: IconButton(
            icon: const Icon(Icons.close),
            onPressed: () => Navigator.pop(context),
          ),
        ),
        body: SafeArea(
          minimum: const EdgeInsets.all(20),
          child: GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 20,
              mainAxisSpacing: 20,
            ),
            itemCount: images.length,
            itemBuilder: (context, index) {
              final imageUrl = images[index];
              return GestureDetector(
                onTap: () {
                  Navigator.push(
                    context,
                    PageRouteBuilder(
                      opaque: false,
                      barrierColor: Colors.transparent,
                      pageBuilder: (context, _, __) => PreviewPage(
                        imageUrl: imageUrl,
                        heroTag: 'hero_$index',
                      ),
                    ),
                  );
                },
                child: Hero(
                  tag: 'hero_$index',
                  placeholderBuilder: (context, heroSize, child) {
                    return Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(12),
                        color: Colors.grey[300],
                      ),
                    );
                  },
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(12),
                    child: Image.network(imageUrl, fit: BoxFit.cover),
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

/// Full-screen image preview page with interactive dismiss gestures.
///
/// Supports vertical drag to dismiss with scale and opacity animations.
class PreviewPage extends StatefulWidget {
  /// The URL of the image to display.
  final String imageUrl;

  /// The Hero tag for the shared element transition.
  final String heroTag;

  const PreviewPage({super.key, required this.imageUrl, required this.heroTag});

  @override
  State<PreviewPage> createState() => _PreviewPageState();
}

class _PreviewPageState extends State<PreviewPage> {
  /// Current vertical drag offset.
  double _dragOffset = 0;

  /// Background opacity based on drag distance.
  double _backgroundOpacity = 1.0;

  /// Image scale based on drag distance.
  double _scale = 1.0;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  /// Handles vertical drag updates to animate opacity and scale.
  void _onVerticalDragUpdate(DragUpdateDetails details) {
    setState(() {
      _dragOffset += details.delta.dy;
      if (_dragOffset < 0) _dragOffset = 0;
      _backgroundOpacity = (1 - (_dragOffset / 400)).clamp(0.0, 1.0);
      _scale = (1 - (_dragOffset / 1000)).clamp(0.5, 1.0);
    });
  }

  /// Handles drag end to dismiss or reset the preview.
  void _onVerticalDragEnd(DragEndDetails details) {
    if (_dragOffset > 100) {
      Navigator.pop(context);
    } else {
      setState(() {
        _dragOffset = 0;
        _backgroundOpacity = 1.0;
        _scale = 1.0;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black.withValues(alpha: _backgroundOpacity),
      body: GestureDetector(
        onVerticalDragUpdate: _onVerticalDragUpdate,
        onVerticalDragEnd: _onVerticalDragEnd,
        child: Stack(
          children: [
            Center(
              child: Transform.translate(
                offset: Offset(0, _dragOffset),
                child: Transform.scale(
                  scale: _scale,
                  child: Hero(
                    tag: widget.heroTag,
                    child: Image.network(widget.imageUrl),
                  ),
                ),
              ),
            ),
            if (_dragOffset == 0)
              Positioned(
                top: MediaQuery.of(context).padding.top + 10,
                left: 10,
                child: IconButton(
                  icon: const Icon(Icons.close, color: Colors.white, size: 30),
                  onPressed: () => Navigator.pop(context),
                ),
              ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
41
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A customizable bottom sheet with Hero animation support, implementing all features of showModalBottomSheet.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on dupe_modal_bottom_sheet