morphing_sheet 0.1.0 copy "morphing_sheet: ^0.1.0" to clipboard
morphing_sheet: ^0.1.0 copied to clipboard

A production-ready multi-stage, gesture-driven, spatially continuous UX sheet with physics-based snapping and morphing fullscreen transitions.

example/lib/main.dart

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

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

class MorphingSheetExampleApp extends StatelessWidget {
  const MorphingSheetExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Morphing Sheet Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF6750A4),
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: const Color(0xFF6750A4),
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      home: const DemoPage(),
    );
  }
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage>
    with SingleTickerProviderStateMixin {
  late final SheetController _controller;

  static const _config = SheetConfig(
    snapPoints: [
      SnapPoint(position: 0.25, label: 'collapsed'),
      SnapPoint(position: 0.6, label: 'half'),
      SnapPoint(position: 1.0, label: 'expanded', enableHorizontalSwipe: false),
    ],
    cornerRadius: 28,
    elevation: 12,
    backgroundMinScale: 0.92,
    backgroundMaxBlur: 8,
  );

  static const _pages = ['Explore', 'Trending', 'Library'];

  @override
  void initState() {
    super.initState();
    _controller = SheetController(vsync: this, config: _config);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: MorphingSheet(
        controller: _controller,
        config: _config,
        pageCount: _pages.length,
        headerBuilder: _buildHeader,
        contentBuilder: _buildContent,
        onPageChanged: (index) {},
        child: const _BackgroundContent(),
      ),
    );
  }

  Widget _buildHeader(
    BuildContext context,
    double progress,
    SheetState state,
  ) {
    final theme = Theme.of(context);
    final normalized = SheetTween.normalize(
      progress,
      _config.snapPoints.first.position,
      _config.snapPoints.last.position,
    );

    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
        child: Column(
          children: [
            // Drag handle
            Center(
              child: Container(
                width: 36,
                height: 4,
                decoration: BoxDecoration(
                  color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Title row
            Row(
              children: [
                Text(
                  _pages[state.selectedIndex],
                  style: TextStyle.lerp(
                    theme.textTheme.titleMedium,
                    theme.textTheme.headlineSmall,
                    normalized,
                  ),
                ),
                const Spacer(),
                // Control buttons
                _ControlButton(
                  icon: Icons.expand_less,
                  onTap: _controller.expand,
                  tooltip: 'Expand',
                ),
                const SizedBox(width: 4),
                _ControlButton(
                  icon: Icons.expand_more,
                  onTap: _controller.collapse,
                  tooltip: 'Collapse',
                ),
              ],
            ),

            // Page indicator (fades in as sheet expands)
            if (normalized > 0.1)
              Opacity(
                opacity: normalized.clamp(0.0, 1.0),
                child: Padding(
                  padding: const EdgeInsets.only(top: 12),
                  child: Row(
                    children: List.generate(_pages.length, (i) {
                      final isActive = i == state.selectedIndex;
                      return Padding(
                        padding: const EdgeInsets.only(right: 8),
                        child: AnimatedContainer(
                          duration: const Duration(milliseconds: 200),
                          width: isActive ? 24 : 8,
                          height: 8,
                          decoration: BoxDecoration(
                            color: isActive
                                ? theme.colorScheme.primary
                                : theme.colorScheme.surfaceContainerHighest,
                            borderRadius: BorderRadius.circular(4),
                          ),
                        ),
                      );
                    }),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildContent(
    BuildContext context,
    int index,
    double progress,
    SheetState state,
  ) {
    final theme = Theme.of(context);
    final items = _generateItems(index);

    return ListView.builder(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      physics: const BouncingScrollPhysics(),
      itemCount: items.length,
      itemBuilder: (context, i) {
        return Card(
          margin: const EdgeInsets.only(bottom: 8),
          child: ListTile(
            leading: CircleAvatar(
              backgroundColor: theme.colorScheme.primaryContainer,
              child: Text(
                '${i + 1}',
                style: TextStyle(color: theme.colorScheme.onPrimaryContainer),
              ),
            ),
            title: Text(items[i]),
            subtitle: Text('${_pages[index]} item ${i + 1}'),
            trailing: Icon(
              Icons.chevron_right,
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        );
      },
    );
  }

  List<String> _generateItems(int pageIndex) {
    switch (pageIndex) {
      case 0:
        return List.generate(20, (i) => 'Discovery Item ${i + 1}');
      case 1:
        return List.generate(15, (i) => 'Trending Topic ${i + 1}');
      case 2:
        return List.generate(25, (i) => 'Library Entry ${i + 1}');
      default:
        return [];
    }
  }
}

class _ControlButton extends StatelessWidget {
  const _ControlButton({
    required this.icon,
    required this.onTap,
    required this.tooltip,
  });

  final IconData icon;
  final VoidCallback onTap;
  final String tooltip;

  @override
  Widget build(BuildContext context) {
    return Tooltip(
      message: tooltip,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(20),
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: Icon(icon, size: 20),
        ),
      ),
    );
  }
}

class _BackgroundContent extends StatelessWidget {
  const _BackgroundContent();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            theme.colorScheme.primaryContainer,
            theme.colorScheme.surface,
          ],
        ),
      ),
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Morphing Sheet', style: theme.textTheme.headlineLarge),
              const SizedBox(height: 8),
              Text(
                'Drag the sheet up and down, or use the buttons.\n'
                'Swipe horizontally between pages when not fully expanded.',
                style: theme.textTheme.bodyLarge?.copyWith(
                  color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
                ),
              ),
              const SizedBox(height: 32),
              Wrap(
                spacing: 12,
                runSpacing: 12,
                children: List.generate(
                  6,
                  (i) => Container(
                    width: 100,
                    height: 100,
                    decoration: BoxDecoration(
                      color: theme.colorScheme.secondaryContainer,
                      borderRadius: BorderRadius.circular(16),
                    ),
                    alignment: Alignment.center,
                    child: Icon(
                      _bgIcons[i % _bgIcons.length],
                      size: 32,
                      color: theme.colorScheme.onSecondaryContainer,
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  static const _bgIcons = [
    Icons.shopping_bag_outlined,
    Icons.music_note_outlined,
    Icons.dashboard_outlined,
    Icons.explore_outlined,
    Icons.auto_awesome_outlined,
    Icons.bar_chart_outlined,
  ];
}
3
likes
0
points
233
downloads

Publisher

unverified uploader

Weekly Downloads

A production-ready multi-stage, gesture-driven, spatially continuous UX sheet with physics-based snapping and morphing fullscreen transitions.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on morphing_sheet