skip to Main Content

In the Telegram app, an interesting feature has been implemented: it contains a BottomSheet that, when swiped up, increases the height of its header as it reaches a specific distance from the top and displays an AppBar.

I wasn’t able to implement this behavior using DraggableScrollableSheet and ChatGPT. Below, I have provided a sample code along with an image illustrating what I have in mind.

enter image description here

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Draggable Bottom Sheet',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DraggableBottomSheetExample(),
    );
  }
}

class DraggableBottomSheetExample extends StatefulWidget {
  @override
  _DraggableBottomSheetExampleState createState() =>
      _DraggableBottomSheetExampleState();
}

class _DraggableBottomSheetExampleState
    extends State<DraggableBottomSheetExample> with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _textSizeAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 300),
    );
    _textSizeAnimation = Tween<double>(begin: 24.0, end: 48.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );
  }

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

  void _onScroll(double offset) {
    if (offset >= 0.8 && !_controller.isAnimating && !_controller.isCompleted) {
      _controller.forward();
    } else if (offset < 0.8 && !_controller.isAnimating && _controller.isCompleted) {
      _controller.reverse();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Draggable Bottom Sheet Example'),
      ),
      body: Stack(
        children: <Widget>[
          Center(
            child: ElevatedButton(
              onPressed: () {
                showModalBottomSheet(
                  context: context,
                  isScrollControlled: true,
                  builder: (BuildContext context) {
                    return DraggableScrollableSheet(
                      initialChildSize: 0.3,
                      minChildSize: 0.1,
                      maxChildSize: 0.8,
                      builder: (context, scrollController) {
                        scrollController.addListener(() {
                          _onScroll(scrollController.position.pixels /
                              scrollController.position.maxScrollExtent);
                        });

                        return Container(
                          color: Colors.blueGrey[200],
                          child: SingleChildScrollView(
                            controller: scrollController,
                            child: Padding(
                              padding: const EdgeInsets.all(16.0),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  AnimatedBuilder(
                                    animation: _controller,
                                    builder: (context, child) {
                                      return Text(
                                        'Draggable Bottom Sheet',
                                        style: TextStyle(
                                          fontSize: _textSizeAnimation.value,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      );
                                    },
                                  ),
                                  SizedBox(height: 16),
                                  Text(
                                    'Swipe up to expand or down to collapse.',
                                    style: TextStyle(fontSize: 16),
                                  ),
                                  SizedBox(height: 16),
                                  // Add more content here
                                  Container(
                                    height: 500,
                                    color: Colors.blue[100],
                                  ),
                                ],
                              ),
                            ),
                          ),
                        );
                      },
                    );
                  },
                );
              },
              child: Text('Show Draggable Bottom Sheet'),
            ),
          ),
        ],
      ),
    );
  }
}

2

Answers


  1. Here is an ExpandableBottomSheet that will transition to an AppBar when it gets near the top.

    Demo of the code

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MaterialApp(home: TelegramBottomSheetDemo()));
    }
    
    class TelegramBottomSheetDemo extends StatelessWidget {
      const TelegramBottomSheetDemo({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Get Public Link Bot')),
          body: Center(
            child: ElevatedButton(
              onPressed: () => showModalBottomSheet(
                context: context,
                isScrollControlled: true,
                builder: (_) => const ExpandableBottomSheet(),
              ),
              child: const Text('Open Sheet'),
            ),
          ),
        );
      }
    }
    
    class ExpandableBottomSheet extends StatefulWidget {
      const ExpandableBottomSheet({super.key});
    
      @override
      State<ExpandableBottomSheet> createState() => _ExpandableBottomSheetState();
    }
    
    class _ExpandableBottomSheetState extends State<ExpandableBottomSheet> {
      double _height = 0.5;
      bool get _isExpanded => _height > 0.9;
    
      void _handleDrag(DragUpdateDetails details) {
        setState(() {
          _height -= details.primaryDelta! / MediaQuery.of(context).size.height;
          _height = _height.clamp(0.3, 1.0);
        });
      }
    
      void _handleDragEnd(DragEndDetails details) {
        setState(() {
          if (_height >= 0.9) {
            _height = 1.0;
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onVerticalDragUpdate: _handleDrag,
          onVerticalDragEnd: _handleDragEnd,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 100),
            height: MediaQuery.of(context).size.height * _height,
            decoration: BoxDecoration(
              color: Theme.of(context).scaffoldBackgroundColor,
              borderRadius: BorderRadius.vertical(
                top: Radius.circular(_isExpanded ? 0 : 20),
              ),
            ),
            child: Column(
              children: [
                _SheetHeader(
                  isExpanded: _isExpanded,
                  onCollapse: () {
                    Navigator.of(context).pop();
                    // Or you can collapse the sheet instead of popping
                    // setState(() => _height = 0.5);
                  },
                ),
                const Expanded(
                  child: _SheetContent(),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class _SheetHeader extends StatelessWidget {
      const _SheetHeader({
        required this.isExpanded,
        required this.onCollapse,
      });
    
      final bool isExpanded;
      final VoidCallback onCollapse;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height:
              isExpanded ? kToolbarHeight + MediaQuery.of(context).padding.top : 60,
          padding: EdgeInsets.only(
            top: isExpanded ? MediaQuery.of(context).padding.top : 0,
          ),
          child: Stack(
            children: [
              if (!isExpanded)
                Center(
                  child: Container(
                    width: 40,
                    height: 4,
                    decoration: BoxDecoration(
                      color: Colors.grey[300],
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                ),
              if (isExpanded)
                AppBar(
                  title: const Text('Gallery'),
                  leading: IconButton(
                    icon: const Icon(Icons.arrow_back),
                    onPressed: onCollapse,
                  ),
                ),
            ],
          ),
        );
      }
    }
    
    class _SheetContent extends StatelessWidget {
      const _SheetContent();
    
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          padding: EdgeInsets.zero,
          itemCount: 50,
          itemBuilder: (_, index) => ListTile(
            title: Text('Item ${index + 1}'),
            subtitle: Text('Subtitle ${index + 1}'),
          ),
        );
      }
    }
    
    Login or Signup to reply.
  2. This is tested and works fine:

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MaterialApp(home: CustomBottomSheetDemo()));
    }
    
    class CustomBottomSheetDemo extends StatelessWidget {
      const CustomBottomSheetDemo({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Expandable Bottom Sheet Demo')),
          body: Center(
            child: ElevatedButton(
              onPressed: () => showModalBottomSheet(
                context: context,
                isScrollControlled: true,
                builder: (_) => const ExpandableSheet(),
              ),
              child: const Text('Show Bottom Sheet'),
            ),
          ),
        );
      }
    }
    
    class ExpandableSheet extends StatefulWidget {
      const ExpandableSheet({super.key});
    
      @override
      State<ExpandableSheet> createState() => _ExpandableSheetState();
    }
    
    class _ExpandableSheetState extends State<ExpandableSheet> {
      double _currentHeightFactor = 0.5;
      bool get _isFullyExpanded => _currentHeightFactor > 0.9;
    
      void _onVerticalDragUpdate(DragUpdateDetails details) {
        setState(() {
          _currentHeightFactor -= details.primaryDelta! / MediaQuery.of(context).size.height;
          _currentHeightFactor = _currentHeightFactor.clamp(0.3, 1.0);
        });
      }
    
      void _onVerticalDragEnd(DragEndDetails details) {
        setState(() {
          if (_currentHeightFactor >= 0.9) {
            _currentHeightFactor = 1.0;
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onVerticalDragUpdate: _onVerticalDragUpdate,
          onVerticalDragEnd: _onVerticalDragEnd,
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 100),
            height: MediaQuery.of(context).size.height * _currentHeightFactor,
            decoration: BoxDecoration(
              color: Theme.of(context).scaffoldBackgroundColor,
              borderRadius: BorderRadius.vertical(
                top: Radius.circular(_isFullyExpanded ? 0 : 20),
              ),
            ),
            child: Column(
              children: [
                _SheetHeader(
                  isExpanded: _isFullyExpanded,
                  onCollapse: () {
                    Navigator.of(context).pop();
                  },
                ),
                const Expanded(
                  child: _SheetContent(),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class _SheetHeader extends StatelessWidget {
      const _SheetHeader({
        required this.isExpanded,
        required this.onCollapse,
      });
    
      final bool isExpanded;
      final VoidCallback onCollapse;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height:
              isExpanded ? kToolbarHeight + MediaQuery.of(context).padding.top : 60,
          padding: EdgeInsets.only(
            top: isExpanded ? MediaQuery.of(context).padding.top : 0,
          ),
          child: Stack(
            children: [
              if (!isExpanded)
                Center(
                  child: Container(
                    width: 40,
                    height: 4,
                    decoration: BoxDecoration(
                      color: Colors.grey[300],
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                ),
              if (isExpanded)
                AppBar(
                  title: const Text('Expanded Header'),
                  leading: IconButton(
                    icon: const Icon(Icons.arrow_back),
                    onPressed: onCollapse,
                  ),
                ),
            ],
          ),
        );
      }
    }
    
    class _SheetContent extends StatelessWidget {
      const _SheetContent();
    
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          padding: EdgeInsets.zero,
          itemCount: 50,
          itemBuilder: (_, index) => ListTile(
            title: Text('List Item ${index + 1}'),
            subtitle: Text('Details for Item ${index + 1}'),
          ),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search