Line data Source code
1 : import 'package:flutter/gestures.dart'; 2 : import 'package:flutter/material.dart'; 3 : 4 : /// {@template stacked_scroll_view} 5 : /// A widget that displays a scrollable view behind another widget. 6 : /// 7 : /// This widget is typically used to display a search bar at the top or a button 8 : /// at the bottom of a scrollable view. The scrollable [backgroundChild] will 9 : /// have padding applied to it so that the content of the scrollable view will 10 : /// be offset properly to account for [foregroundChild]. 11 : /// 12 : /// For example, if [alignToTop] is `true`, the [foregroundChild] will be placed 13 : /// at the top and the scrollable [backgroundChild] will have padding applied to 14 : /// the top to account for the size of the [foregroundChild]. 15 : /// {@endtemplate} 16 : class StackedScrollView extends StatefulWidget { 17 : /// {@macro stacked_scroll_view} 18 1 : const StackedScrollView({ 19 : Key? key, 20 : this.alignToTop = true, 21 : this.scrollDirection = Axis.vertical, 22 : this.reverse = false, 23 : this.padding, 24 : this.primary, 25 : this.physics, 26 : this.controller, 27 : this.dragStartBehavior = DragStartBehavior.start, 28 : this.clipBehavior = Clip.hardEdge, 29 : this.restorationId, 30 : this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, 31 : required this.foregroundChild, 32 : required this.backgroundChild, 33 1 : }) : super(key: key); 34 : 35 : /// Whether to align the [foregroundChild] to the top of the scrollable view. 36 : /// 37 : /// If `true`, the [foregroundChild] and the padding necessary to account for 38 : /// it will be placed at the top of the scrollable view. Otherwise, they will 39 : /// be placed at the bottom. 40 : final bool alignToTop; 41 : 42 : /// The direction in which this widget scrolls. 43 : final Axis scrollDirection; 44 : 45 : /// Whether the scroll view scrolls in the reading direction. 46 : final bool reverse; 47 : 48 : /// The amount of space by which to inset the [foregroundChild]. 49 : final EdgeInsets? padding; 50 : 51 : /// Whether this is the primary scroll view associated with the parent 52 : final bool? primary; 53 : 54 : /// How the scroll view should respond to user input. 55 : final ScrollPhysics? physics; 56 : 57 : /// The controller that manages the scroll view. 58 : final ScrollController? controller; 59 : 60 : /// {@macro flutter.widgets.scrollable.dragStartBehavior} 61 : final DragStartBehavior dragStartBehavior; 62 : 63 : /// The clipping behavior to use. 64 : final Clip clipBehavior; 65 : 66 : /// {@macro flutter.widgets.scrollable.restorationId} 67 : final String? restorationId; 68 : 69 : /// How this scroll view should dismiss the on-screen keyboard. 70 : final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; 71 : 72 : /// The widget to show in front of the [backgroundChild]. 73 : /// 74 : /// Depending on [alignToTop], this widget will either be placed at the top 75 : /// or the bottom of the viewport. The height of this widget will determine 76 : /// how much padding is applied to the appropriate side of the scrollable 77 : /// [backgroundChild]. 78 : final Widget foregroundChild; 79 : 80 : /// The scrollable widget to show behind the [foregroundChild]. 81 : /// 82 : /// This widget will have padding applied to it so that the content of the 83 : /// scrollable view will be offset properly to account for the size of the 84 : /// [foregroundChild]. Where this padding is applied is determined by 85 : /// [alignToTop]. 86 : final Widget backgroundChild; 87 : 88 1 : @override 89 1 : State<StackedScrollView> createState() => _StackedScrollViewState(); 90 : } 91 : 92 : class _StackedScrollViewState extends State<StackedScrollView> { 93 : final _foregroundKey = GlobalKey(); 94 : 95 : var _foregroundHeight = 0.0; 96 : 97 1 : @override 98 : void initState() { 99 1 : super.initState(); 100 1 : _scheduleForegroundHeightUpdate(); 101 : } 102 : 103 1 : void _scheduleForegroundHeightUpdate() { 104 3 : WidgetsBinding.instance!.addPostFrameCallback((_) { 105 2 : setState(() { 106 5 : _foregroundHeight = _foregroundKey.currentContext!.size!.height; 107 : }); 108 : }); 109 : } 110 : 111 1 : @override 112 : Widget build(BuildContext context) { 113 1 : final foregroundInsets = EdgeInsets.only( 114 3 : top: widget.alignToTop ? _foregroundHeight : 0.0, 115 3 : bottom: widget.alignToTop ? 0.0 : _foregroundHeight, 116 : ); 117 : final effectivePadding = 118 3 : (widget.padding ?? EdgeInsets.zero).add(foregroundInsets); 119 : 120 1 : return NotificationListener<SizeChangedLayoutNotification>( 121 1 : onNotification: (notification) { 122 1 : _scheduleForegroundHeightUpdate(); 123 : return true; 124 : }, 125 1 : child: Stack( 126 1 : children: <Widget>[ 127 1 : SingleChildScrollView( 128 2 : scrollDirection: widget.scrollDirection, 129 2 : reverse: widget.reverse, 130 : padding: effectivePadding, 131 2 : primary: widget.primary, 132 2 : physics: widget.physics, 133 2 : controller: widget.controller, 134 2 : dragStartBehavior: widget.dragStartBehavior, 135 2 : clipBehavior: widget.clipBehavior, 136 2 : restorationId: widget.restorationId, 137 2 : keyboardDismissBehavior: widget.keyboardDismissBehavior, 138 2 : child: widget.backgroundChild, 139 : ), 140 1 : Align( 141 2 : alignment: widget.alignToTop 142 : ? Alignment.topCenter 143 : : Alignment.bottomCenter, 144 1 : child: SizeChangedLayoutNotifier( 145 1 : key: _foregroundKey, 146 2 : child: widget.foregroundChild, 147 : ), 148 : ) 149 : ], 150 : ), 151 : ); 152 : } 153 : }