diff --git a/example/lib/main.dart b/example/lib/main.dart index 44bf03f9..6b35c3bb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:ui_kit/components/card/gf_card.dart'; import 'package:ui_kit/components/list_tile/gf_list_tile.dart'; import 'package:ui_kit/components/image/gf_image_overlay.dart'; import 'package:ui_kit/components/button_bar/gf_button_bar.dart'; +import 'package:ui_kit/components/segment_tabs/gf_segment_tabs.dart'; import 'package:ui_kit/types/gf_type.dart'; import 'package:ui_kit/shape/gf_avatar_shape.dart'; import 'package:ui_kit/shape/gf_button_shape.dart'; @@ -22,6 +23,9 @@ import 'package:ui_kit/components/tabs/gf_tabs.dart'; import 'package:ui_kit/components/slider/gf_items_slider.dart'; import 'package:ui_kit/components/drawer/gf_drawer.dart'; import 'package:ui_kit/components/drawer/gf_drawer_header.dart'; +import 'package:ui_kit/components/toast/gf_toast.dart'; +import 'package:ui_kit/components/appbar/gf_appbar.dart'; +import 'package:ui_kit/components/tabs/gf_tabBarView.dart'; final List imageList = [ "https://cdn.pixabay.com/photo/2017/12/03/18/04/christmas-balls-2995437_960_720.jpg", @@ -57,8 +61,23 @@ class MyHomePage extends StatefulWidget { _MyHomePageState createState() => _MyHomePageState(); } -class _MyHomePageState extends State { +class _MyHomePageState extends State with SingleTickerProviderStateMixin { + TabController tabController; + + @override + void initState() { + super.initState(); + tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + bool switchValue = true; + @override Widget build(BuildContext context) { return Scaffold( @@ -120,11 +139,42 @@ class _MyHomePageState extends State { ], ), ), - backgroundColor: Colors.cyanAccent, - appBar: AppBar( - title: Text(widget.title), + appBar: GFAppBar( +// backgroundColor: Colors.tealAccent, + centerTitle: true, + title: GFSegmentTabs( + initialIndex: 0, + length: 3, + tabs: [ + Tab( + child: Text( + "Gelatin", + ), + ), + Tab( + child: Text( + "Donuts", + ), + ), + Tab( + child: Text( + "Pastry", + ), + ), + ], + ), +// trailing: [ +// GFIconButton(icon: Icon(Icons.directions_bus), onPressed: null) +// ], ), - body: SingleChildScrollView( + backgroundColor: Colors.teal, + body: +// GFTabBarView(controller: tabController, children: [ +// Container(color: Colors.red), +// Container(color: Colors.green), +// Container(color: Colors.blue) +// ]), + SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -134,6 +184,44 @@ class _MyHomePageState extends State { backgroundImage: NetworkImage("https://cdn.pixabay.com/photo/2017/12/03/18/04/christmas-balls-2995437_960_720.jpg"), ), + GFSegmentTabs( +// height: 38.0, +// width: 180.0, + initialIndex: 0, + length: 3, + tabs: [ + Tab( + child: Text( + "Gelatin", + ), + ), + Tab( + child: Text( + "Donuts", + ), + ), + Tab( + child: Text( + "Pastry", + ), + ), + ], +// tabBarColor: Colors.pink.withOpacity(0.6), +// indicatorSize: TabBarIndicatorSize.tab, +// indicatorColor: Colors.tealAccent, +// indicator: BoxDecoration( +// color: Colors.pink, +// border: Border.all(color: Colors.green, width: 1.0), +// borderRadius: BorderRadius.circular(50.0) +// ), +// indicatorPadding: EdgeInsets.all(8.0), +// indicatorWeight: 2.0, +// border: Border.all(color: Colors.orange, width: 2.0), +// borderRadius: BorderRadius.circular(50.0) + ), + + + // GFItemsSlider( // rowCount: 3, // children: imageList.map( @@ -202,73 +290,84 @@ class _MyHomePageState extends State { // ), ), -// GFTabs( -// initialIndex: 0, -// length: 3, -// tabs: [ -// GFButton( -// onPressed: null, -// child: Text("share"), -// icon: Icon(Icons.share), -// buttonBoxShadow: true, -// ), -// Tab( -// icon: Icon(Icons.error), -// child: Text( -// "Orders", -// ), -// ), -// Tab( -// child: Text( -// "Pastry", -// ), -// ), -// ], -// tabBarView: TabBarView( -// children: [ -// Container( -// color: Colors.red, -// child: Column( -// mainAxisAlignment: MainAxisAlignment.center, -// crossAxisAlignment: CrossAxisAlignment.center, -// children: [ -// RawMaterialButton( -// onPressed: null, -// child: Text("fv"), -// ), -// FlatButton(onPressed: null, child: Text("cds")), -// Icon(Icons.directions_railway), -// GFButton( -// onPressed: null, -// child: Text("share"), -// icon: Icon(Icons.share), -// shape: GFButtonShape.pills, -// type: GFType.transparent, -// ), -// ], -// ), -// ), -// Icon(Icons.directions_car), -// Icon(Icons.directions_transit), -// ], -// ), -// indicatorColor: Colors.teal, -// indicatorSize: TabBarIndicatorSize.label, -// labelColor: Colors.lightGreen, -// unselectedLabelColor: Colors.black, -// labelStyle: TextStyle( -// fontWeight: FontWeight.w500, -// fontSize: 13.0, -// color: Colors.deepOrange, -// fontFamily: 'OpenSansBold', -// ), -// unselectedLabelStyle: TextStyle( -// fontWeight: FontWeight.w500, -// fontSize: 13.0, -// color: Colors.black, -// fontFamily: 'OpenSansBold', -// ), + GFTabs( + initialIndex: 0, + length: 3, + tabs: [ + GFButton( + onPressed: null, + child: Text("share"), + icon: Icon(Icons.share), + buttonBoxShadow: true, + ), + Tab( + icon: Icon(Icons.error), + child: Text( + "Orders", + ), + ), + Tab( + child: Text( + "Pastry", + ), + ), + ], + tabBarView: GFTabBarView( + children: [ + Container( + color: Colors.red, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + + GFToast( + child: Text("sdc"), + backgroundColor: Colors.pink, + button: GFButton( + text: 'dsx', + onPressed: (){ + print("fdsc"); + }, + ), + ), + RawMaterialButton( + onPressed: null, + child: Text("fv"), + ), + FlatButton(onPressed: null, child: Text("cds")), + Icon(Icons.directions_railway), + GFButton( + onPressed: null, + child: Text("share"), + icon: Icon(Icons.share), + shape: GFButtonShape.pills, + type: GFType.transparent, + ), + ], + ), + ), + Icon(Icons.directions_car), + Icon(Icons.directions_transit), + ], + ), + indicatorColor: Colors.teal, +// indicatorSize: TabBarIndicatorSize.label, +// labelColor: Colors.lightGreen, +// unselectedLabelColor: Colors.black, +// labelStyle: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 13.0, +// color: Colors.deepOrange, +// fontFamily: 'OpenSansBold', +// ), +// unselectedLabelStyle: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 13.0, +// color: Colors.black, +// fontFamily: 'OpenSansBold', // ), + ), // // GFSlider( // autoPlay: true, @@ -459,30 +558,30 @@ class _MyHomePageState extends State { //// //// borderRadius: BorderRadius.circular(20.0), // ), -// GFIconBadges( -// onPressed: null, -// child: GFIconButton( -// onPressed: null, -// icon: Icon(Icons.ac_unit), -// ), -// counterChild: GFBadge( -// text: '12', -// -//// color: GFColor.dark, -//// + GFIconBadges( + onPressed: null, + child: GFIconButton( + onPressed: null, + icon: Icon(Icons.ac_unit), + ), + counterChild: GFBadge( + text: '12', + +// color: GFColor.dark +////, // shape: GFBadgeShape.circle, // // size: GFSize.small, -//// -//// border: BorderSide(color: Colors.pink, width: 1.0, style: BorderStyle.solid), -//// -//// textColor: GFColor.white, -//// -//// textStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: 8.0), -//// -//// borderShape: RoundedRectangleBorder(side: BorderSide(color: Colors.orange, width: 2.0, style: BorderStyle.solid), borderRadius: BorderRadius.zero), -// ), -// ), +// +// border: BorderSide(color: Colors.pink, width: 1.0, style: BorderStyle.solid), +// +// textColor: GFColor.white, +// +// textStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: 8.0), +// +// borderShape: RoundedRectangleBorder(side: BorderSide(color: Colors.orange, width: 2.0, style: BorderStyle.solid), borderRadius: BorderRadius.zero), + ), + ), // GFIconButton( // onPressed: null, // icon: Icon(Icons.ac_unit), diff --git a/lib/components/appbar/gf_appbar.dart b/lib/components/appbar/gf_appbar.dart new file mode 100644 index 00000000..5f868550 --- /dev/null +++ b/lib/components/appbar/gf_appbar.dart @@ -0,0 +1,1299 @@ +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; + +const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. + +// Bottom justify the kToolbarHeight child which may overflow the top. +class _ToolbarContainerLayout extends SingleChildLayoutDelegate { + const _ToolbarContainerLayout(); + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + return constraints.tighten(height: kToolbarHeight); + } + + @override + Size getSize(BoxConstraints constraints) { + return Size(constraints.maxWidth, kToolbarHeight); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + return Offset(0.0, size.height - childSize.height); + } + + @override + bool shouldRelayout(_ToolbarContainerLayout oldDelegate) => false; +} + +// TODO(eseidel): Toolbar needs to change size based on orientation: +// https://material.io/design/components/app-bars-top.html#specs +// Mobile Landscape: 48dp +// Mobile Portrait: 56dp +// Tablet/Desktop: 64dp + +/// A material design app bar. +/// +/// An app bar consists of a toolbar and potentially other widgets, such as a +/// [TabBar] and a [FlexibleSpaceBar]. App bars typically expose one or more +/// common [actions] with [IconButton]s which are optionally followed by a +/// [PopupMenuButton] for less common operations (sometimes called the "overflow +/// menu"). +/// +/// App bars are typically used in the [Scaffold.appBar] property, which places +/// the app bar as a fixed-height widget at the top of the screen. For a scrollable +/// app bar, see [SliverGFAppBar], which embeds an [GFAppBar] in a sliver for use in +/// a [CustomScrollView]. +/// +/// When not used as [Scaffold.appBar], or when wrapped in a [Hero], place the app +/// bar in a [MediaQuery] to take care of the padding around the content of the +/// app bar if needed, as the padding will not be handled by [Scaffold]. +/// +/// The GFAppBar displays the toolbar widgets, [leading], [title], and [actions], +/// above the [bottom] (if any). The [bottom] is usually used for a [TabBar]. If +/// a [flexibleSpace] widget is specified then it is stacked behind the toolbar +/// and the bottom widget. The following diagram shows where each of these slots +/// appears in the toolbar when the writing language is left-to-right (e.g. +/// English): +/// +/// ![The leading widget is in the top left, the actions are in the top right, +/// the title is between them. The bottom is, naturally, at the bottom, and the +/// flexibleSpace is behind all of them.](https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.png) +/// +/// If the [leading] widget is omitted, but the [GFAppBar] is in a [Scaffold] with +/// a [Drawer], then a button will be inserted to open the drawer. Otherwise, if +/// the nearest [Navigator] has any previous routes, a [BackButton] is inserted +/// instead. This behavior can be turned off by setting the [automaticallyImplyLeading] +/// to false. In that case a null leading widget will result in the middle/title widget +/// stretching to start. +/// +/// {@tool dartpad --template=stateless_widget_material} +/// +/// This sample shows an [GFAppBar] with two simple actions. The first action +/// opens a [SnackBar], while the second action navigates to a new page. +/// +/// ```dart preamble +/// final GlobalKey scaffoldKey = GlobalKey(); +/// final SnackBar snackBar = const SnackBar(content: Text('Showing Snackbar')); +/// +/// void openPage(BuildContext context) { +/// Navigator.push(context, MaterialPageRoute( +/// builder: (BuildContext context) { +/// return Scaffold( +/// appBar: GFAppBar( +/// title: const Text('Next page'), +/// ), +/// body: const Center( +/// child: Text( +/// 'This is the next page', +/// style: TextStyle(fontSize: 24), +/// ), +/// ), +/// ); +/// }, +/// )); +/// } +/// ``` +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return Scaffold( +/// key: scaffoldKey, +/// appBar: GFAppBar( +/// title: const Text('GFAppBar Demo'), +/// actions: [ +/// IconButton( +/// icon: const Icon(Icons.add_alert), +/// tooltip: 'Show Snackbar', +/// onPressed: () { +/// scaffoldKey.currentState.showSnackBar(snackBar); +/// }, +/// ), +/// IconButton( +/// icon: const Icon(Icons.navigate_next), +/// tooltip: 'Next page', +/// onPressed: () { +/// openPage(context); +/// }, +/// ), +/// ], +/// ), +/// body: const Center( +/// child: Text( +/// 'This is the home page', +/// style: TextStyle(fontSize: 24), +/// ), +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [Scaffold], which displays the [GFAppBar] in its [Scaffold.appBar] slot. +/// * [SliverGFAppBar], which uses [GFAppBar] to provide a flexible app bar that +/// can be used in a [CustomScrollView]. +/// * [TabBar], which is typically placed in the [bottom] slot of the [GFAppBar] +/// if the screen has multiple pages arranged in tabs. +/// * [IconButton], which is used with [actions] to show buttons on the app bar. +/// * [PopupMenuButton], to show a popup menu on the app bar, via [actions]. +/// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar +/// can expand and collapse. +/// * +class GFAppBar extends StatefulWidget implements PreferredSizeWidget { + /// Creates a material design app bar. + /// + /// The arguments [primary], [toolbarOpacity], [bottomOpacity] + /// and [automaticallyImplyLeading] must not be null. Additionally, if + /// [elevation] is specified, it must be non-negative. + /// + /// If [backgroundColor], [elevation], [brightness], [iconTheme], + /// [actionsIconTheme], or [textTheme] are null, then their [GFAppBarTheme] + /// values will be used. If the corresponding [GFAppBarTheme] property is null, + /// then the default specified in the property's documentation will be used. + /// + /// Typically used in the [Scaffold.appBar] property. + GFAppBar({ + Key key, + this.leading, + this.automaticallyImplyLeading = true, + this.title, + this.trailing, + this.flexibleSpace, + this.bottom, + this.elevation, + this.shape, + this.backgroundColor, + this.brightness, + this.iconTheme, + this.actionsIconTheme, + this.textTheme, + this.primary = true, + this.centerTitle, + this.titleSpacing = NavigationToolbar.kMiddleSpacing, + this.toolbarOpacity = 1.0, + this.bottomOpacity = 1.0, + }) : assert(automaticallyImplyLeading != null), + assert(elevation == null || elevation >= 0.0), + assert(primary != null), + assert(titleSpacing != null), + assert(toolbarOpacity != null), + assert(bottomOpacity != null), + preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)), + super(key: key); + + /// A widget to display before the [title]. + /// + /// If this is null and [automaticallyImplyLeading] is set to true, the + /// [GFAppBar] will imply an appropriate widget. For example, if the [GFAppBar] is + /// in a [Scaffold] that also has a [Drawer], the [Scaffold] will fill this + /// widget with an [IconButton] that opens the drawer (using [Icons.menu]). If + /// there's no [Drawer] and the parent [Navigator] can go back, the [GFAppBar] + /// will use a [BackButton] that calls [Navigator.maybePop]. + /// + /// {@tool sample} + /// + /// The following code shows how the drawer button could be manually specified + /// instead of relying on [automaticallyImplyLeading]: + /// + /// ```dart + /// GFAppBar( + /// leading: Builder( + /// builder: (BuildContext context) { + /// return IconButton( + /// icon: const Icon(Icons.menu), + /// onPressed: () { Scaffold.of(context).openDrawer(); }, + /// tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + /// ); + /// }, + /// ), + /// ) + /// ``` + /// {@end-tool} + /// + /// The [Builder] is used in this example to ensure that the `context` refers + /// to that part of the subtree. That way this code snippet can be used even + /// inside the very code that is creating the [Scaffold] (in which case, + /// without the [Builder], the `context` wouldn't be able to see the + /// [Scaffold], since it would refer to an ancestor of that widget). + /// + /// See also: + /// + /// * [Scaffold.appBar], in which an [GFAppBar] is usually placed. + /// * [Scaffold.drawer], in which the [Drawer] is usually placed. + final Widget leading; + + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the leading + /// widget should be. If false and [leading] is null, leading space is given to [title]. + /// If leading widget is not null, this parameter has no effect. + final bool automaticallyImplyLeading; + + /// The primary widget displayed in the app bar. + /// + /// Typically a [Text] widget containing a description of the current contents + /// of the app. + final Widget title; + + /// Widgets to display after the [title] widget. + /// + /// Typically these widgets are [IconButton]s representing common operations. + /// For less common operations, consider using a [PopupMenuButton] as the + /// last action. + final List trailing; + + /// This widget is stacked behind the toolbar and the tab bar. It's height will + /// be the same as the app bar's overall height. + /// + /// A flexible space isn't actually flexible unless the [GFAppBar]'s container + /// changes the [GFAppBar]'s size. A [SliverGFAppBar] in a [CustomScrollView] + /// changes the [GFAppBar]'s height when scrolled. + /// + /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. + final Widget flexibleSpace; + + /// This widget appears across the bottom of the app bar. + /// + /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can + /// be used at the bottom of an app bar. + /// + /// See also: + /// + /// * [PreferredSize], which can be used to give an arbitrary widget a preferred size. + final PreferredSizeWidget bottom; + + /// The z-coordinate at which to place this app bar relative to its parent. + /// + /// This controls the size of the shadow below the app bar. + /// + /// The value is non-negative. + /// + /// If this property is null, then [ThemeData.appBarTheme.elevation] is used, + /// if that is also null, the default value is 4, the appropriate elevation + /// for app bars. + final double elevation; + + /// The material's shape as well its shadow. + /// + /// A shadow is only displayed if the [elevation] is greater than + /// zero. + final ShapeBorder shape; + + /// The color to use for the app bar's material. Typically this should be set + /// along with [brightness], [iconTheme], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.color] is used, + /// if that is also null, then [ThemeData.primaryColor] is used. + final Color backgroundColor; + + /// The brightness of the app bar's material. Typically this is set along + /// with [backgroundColor], [iconTheme], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.brightness] is used, + /// if that is also null, then [ThemeData.primaryColorBrightness] is used. + final Brightness brightness; + + /// The color, opacity, and size to use for app bar icons. Typically this + /// is set along with [backgroundColor], [brightness], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used, + /// if that is also null, then [ThemeData.primaryIconTheme] is used. + final IconThemeData iconTheme; + + /// The color, opacity, and size to use for the icons that appear in the app + /// bar's [actions]. This should only be used when the [actions] should be + /// themed differently than the icon that appears in the app bar's [leading] + /// widget. + /// + /// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is + /// used, if that is also null, then this falls back to [iconTheme]. + final IconThemeData actionsIconTheme; + + /// The typographic styles to use for text in the app bar. Typically this is + /// set along with [brightness] [backgroundColor], [iconTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.textTheme] is used, + /// if that is also null, then [ThemeData.primaryTextTheme] is used. + final TextTheme textTheme; + + /// Whether this app bar is being displayed at the top of the screen. + /// + /// If true, the app bar's toolbar elements and [bottom] widget will be + /// padded on top by the height of the system status bar. The layout + /// of the [flexibleSpace] is not affected by the [primary] property. + final bool primary; + + /// Whether the title should be centered. + /// + /// Defaults to being adapted to the current [TargetPlatform]. + final bool centerTitle; + + /// The spacing around [title] content on the horizontal axis. This spacing is + /// applied even if there is no [leading] content or [actions]. If you want + /// [title] to take all the space available, set this value to 0.0. + /// + /// Defaults to [NavigationToolbar.kMiddleSpacing]. + final double titleSpacing; + + /// How opaque the toolbar part of the app bar is. + /// + /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent. + /// + /// Typically, this value is not changed from its default value (1.0). It is + /// used by [SliverGFAppBar] to animate the opacity of the toolbar when the app + /// bar is scrolled. + final double toolbarOpacity; + + /// How opaque the bottom part of the app bar is. + /// + /// A value of 1.0 is fully opaque, and a value of 0.0 is fully transparent. + /// + /// Typically, this value is not changed from its default value (1.0). It is + /// used by [SliverGFAppBar] to animate the opacity of the toolbar when the app + /// bar is scrolled. + final double bottomOpacity; + + /// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's + /// preferred height. + /// + /// [Scaffold] uses this size to set its app bar's height. + @override + final Size preferredSize; + + bool _getEffectiveCenterTitle(ThemeData theme) { + if (centerTitle != null) + return centerTitle; + assert(theme.platform != null); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return false; + case TargetPlatform.iOS: + return trailing == null || trailing.length < 2; + } + return null; + } + + @override + _GFAppBarState createState() => _GFAppBarState(); +} + +class _GFAppBarState extends State { + static const double _defaultElevation = 4.0; + + void _handleDrawerButton() { + Scaffold.of(context).openDrawer(); + } + + void _handleDrawerButtonEnd() { + Scaffold.of(context).openEndDrawer(); + } + + @override + Widget build(BuildContext context) { + assert(!widget.primary || debugCheckHasMediaQuery(context)); + assert(debugCheckHasMaterialLocalizations(context)); + final ThemeData theme = Theme.of(context); + final AppBarTheme appBarTheme = AppBarTheme.of(context); + final ScaffoldState scaffold = Scaffold.of(context, nullOk: true); + final ModalRoute parentRoute = ModalRoute.of(context); + + final bool hasDrawer = scaffold?.hasDrawer ?? false; + final bool hasEndDrawer = scaffold?.hasEndDrawer ?? false; + final bool canPop = parentRoute?.canPop ?? false; + final bool useCloseButton = parentRoute is PageRoute && parentRoute.fullscreenDialog; + + IconThemeData overallIconTheme = widget.iconTheme + ?? appBarTheme.iconTheme + ?? theme.primaryIconTheme; + IconThemeData actionsIconTheme = widget.actionsIconTheme + ?? appBarTheme.actionsIconTheme + ?? overallIconTheme; + TextStyle centerStyle = widget.textTheme?.title + ?? appBarTheme.textTheme?.title + ?? theme.primaryTextTheme.title; + TextStyle sideStyle = widget.textTheme?.body1 + ?? appBarTheme.textTheme?.body1 + ?? theme.primaryTextTheme.body1; + + if (widget.toolbarOpacity != 1.0) { + final double opacity = const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.toolbarOpacity); + if (centerStyle?.color != null) + centerStyle = centerStyle.copyWith(color: centerStyle.color.withOpacity(opacity)); + if (sideStyle?.color != null) + sideStyle = sideStyle.copyWith(color: sideStyle.color.withOpacity(opacity)); + overallIconTheme = overallIconTheme.copyWith( + opacity: opacity * (overallIconTheme.opacity ?? 1.0) + ); + actionsIconTheme = actionsIconTheme.copyWith( + opacity: opacity * (actionsIconTheme.opacity ?? 1.0) + ); + } + + Widget leading = widget.leading; + if (leading == null && widget.automaticallyImplyLeading) { + if (hasDrawer) { + leading = IconButton( + icon: const Icon(Icons.menu), + onPressed: _handleDrawerButton, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + ); + } else { + if (canPop) + leading = useCloseButton ? const CloseButton() : const BackButton(); + } + } + if (leading != null) { + leading = ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: _kLeadingWidth), + child: leading, + ); + } + + Widget title = widget.title; + if (title != null) { + bool namesRoute; + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + namesRoute = true; + break; + case TargetPlatform.iOS: + break; + } + title = DefaultTextStyle( + style: centerStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: Semantics( + namesRoute: namesRoute, + child: _GFAppBarTitleBox(child: title), + header: true, + ), + ); + } + + Widget actions; + if (widget.trailing != null && widget.trailing.isNotEmpty) { + actions = Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.trailing, + ); + } else if (hasEndDrawer) { + actions = IconButton( + icon: const Icon(Icons.menu), + onPressed: _handleDrawerButtonEnd, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + ); + } + + // Allow the trailing actions to have their own theme if necessary. + if (actions != null) { + actions = IconTheme.merge( + data: actionsIconTheme, + child: actions, + ); + } + + final Widget toolbar = NavigationToolbar( + leading: leading, + middle: title, + trailing: actions, + centerMiddle: widget._getEffectiveCenterTitle(theme), + middleSpacing: widget.titleSpacing, + ); + + // If the toolbar is allocated less than kToolbarHeight make it + // appear to scroll upwards within its shrinking container. + Widget appBar = ClipRect( + child: CustomSingleChildLayout( + delegate: const _ToolbarContainerLayout(), + child: IconTheme.merge( + data: overallIconTheme, + child: DefaultTextStyle( + style: sideStyle, + child: toolbar, + ), + ), + ), + ); + if (widget.bottom != null) { + appBar = Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: kToolbarHeight), + child: appBar, + ), + ), + if (widget.bottomOpacity == 1.0) + widget.bottom + else + Opacity( + opacity: const Interval(0.25, 1.0, curve: Curves.fastOutSlowIn).transform(widget.bottomOpacity), + child: widget.bottom, + ), + ], + ); + } + + // The padding applies to the toolbar and tabbar, not the flexible space. + if (widget.primary) { + appBar = SafeArea( + top: true, + child: appBar, + ); + } + + appBar = Align( + alignment: Alignment.topCenter, + child: appBar, + ); + + if (widget.flexibleSpace != null) { + appBar = Stack( + fit: StackFit.passthrough, + children: [ + widget.flexibleSpace, + appBar, + ], + ); + } + final Brightness brightness = widget.brightness + ?? appBarTheme.brightness + ?? theme.primaryColorBrightness; + final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark; + + return Semantics( + container: true, + child: AnnotatedRegion( + value: overlayStyle, + child: Material( + color: widget.backgroundColor + ?? appBarTheme.color + ?? theme.primaryColor, + elevation: widget.elevation + ?? appBarTheme.elevation + ?? _defaultElevation, + shape: widget.shape, + child: Semantics( + explicitChildNodes: true, + child: appBar, + ), + ), + ), + ); + } +} + +class _FloatingGFAppBar extends StatefulWidget { + const _FloatingGFAppBar({ Key key, this.child }) : super(key: key); + + final Widget child; + + @override + _FloatingGFAppBarState createState() => _FloatingGFAppBarState(); +} + +// A wrapper for the widget created by _SliverGFAppBarDelegate that starts and +// stops the floating app bar's snap-into-view or snap-out-of-view animation. +class _FloatingGFAppBarState extends State<_FloatingGFAppBar> { + ScrollPosition _position; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_position != null) + _position.isScrollingNotifier.removeListener(_isScrollingListener); + _position = Scrollable.of(context)?.position; + if (_position != null) + _position.isScrollingNotifier.addListener(_isScrollingListener); + } + + @override + void dispose() { + if (_position != null) + _position.isScrollingNotifier.removeListener(_isScrollingListener); + super.dispose(); + } + + RenderSliverFloatingPersistentHeader _headerRenderer() { + return context.findAncestorRenderObjectOfType(); + } + + void _isScrollingListener() { + if (_position == null) + return; + + // When a scroll stops, then maybe snap the appbar into view. + // Similarly, when a scroll starts, then maybe stop the snap animation. + final RenderSliverFloatingPersistentHeader header = _headerRenderer(); + if (_position.isScrollingNotifier.value) + header?.maybeStopSnapAnimation(_position.userScrollDirection); + else + header?.maybeStartSnapAnimation(_position.userScrollDirection); + } + + @override + Widget build(BuildContext context) => widget.child; +} + +class _SliverGFAppBarDelegate extends SliverPersistentHeaderDelegate { + _SliverGFAppBarDelegate({ + @required this.leading, + @required this.automaticallyImplyLeading, + @required this.title, + @required this.actions, + @required this.flexibleSpace, + @required this.bottom, + @required this.elevation, + @required this.forceElevated, + @required this.backgroundColor, + @required this.brightness, + @required this.iconTheme, + @required this.actionsIconTheme, + @required this.textTheme, + @required this.primary, + @required this.centerTitle, + @required this.titleSpacing, + @required this.expandedHeight, + @required this.collapsedHeight, + @required this.topPadding, + @required this.floating, + @required this.pinned, + @required this.snapConfiguration, + @required this.stretchConfiguration, + @required this.shape, + }) : assert(primary || topPadding == 0.0), + _bottomHeight = bottom?.preferredSize?.height ?? 0.0; + + final Widget leading; + final bool automaticallyImplyLeading; + final Widget title; + final List actions; + final Widget flexibleSpace; + final PreferredSizeWidget bottom; + final double elevation; + final bool forceElevated; + final Color backgroundColor; + final Brightness brightness; + final IconThemeData iconTheme; + final IconThemeData actionsIconTheme; + final TextTheme textTheme; + final bool primary; + final bool centerTitle; + final double titleSpacing; + final double expandedHeight; + final double collapsedHeight; + final double topPadding; + final bool floating; + final bool pinned; + final ShapeBorder shape; + + final double _bottomHeight; + + @override + double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight + _bottomHeight); + + @override + double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent); + + @override + final FloatingHeaderSnapConfiguration snapConfiguration; + + @override + final OverScrollHeaderStretchConfiguration stretchConfiguration; + + @override + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + final double visibleMainHeight = maxExtent - shrinkOffset - topPadding; + + // Truth table for `toolbarOpacity`: + // pinned | floating | bottom != null || opacity + // ---------------------------------------------- + // 0 | 0 | 0 || fade + // 0 | 0 | 1 || fade + // 0 | 1 | 0 || fade + // 0 | 1 | 1 || fade + // 1 | 0 | 0 || 1.0 + // 1 | 0 | 1 || 1.0 + // 1 | 1 | 0 || 1.0 + // 1 | 1 | 1 || fade + final double toolbarOpacity = !pinned || (floating && bottom != null) + ? ((visibleMainHeight - _bottomHeight) / kToolbarHeight).clamp(0.0, 1.0) + : 1.0; + + final Widget appBar = FlexibleSpaceBar.createSettings( + minExtent: minExtent, + maxExtent: maxExtent, + currentExtent: math.max(minExtent, maxExtent - shrinkOffset), + toolbarOpacity: toolbarOpacity, + child: GFAppBar( + leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, + title: title, + trailing: actions, + flexibleSpace: (title == null && flexibleSpace != null) + ? Semantics(child: flexibleSpace, header: true) + : flexibleSpace, + bottom: bottom, + elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4.0 : 0.0, + backgroundColor: backgroundColor, + brightness: brightness, + iconTheme: iconTheme, + actionsIconTheme: actionsIconTheme, + textTheme: textTheme, + primary: primary, + centerTitle: centerTitle, + titleSpacing: titleSpacing, + shape: shape, + toolbarOpacity: toolbarOpacity, + bottomOpacity: pinned ? 1.0 : (visibleMainHeight / _bottomHeight).clamp(0.0, 1.0), + ), + ); + return floating ? _FloatingGFAppBar(child: appBar) : appBar; + } + + @override + bool shouldRebuild(covariant _SliverGFAppBarDelegate oldDelegate) { + return leading != oldDelegate.leading + || automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading + || title != oldDelegate.title + || actions != oldDelegate.actions + || flexibleSpace != oldDelegate.flexibleSpace + || bottom != oldDelegate.bottom + || _bottomHeight != oldDelegate._bottomHeight + || elevation != oldDelegate.elevation + || backgroundColor != oldDelegate.backgroundColor + || brightness != oldDelegate.brightness + || iconTheme != oldDelegate.iconTheme + || actionsIconTheme != oldDelegate.actionsIconTheme + || textTheme != oldDelegate.textTheme + || primary != oldDelegate.primary + || centerTitle != oldDelegate.centerTitle + || titleSpacing != oldDelegate.titleSpacing + || expandedHeight != oldDelegate.expandedHeight + || topPadding != oldDelegate.topPadding + || pinned != oldDelegate.pinned + || floating != oldDelegate.floating + || snapConfiguration != oldDelegate.snapConfiguration + || stretchConfiguration != oldDelegate.stretchConfiguration; + } + + @override + String toString() { + return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; + } +} + +/// A material design app bar that integrates with a [CustomScrollView]. +/// +/// An app bar consists of a toolbar and potentially other widgets, such as a +/// [TabBar] and a [FlexibleSpaceBar]. App bars typically expose one or more +/// common actions with [IconButton]s which are optionally followed by a +/// [PopupMenuButton] for less common operations. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=R9C5KMJKluE} +/// +/// Sliver app bars are typically used as the first child of a +/// [CustomScrollView], which lets the app bar integrate with the scroll view so +/// that it can vary in height according to the scroll offset or float above the +/// other content in the scroll view. For a fixed-height app bar at the top of +/// the screen see [GFAppBar], which is used in the [Scaffold.appBar] slot. +/// +/// The GFAppBar displays the toolbar widgets, [leading], [title], and +/// [actions], above the [bottom] (if any). If a [flexibleSpace] widget is +/// specified then it is stacked behind the toolbar and the bottom widget. +/// +/// {@tool sample} +/// +/// This is an example that could be included in a [CustomScrollView]'s +/// [CustomScrollView.slivers] list: +/// +/// ```dart +/// SliverGFAppBar( +/// expandedHeight: 150.0, +/// flexibleSpace: const FlexibleSpaceBar( +/// title: Text('Available seats'), +/// ), +/// actions: [ +/// IconButton( +/// icon: const Icon(Icons.add_circle), +/// tooltip: 'Add new entry', +/// onPressed: () { /* ... */ }, +/// ), +/// ] +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Animated Examples +/// +/// The following animations show how app bars with different configurations +/// behave when a user scrolls up and then down again. +/// +/// * App bar with [floating]: false, [pinned]: false, [snap]: false: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} +/// +/// * App bar with [floating]: true, [pinned]: false, [snap]: false: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} +/// +/// * App bar with [floating]: true, [pinned]: false, [snap]: true: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating_snap.mp4} +/// +/// * App bar with [floating]: true, [pinned]: true, [snap]: false: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned_floating.mp4} +/// +/// * App bar with [floating]: true, [pinned]: true, [snap]: true: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned_floating_snap.mp4} +/// +/// * App bar with [floating]: false, [pinned]: true, [snap]: false: +/// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned.mp4} +/// +/// The property [snap] can only be set to true if [floating] is also true. +/// +/// See also: +/// +/// * [CustomScrollView], which integrates the [SliverGFAppBar] into its +/// scrolling. +/// * [GFAppBar], which is a fixed-height app bar for use in [Scaffold.appBar]. +/// * [TabBar], which is typically placed in the [bottom] slot of the [GFAppBar] +/// if the screen has multiple pages arranged in tabs. +/// * [IconButton], which is used with [actions] to show buttons on the app bar. +/// * [PopupMenuButton], to show a popup menu on the app bar, via [actions]. +/// * [FlexibleSpaceBar], which is used with [flexibleSpace] when the app bar +/// can expand and collapse. +/// * +class SliverGFAppBar extends StatefulWidget { + /// Creates a material design app bar that can be placed in a [CustomScrollView]. + /// + /// The arguments [forceElevated], [primary], [floating], [pinned], [snap] + /// and [automaticallyImplyLeading] must not be null. + const SliverGFAppBar({ + Key key, + this.leading, + this.automaticallyImplyLeading = true, + this.title, + this.actions, + this.flexibleSpace, + this.bottom, + this.elevation, + this.forceElevated = false, + this.backgroundColor, + this.brightness, + this.iconTheme, + this.actionsIconTheme, + this.textTheme, + this.primary = true, + this.centerTitle, + this.titleSpacing = NavigationToolbar.kMiddleSpacing, + this.expandedHeight, + this.floating = false, + this.pinned = false, + this.snap = false, + this.stretch = false, + this.stretchTriggerOffset = 100.0, + this.onStretchTrigger, + this.shape, + }) : assert(automaticallyImplyLeading != null), + assert(forceElevated != null), + assert(primary != null), + assert(titleSpacing != null), + assert(floating != null), + assert(pinned != null), + assert(snap != null), + assert(stretch != null), + assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'), + assert(stretchTriggerOffset > 0.0), + super(key: key); + + /// A widget to display before the [title]. + /// + /// If this is null and [automaticallyImplyLeading] is set to true, the [GFAppBar] will + /// imply an appropriate widget. For example, if the [GFAppBar] is in a [Scaffold] + /// that also has a [Drawer], the [Scaffold] will fill this widget with an + /// [IconButton] that opens the drawer. If there's no [Drawer] and the parent + /// [Navigator] can go back, the [GFAppBar] will use a [BackButton] that calls + /// [Navigator.maybePop]. + final Widget leading; + + /// Controls whether we should try to imply the leading widget if null. + /// + /// If true and [leading] is null, automatically try to deduce what the leading + /// widget should be. If false and [leading] is null, leading space is given to [title]. + /// If leading widget is not null, this parameter has no effect. + final bool automaticallyImplyLeading; + + /// The primary widget displayed in the app bar. + /// + /// Typically a [Text] widget containing a description of the current contents + /// of the app. + final Widget title; + + /// Widgets to display after the [title] widget. + /// + /// Typically these widgets are [IconButton]s representing common operations. + /// For less common operations, consider using a [PopupMenuButton] as the + /// last action. + /// + /// {@tool sample} + /// + /// ```dart + /// Scaffold( + /// body: CustomScrollView( + /// primary: true, + /// slivers: [ + /// SliverGFAppBar( + /// title: Text('Hello World'), + /// actions: [ + /// IconButton( + /// icon: Icon(Icons.shopping_cart), + /// tooltip: 'Open shopping cart', + /// onPressed: () { + /// // handle the press + /// }, + /// ), + /// ], + /// ), + /// // ...rest of body... + /// ], + /// ), + /// ) + /// ``` + /// {@end-tool} + final List actions; + + /// This widget is stacked behind the toolbar and the tab bar. It's height will + /// be the same as the app bar's overall height. + /// + /// Typically a [FlexibleSpaceBar]. See [FlexibleSpaceBar] for details. + final Widget flexibleSpace; + + /// This widget appears across the bottom of the app bar. + /// + /// Typically a [TabBar]. Only widgets that implement [PreferredSizeWidget] can + /// be used at the bottom of an app bar. + /// + /// See also: + /// + /// * [PreferredSize], which can be used to give an arbitrary widget a preferred size. + final PreferredSizeWidget bottom; + + /// The z-coordinate at which to place this app bar when it is above other + /// content. This controls the size of the shadow below the app bar. + /// + /// If this property is null, then [ThemeData.appBarTheme.elevation] is used, + /// if that is also null, the default value is 4, the appropriate elevation + /// for app bars. + /// + /// If [forceElevated] is false, the elevation is ignored when the app bar has + /// no content underneath it. For example, if the app bar is [pinned] but no + /// content is scrolled under it, or if it scrolls with the content, then no + /// shadow is drawn, regardless of the value of [elevation]. + final double elevation; + + /// Whether to show the shadow appropriate for the [elevation] even if the + /// content is not scrolled under the [GFAppBar]. + /// + /// Defaults to false, meaning that the [elevation] is only applied when the + /// [GFAppBar] is being displayed over content that is scrolled under it. + /// + /// When set to true, the [elevation] is applied regardless. + /// + /// Ignored when [elevation] is zero. + final bool forceElevated; + + /// The color to use for the app bar's material. Typically this should be set + /// along with [brightness], [iconTheme], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.color] is used, + /// if that is also null, then [ThemeData.primaryColor] is used. + final Color backgroundColor; + + /// The brightness of the app bar's material. Typically this is set along + /// with [backgroundColor], [iconTheme], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.brightness] is used, + /// if that is also null, then [ThemeData.primaryColorBrightness] is used. + final Brightness brightness; + + /// The color, opacity, and size to use for app bar icons. Typically this + /// is set along with [backgroundColor], [brightness], [textTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.iconTheme] is used, + /// if that is also null, then [ThemeData.primaryIconTheme] is used. + final IconThemeData iconTheme; + + /// The color, opacity, and size to use for trailing app bar icons. This + /// should only be used when the trailing icons should be themed differently + /// than the leading icons. + /// + /// If this property is null, then [ThemeData.appBarTheme.actionsIconTheme] is + /// used, if that is also null, then this falls back to [iconTheme]. + final IconThemeData actionsIconTheme; + + /// The typographic styles to use for text in the app bar. Typically this is + /// set along with [brightness] [backgroundColor], [iconTheme]. + /// + /// If this property is null, then [ThemeData.appBarTheme.textTheme] is used, + /// if that is also null, then [ThemeData.primaryTextTheme] is used. + final TextTheme textTheme; + + /// Whether this app bar is being displayed at the top of the screen. + /// + /// If this is true, the top padding specified by the [MediaQuery] will be + /// added to the top of the toolbar. + final bool primary; + + /// Whether the title should be centered. + /// + /// Defaults to being adapted to the current [TargetPlatform]. + final bool centerTitle; + + /// The spacing around [title] content on the horizontal axis. This spacing is + /// applied even if there is no [leading] content or [actions]. If you want + /// [title] to take all the space available, set this value to 0.0. + /// + /// Defaults to [NavigationToolbar.kMiddleSpacing]. + final double titleSpacing; + + /// The size of the app bar when it is fully expanded. + /// + /// By default, the total height of the toolbar and the bottom widget (if + /// any). If a [flexibleSpace] widget is specified this height should be big + /// enough to accommodate whatever that widget contains. + /// + /// This does not include the status bar height (which will be automatically + /// included if [primary] is true). + final double expandedHeight; + + /// Whether the app bar should become visible as soon as the user scrolls + /// towards the app bar. + /// + /// Otherwise, the user will need to scroll near the top of the scroll view to + /// reveal the app bar. + /// + /// If [snap] is true then a scroll that exposes the app bar will trigger an + /// animation that slides the entire app bar into view. Similarly if a scroll + /// dismisses the app bar, the animation will slide it completely out of view. + /// + /// ## Animated Examples + /// + /// The following animations show how the app bar changes its scrolling + /// behavior based on the value of this property. + /// + /// * App bar with [floating] set to false: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} + /// * App bar with [floating] set to true: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} + /// + /// See also: + /// + /// * [SliverGFAppBar] for more animated examples of how this property changes the + /// behavior of the app bar in combination with [pinned] and [snap]. + final bool floating; + + /// Whether the app bar should remain visible at the start of the scroll view. + /// + /// The app bar can still expand and contract as the user scrolls, but it will + /// remain visible rather than being scrolled out of view. + /// + /// ## Animated Examples + /// + /// The following animations show how the app bar changes its scrolling + /// behavior based on the value of this property. + /// + /// * App bar with [pinned] set to false: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar.mp4} + /// * App bar with [pinned] set to true: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_pinned.mp4} + /// + /// See also: + /// + /// * [SliverGFAppBar] for more animated examples of how this property changes the + /// behavior of the app bar in combination with [floating]. + final bool pinned; + + /// The material's shape as well as its shadow. + /// + /// A shadow is only displayed if the [elevation] is greater than zero. + final ShapeBorder shape; + + /// If [snap] and [floating] are true then the floating app bar will "snap" + /// into view. + /// + /// If [snap] is true then a scroll that exposes the floating app bar will + /// trigger an animation that slides the entire app bar into view. Similarly if + /// a scroll dismisses the app bar, the animation will slide the app bar + /// completely out of view. + /// + /// Snapping only applies when the app bar is floating, not when the app bar + /// appears at the top of its scroll view. + /// + /// ## Animated Examples + /// + /// The following animations show how the app bar changes its scrolling + /// behavior based on the value of this property. + /// + /// * App bar with [snap] set to false: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating.mp4} + /// * App bar with [snap] set to true: + /// {@animation 476 400 https://flutter.github.io/assets-for-api-docs/assets/material/app_bar_floating_snap.mp4} + /// + /// See also: + /// + /// * [SliverGFAppBar] for more animated examples of how this property changes the + /// behavior of the app bar in combination with [pinned] and [floating]. + final bool snap; + + /// Whether the app bar should stretch to fill the over-scroll area. + /// + /// The app bar can still expand and contract as the user scrolls, but it will + /// also stretch when the user over-scrolls. + final bool stretch; + + /// The offset of overscroll required to activate [onStretchTrigger]. + /// + /// This defaults to 100.0. + final double stretchTriggerOffset; + + /// The callback function to be executed when a user over-scrolls to the + /// offset specified by [stretchTriggerOffset]. + final AsyncCallback onStretchTrigger; + + @override + _SliverGFAppBarState createState() => _SliverGFAppBarState(); +} + +// This class is only Stateful because it owns the TickerProvider used +// by the floating appbar snap animation (via FloatingHeaderSnapConfiguration). +class _SliverGFAppBarState extends State with TickerProviderStateMixin { + FloatingHeaderSnapConfiguration _snapConfiguration; + OverScrollHeaderStretchConfiguration _stretchConfiguration; + + void _updateSnapConfiguration() { + if (widget.snap && widget.floating) { + _snapConfiguration = FloatingHeaderSnapConfiguration( + vsync: this, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + ); + } else { + _snapConfiguration = null; + } + } + + void _updateStretchConfiguration() { + if (widget.stretch) { + _stretchConfiguration = OverScrollHeaderStretchConfiguration( + stretchTriggerOffset: widget.stretchTriggerOffset, + onStretchTrigger: widget.onStretchTrigger, + ); + } else { + _stretchConfiguration = null; + } + } + + @override + void initState() { + super.initState(); + _updateSnapConfiguration(); + _updateStretchConfiguration(); + } + + @override + void didUpdateWidget(SliverGFAppBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating) + _updateSnapConfiguration(); + if (widget.stretch != oldWidget.stretch) + _updateStretchConfiguration(); + } + + @override + Widget build(BuildContext context) { + assert(!widget.primary || debugCheckHasMediaQuery(context)); + final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0; + final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null) + ? widget.bottom.preferredSize.height + topPadding : null; + + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: SliverPersistentHeader( + floating: widget.floating, + pinned: widget.pinned, + delegate: _SliverGFAppBarDelegate( + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + title: widget.title, + actions: widget.actions, + flexibleSpace: widget.flexibleSpace, + bottom: widget.bottom, + elevation: widget.elevation, + forceElevated: widget.forceElevated, + backgroundColor: widget.backgroundColor, + brightness: widget.brightness, + iconTheme: widget.iconTheme, + actionsIconTheme: widget.actionsIconTheme, + textTheme: widget.textTheme, + primary: widget.primary, + centerTitle: widget.centerTitle, + titleSpacing: widget.titleSpacing, + expandedHeight: widget.expandedHeight, + collapsedHeight: collapsedHeight, + topPadding: topPadding, + floating: widget.floating, + pinned: widget.pinned, + shape: widget.shape, + snapConfiguration: _snapConfiguration, + stretchConfiguration: _stretchConfiguration, + ), + ), + ); + } +} + +// Layout the GFAppBar's title with unconstrained height, vertically +// center it within its (NavigationToolbar) parent, and allow the +// parent to constrain the title's actual height. +class _GFAppBarTitleBox extends SingleChildRenderObjectWidget { + const _GFAppBarTitleBox({ Key key, @required Widget child }) : assert(child != null), super(key: key, child: child); + + @override + _RenderGFAppBarTitleBox createRenderObject(BuildContext context) { + return _RenderGFAppBarTitleBox( + textDirection: Directionality.of(context), + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderGFAppBarTitleBox renderObject) { + renderObject.textDirection = Directionality.of(context); + } +} + +class _RenderGFAppBarTitleBox extends RenderAligningShiftedBox { + _RenderGFAppBarTitleBox({ + RenderBox child, + TextDirection textDirection, + }) : super(child: child, alignment: Alignment.center, textDirection: textDirection); + + @override + void performLayout() { + final BoxConstraints innerConstraints = constraints.copyWith(maxHeight: double.infinity); + child.layout(innerConstraints, parentUsesSize: true); + size = constraints.constrain(child.size); + alignChild(); + } +} diff --git a/lib/components/button/gf_icon_button.dart b/lib/components/button/gf_icon_button.dart index 678d9434..8f7c5acc 100644 --- a/lib/components/button/gf_icon_button.dart +++ b/lib/components/button/gf_icon_button.dart @@ -87,7 +87,7 @@ class GFIconButton extends StatefulWidget { this.focusNode, this.autofocus = false, this.tooltip, - this.type = GFType.outline, + this.type = GFType.transparent, this.shape = GFButtonShape.standard, this.color = GFColor.primary, this.borderShape, diff --git a/lib/components/search_bar/gf_search_bar.dart b/lib/components/search_bar/gf_search_bar.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/components/segment_tabs/gf_segment_tabs.dart b/lib/components/segment_tabs/gf_segment_tabs.dart new file mode 100644 index 00000000..06b98a34 --- /dev/null +++ b/lib/components/segment_tabs/gf_segment_tabs.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:ui_kit/components/tabs/gf_tabBarView.dart'; + + +class GFSegmentTabs extends StatefulWidget { + GFSegmentTabs({ + Key key, + this.initialIndex = 0, + @required this.length, + this.height, + this.width, + this.border, + this.borderRadius, + this.tabBarColor, + this.indicatorColor, + this.indicatorWeight = 2.0, + this.indicatorPadding = EdgeInsets.zero, + this.indicator, + this.indicatorSize, + this.labelColor, + this.labelStyle, + this.labelPadding, + this.unselectedLabelColor, + this.unselectedLabelStyle, + this.tabBarView, + this.tabs, + this.tabController + }): + assert(length != null && length >= 0), + assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)); + + /// The initial index of the selected tab. Defaults to zero. + final int initialIndex; + + /// The total number of tabs. Typically greater than one. Must match [TabBar.tabs]'s and + /// [TabBarView.children]'s length. + final int length; + + /// Sets [GFSegmentTabs] height + final double height; + + /// Sets [TabBar] color using material color [Color] + final Color tabBarColor; + + /// The color of the line that appears below the selected tab. + /// + /// If this parameter is null, then the value of the Theme's indicatorColor + /// property is used. + /// + /// If [indicator] is specified, this property is ignored. + final Color indicatorColor; + + /// The thickness of the line that appears below the selected tab. + /// + /// The value of this parameter must be greater than zero and its default + /// value is 2.0. + /// + /// If [indicator] is specified, this property is ignored. + final double indicatorWeight; + + /// The horizontal padding for the line that appears below the selected tab. + /// + /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align + /// the indicator with the tab's text for [Tab] widgets and all but the + /// shortest [Tab.text] values. + /// + /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the + /// [indicatorPadding] are ignored. + /// + /// The default value of [indicatorPadding] is [EdgeInsets.zero]. + /// + /// If [indicator] is specified, this property is ignored. + final EdgeInsetsGeometry indicatorPadding; + + /// Defines the appearance of the selected tab indicator. + /// + /// If [indicator] is specified, the [indicatorColor], [indicatorWeight], + /// and [indicatorPadding] properties are ignored. + /// + /// The default, underline-style, selected tab indicator can be defined with + /// [UnderlineTabIndicator]. + /// + /// The indicator's size is based on the tab's bounds. If [indicatorSize] + /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space + /// occupied by the tab in the tab bar. If [indicatorSize] is + /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as + /// the tab widget itself. + final Decoration indicator; + + /// Defines how the selected tab indicator's size is computed. + /// + /// The size of the selected tab indicator is defined relative to the + /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab] + /// (the default) or relative to the bounds of the tab's widget if + /// [indicatorSize] is [TabBarIndicatorSize.label]. + /// + /// The selected tab's location appearance can be refined further with + /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and + /// [indicator] properties. + final TabBarIndicatorSize indicatorSize; + + /// The color of selected tab labels. + /// + /// Unselected tab labels are rendered with the same color rendered at 70% + /// opacity unless [unselectedLabelColor] is non-null. + /// + /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s + /// body2 text color is used. + final Color labelColor; + + /// The color of unselected tab labels. + /// + /// If this property is null, unselected tab labels are rendered with the + /// [labelColor] with 70% opacity. + final Color unselectedLabelColor; + + /// The text style of the selected tab labels. + /// + /// If [unselectedLabelStyle] is null, then this text style will be used for + /// both selected and unselected label styles. + /// + /// If this property is null, then the text style of the + /// [ThemeData.primaryTextTheme]'s body2 definition is used. + final TextStyle labelStyle; + + /// The padding added to each of the tab labels. + /// + /// If this property is null, then kTabLabelPadding is used. + final EdgeInsetsGeometry labelPadding; + + /// The text style of the unselected tab labels + /// + /// If this property is null, then the [labelStyle] value is used. If [labelStyle] + /// is null, then the text style of the [ThemeData.primaryTextTheme]'s + /// body2 definition is used. + final TextStyle unselectedLabelStyle; + + /// One widget per tab. + /// Its length must match the length of the [GFSegmentTabs.tabs] + /// list, as well as the [controller]'s [GFSegmentTabs.length]. + final GFTabBarView tabBarView; + + /// Typically a list of two or more [Tab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [TabBarView.children] list. + final List tabs; + + final Border border; + final BorderRadius borderRadius; + final TabController tabController; + final double width; + + @override + _GFSegmentTabsState createState() => _GFSegmentTabsState(); +} + +class _GFSegmentTabsState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: widget.height == null ? 28.0 : widget.height, + width: widget.width == null ? 240.0 : widget.width, + decoration: BoxDecoration( + border: widget.border == null ? Border.all(color: Colors.black26, width: 2.0) : widget.border, + borderRadius: widget.borderRadius == null ? BorderRadius.circular(8.0) : widget.borderRadius, + ), + child: DefaultTabController( + initialIndex: widget.initialIndex, + length: widget.length, + child: Material( + borderRadius: widget.borderRadius == null ? BorderRadius.circular(8.0) : widget.borderRadius, + type: MaterialType.button, + color: widget.tabBarColor ?? Colors.transparent, + child: TabBar( + controller: widget.tabController, + labelColor: widget.labelColor, + unselectedLabelColor: widget.unselectedLabelColor, + labelStyle: widget.labelStyle, + unselectedLabelStyle: widget.unselectedLabelStyle, + indicatorColor: widget.indicatorColor == null ? Colors.blueGrey : widget.indicatorColor, + indicatorSize: widget.indicatorSize, + indicator: widget.indicator == null ? + BoxDecoration( + color: widget.indicatorColor == null ? Colors.blueGrey : widget.indicatorColor, + border: Border.all(color: widget.indicatorColor == null ? Colors.blueGrey : widget.indicatorColor, width: 2.0), + borderRadius: widget.borderRadius == null ? BorderRadius.circular(6.0) : widget.borderRadius, + ) : widget.indicator, + indicatorPadding: widget.indicatorPadding, + indicatorWeight: widget.indicatorWeight, + tabs: widget.tabs, + ), + ), + ), + ); + } +} + + + + + + + + + + + + + + + + + diff --git a/lib/components/tabs/gf_tabBarView.dart b/lib/components/tabs/gf_tabBarView.dart new file mode 100644 index 00000000..86886f4f --- /dev/null +++ b/lib/components/tabs/gf_tabBarView.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; + +/// A page view that displays the widget which corresponds to the currently +/// selected tab. +/// +/// This widget is typically used in conjunction with a [TabBar]. +/// +/// If a [TabController] is not provided, then there must be a [DefaultTabController] +/// ancestor. +/// +/// The tab controller's [TabController.length] must equal the length of the +/// [children] list and the length of the [TabBar.tabs] list. +/// +/// To see a sample implementation, visit the [TabController] documentation. +class GFTabBarView extends StatefulWidget { + /// Creates a page view with one child per tab. + /// + /// The length of [children] must be the same as the [controller]'s length. + const GFTabBarView({ + Key key, + @required this.children, + this.controller, + this.physics, + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(children != null), + assert(dragStartBehavior != null), + super(key: key); + + /// This widget's selection and animation state. + /// + /// If [TabController] is not provided, then the value of [DefaultTabController.of] + /// will be used. + final TabController controller; + + /// One widget per tab. + /// + /// Its length must match the length of the [TabBar.tabs] + /// list, as well as the [controller]'s [TabController.length]. + final List children; + + /// How the page view should respond to user input. + /// + /// For example, determines how the page view continues to animate after the + /// user stops dragging the page view. + /// + /// The physics are modified to snap to page boundaries using + /// [PageScrollPhysics] prior to being used. + /// + /// Defaults to matching platform conventions. + final ScrollPhysics physics; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + @override + _GFTabBarViewState createState() => _GFTabBarViewState(); +} + +final PageScrollPhysics _kGFTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics()); + +class _GFTabBarViewState extends State { + TabController _controller; + PageController _pageController; + List _children; + List _childrenWithKey; + int _currentIndex; + int _warpUnderwayCount = 0; + + // If the GFTabBarView is rebuilt with a new tab controller, the caller should + // dispose the old one. In that case the old controller's animation will be + // null and should not be accessed. + bool get _controllerIsValid => _controller?.animation != null; + + void _updateTabController() { + final TabController newController = widget.controller ?? DefaultTabController.of(context); + assert(() { + if (newController == null) { + throw FlutterError( + 'No TabController for ${widget.runtimeType}.\n' + 'When creating a ${widget.runtimeType}, you must either provide an explicit ' + 'TabController using the "controller" property, or you must ensure that there ' + 'is a DefaultTabController above the ${widget.runtimeType}.\n' + 'In this case, there was neither an explicit controller nor a default controller.' + ); + } + return true; + }()); + + if (newController == _controller) + return; + + if (_controllerIsValid) + _controller.animation.removeListener(_handleTabControllerAnimationTick); + _controller = newController; + if (_controller != null) + _controller.animation.addListener(_handleTabControllerAnimationTick); + } + + @override + void initState() { + super.initState(); + _updateChildren(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateTabController(); + _currentIndex = _controller?.index; + _pageController = PageController(initialPage: _currentIndex ?? 0); + } + + @override + void didUpdateWidget(GFTabBarView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) + _updateTabController(); + if (widget.children != oldWidget.children && _warpUnderwayCount == 0) + _updateChildren(); + } + + @override + void dispose() { + if (_controllerIsValid) + _controller.animation.removeListener(_handleTabControllerAnimationTick); + _controller = null; + // We don't own the _controller Animation, so it's not disposed here. + super.dispose(); + } + + void _updateChildren() { + _children = widget.children; + _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children); + } + + void _handleTabControllerAnimationTick() { + if (_warpUnderwayCount > 0 || !_controller.indexIsChanging) + return; // This widget is driving the controller's animation. + + if (_controller.index != _currentIndex) { + _currentIndex = _controller.index; + _warpToCurrentIndex(); + } + } + + Future _warpToCurrentIndex() async { + if (!mounted) + return Future.value(); + + if (_pageController.page == _currentIndex.toDouble()) + return Future.value(); + + final int previousIndex = _controller.previousIndex; + if ((_currentIndex - previousIndex).abs() == 1) + return _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); + + assert((_currentIndex - previousIndex).abs() > 1); + final int initialPage = _currentIndex > previousIndex + ? _currentIndex - 1 + : _currentIndex + 1; + final List originalChildren = _childrenWithKey; + setState(() { + _warpUnderwayCount += 1; + + _childrenWithKey = List.from(_childrenWithKey, growable: false); + final Widget temp = _childrenWithKey[initialPage]; + _childrenWithKey[initialPage] = _childrenWithKey[previousIndex]; + _childrenWithKey[previousIndex] = temp; + }); + _pageController.jumpToPage(initialPage); + + await _pageController.animateToPage(_currentIndex, duration: kTabScrollDuration, curve: Curves.ease); + if (!mounted) + return Future.value(); + setState(() { + _warpUnderwayCount -= 1; + if (widget.children != _children) { + _updateChildren(); + } else { + _childrenWithKey = originalChildren; + } + }); + } + + // Called when the PageView scrolls + bool _handleScrollNotification(ScrollNotification notification) { + if (_warpUnderwayCount > 0) + return false; + + if (notification.depth != 0) + return false; + + _warpUnderwayCount += 1; + if (notification is ScrollUpdateNotification && !_controller.indexIsChanging) { + if ((_pageController.page - _controller.index).abs() > 1.0) { + _controller.index = _pageController.page.floor(); + _currentIndex =_controller.index; + } + _controller.offset = (_pageController.page - _controller.index).clamp(-1.0, 1.0); + } else if (notification is ScrollEndNotification) { + _controller.index = _pageController.page.round(); + _currentIndex = _controller.index; + } + _warpUnderwayCount -= 1; + + return false; + } + + @override + Widget build(BuildContext context) { + assert(() { + if (_controller.length != widget.children.length) { + throw FlutterError( + 'Controller\'s length property (${_controller.length}) does not match the \n' + 'number of tabs (${widget.children.length}) present in TabBar\'s tabs property.' + ); + } + return true; + }()); + return NotificationListener( + onNotification: _handleScrollNotification, + child: PageView( + dragStartBehavior: widget.dragStartBehavior, + controller: _pageController, + physics: widget.physics == null ? _kGFTabBarViewPhysics : _kGFTabBarViewPhysics.applyTo(widget.physics), + children: _childrenWithKey, + ), + ); + } +} + diff --git a/lib/components/tabs/gf_tabs.dart b/lib/components/tabs/gf_tabs.dart index 6226d7cf..b1e17584 100644 --- a/lib/components/tabs/gf_tabs.dart +++ b/lib/components/tabs/gf_tabs.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:ui_kit/components/button/gf_button.dart'; - +import 'package:ui_kit/components/tabs/gf_tabBarView.dart'; class GFTabs extends StatefulWidget { GFTabs({ @@ -137,7 +136,7 @@ class GFTabs extends StatefulWidget { /// One widget per tab. /// Its length must match the length of the [GFTabs.tabs] /// list, as well as the [controller]'s [GFTabs.length]. - final TabBarView tabBarView; + final GFTabBarView tabBarView; /// Typically a list of two or more [Tab] widgets. /// @@ -145,7 +144,6 @@ class GFTabs extends StatefulWidget { /// and the length of the [TabBarView.children] list. final List tabs; - @override _GFTabsState createState() => _GFTabsState(); }