diff --git a/app/lib/widget/home_app_bar.dart b/app/lib/widget/home_app_bar.dart index ac3a0f4c..8dd61c20 100644 --- a/app/lib/widget/home_app_bar.dart +++ b/app/lib/widget/home_app_bar.dart @@ -12,6 +12,7 @@ import 'package:nc_photos/widget/account_picker_dialog.dart'; import 'package:nc_photos/widget/app_bar_circular_progress_indicator.dart'; import 'package:nc_photos/widget/app_bar_title_container.dart'; import 'package:nc_photos/widget/settings.dart'; +import 'package:nc_photos/widget/translucent_sliver_app_bar.dart'; /// AppBar for home screens class HomeSliverAppBar extends StatelessWidget { @@ -27,7 +28,7 @@ class HomeSliverAppBar extends StatelessWidget { @override build(BuildContext context) { final accountLabel = AccountPref.of(account).getAccountLabel(); - return SliverAppBar( + return TranslucentSliverAppBar( title: InkWell( onTap: () { showDialog( @@ -53,6 +54,8 @@ class HomeSliverAppBar extends StatelessWidget { )), ), ), + scrolledUnderBackgroundColor: + Theme.of(context).homeNavigationBarBackgroundColor, floating: true, automaticallyImplyLeading: false, actions: (actions ?? []) + diff --git a/app/lib/widget/translucent_sliver_app_bar.dart b/app/lib/widget/translucent_sliver_app_bar.dart new file mode 100644 index 00000000..5338f3bc --- /dev/null +++ b/app/lib/widget/translucent_sliver_app_bar.dart @@ -0,0 +1,708 @@ +// ignore_for_file: deprecated_member_use, unnecessary_null_comparison, curly_braces_in_flow_control_structures, deprecated_member_use_from_same_package + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +/// A translucent sliver app bar used in home pages +/// +/// This calss is adopted from Flutter's SliverAppBar +class TranslucentSliverAppBar 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 TranslucentSliverAppBar({ + Key? key, + this.leading, + this.automaticallyImplyLeading = true, + this.title, + this.actions, + this.flexibleSpace, + this.bottom, + this.elevation, + this.shadowColor, + this.surfaceTintColor, + this.forceElevated = false, + this.backgroundColor, + this.scrolledUnderBackgroundColor, + this.foregroundColor, + @Deprecated( + 'This property is no longer used, please use systemOverlayStyle instead. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + this.brightness, + this.iconTheme, + this.actionsIconTheme, + @Deprecated( + 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + this.textTheme, + this.primary = true, + this.centerTitle, + this.excludeHeaderSemantics = false, + this.titleSpacing, + this.collapsedHeight, + this.expandedHeight, + this.floating = false, + this.pinned = false, + this.snap = false, + this.stretch = false, + this.stretchTriggerOffset = 100.0, + this.onStretchTrigger, + this.shape, + this.toolbarHeight = kToolbarHeight, + this.leadingWidth, + @Deprecated( + 'This property is obsolete and is false by default. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + this.backwardsCompatibility, + this.toolbarTextStyle, + this.titleTextStyle, + this.systemOverlayStyle, + }) : assert(automaticallyImplyLeading != null), + assert(forceElevated != null), + assert(primary != null), + assert(floating != null), + assert(pinned != null), + assert(snap != null), + assert(stretch != null), + assert(toolbarHeight != null), + assert(floating || !snap, + 'The "snap" argument only makes sense for floating app bars.'), + assert(stretchTriggerOffset > 0.0), + assert(collapsedHeight == null || collapsedHeight >= toolbarHeight, + 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].'), + super(key: key); + + /// {@macro flutter.material.appbar.leading} + /// + /// This property is used to configure an [AppBar]. + final Widget? leading; + + /// {@macro flutter.material.appbar.automaticallyImplyLeading} + /// + /// This property is used to configure an [AppBar]. + final bool automaticallyImplyLeading; + + /// {@macro flutter.material.appbar.title} + /// + /// This property is used to configure an [AppBar]. + final Widget? title; + + /// {@macro flutter.material.appbar.actions} + /// + /// This property is used to configure an [AppBar]. + final List? actions; + + /// {@macro flutter.material.appbar.flexibleSpace} + /// + /// This property is used to configure an [AppBar]. + final Widget? flexibleSpace; + + /// {@macro flutter.material.appbar.bottom} + /// + /// This property is used to configure an [AppBar]. + final PreferredSizeWidget? bottom; + + /// {@macro flutter.material.appbar.elevation} + /// + /// This property is used to configure an [AppBar]. + final double? elevation; + + /// {@macro flutter.material.appbar.shadowColor} + /// + /// This property is used to configure an [AppBar]. + final Color? shadowColor; + + /// {@macro flutter.material.appbar.surfaceTintColor} + /// + /// This property is used to configure an [AppBar]. + final Color? surfaceTintColor; + + /// Whether to show the shadow appropriate for the [elevation] even if the + /// content is not scrolled under the [AppBar]. + /// + /// Defaults to false, meaning that the [elevation] is only applied when the + /// [AppBar] 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; + + /// {@macro flutter.material.appbar.backgroundColor} + /// + /// This property is used to configure an [AppBar]. + final Color? backgroundColor; + + final Color? scrolledUnderBackgroundColor; + + /// {@macro flutter.material.appbar.foregroundColor} + /// + /// This property is used to configure an [AppBar]. + final Color? foregroundColor; + + /// {@macro flutter.material.appbar.brightness} + /// + /// This property is used to configure an [AppBar]. + @Deprecated( + 'This property is no longer used, please use systemOverlayStyle instead. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + final Brightness? brightness; + + /// {@macro flutter.material.appbar.iconTheme} + /// + /// This property is used to configure an [AppBar]. + final IconThemeData? iconTheme; + + /// {@macro flutter.material.appbar.actionsIconTheme} + /// + /// This property is used to configure an [AppBar]. + final IconThemeData? actionsIconTheme; + + /// {@macro flutter.material.appbar.textTheme} + /// + /// This property is used to configure an [AppBar]. + @Deprecated( + 'This property is no longer used, please use toolbarTextStyle and titleTextStyle instead. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + final TextTheme? textTheme; + + /// {@macro flutter.material.appbar.primary} + /// + /// This property is used to configure an [AppBar]. + final bool primary; + + /// {@macro flutter.material.appbar.centerTitle} + /// + /// This property is used to configure an [AppBar]. + final bool? centerTitle; + + /// {@macro flutter.material.appbar.excludeHeaderSemantics} + /// + /// This property is used to configure an [AppBar]. + final bool excludeHeaderSemantics; + + /// {@macro flutter.material.appbar.titleSpacing} + /// + /// This property is used to configure an [AppBar]. + final double? titleSpacing; + + /// Defines the height of the app bar when it is collapsed. + /// + /// By default, the collapsed height is [toolbarHeight]. If [bottom] widget is + /// specified, then its height from [PreferredSizeWidget.preferredSize] is + /// added to the height. If [primary] is true, then the [MediaQuery] top + /// padding, [EdgeInsets.top] of [MediaQueryData.padding], is added as well. + /// + /// If [pinned] and [floating] are true, with [bottom] set, the default + /// collapsed height is only the height of [PreferredSizeWidget.preferredSize] + /// with the [MediaQuery] top padding. + final double? collapsedHeight; + + /// 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: + /// + /// * [SliverAppBar] 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: + /// + /// * [SliverAppBar] for more animated examples of how this property changes the + /// behavior of the app bar in combination with [floating]. + final bool pinned; + + /// {@macro flutter.material.appbar.shape} + /// + /// This property is used to configure an [AppBar]. + 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. Additionally, setting [snap] to true will fully + /// expand the floating app bar when the framework tries to reveal the + /// contents of the app bar by calling [RenderObject.showOnScreen]. For + /// example, when a [TextField] in the floating app bar gains focus, if [snap] + /// is true, the framework will always fully expand the floating app bar, in + /// order to reveal the focused [TextField]. + /// + /// 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: + /// + /// * [SliverAppBar] 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; + + /// {@macro flutter.material.appbar.toolbarHeight} + /// + /// This property is used to configure an [AppBar]. + final double toolbarHeight; + + /// {@macro flutter.material.appbar.leadingWidth} + /// + /// This property is used to configure an [AppBar]. + final double? leadingWidth; + + /// {@macro flutter.material.appbar.backwardsCompatibility} + /// + /// This property is used to configure an [AppBar]. + @Deprecated( + 'This property is obsolete and is false by default. ' + 'This feature was deprecated after v2.4.0-0.0.pre.', + ) + final bool? backwardsCompatibility; + + /// {@macro flutter.material.appbar.toolbarTextStyle} + /// + /// This property is used to configure an [AppBar]. + final TextStyle? toolbarTextStyle; + + /// {@macro flutter.material.appbar.titleTextStyle} + /// + /// This property is used to configure an [AppBar]. + final TextStyle? titleTextStyle; + + /// {@macro flutter.material.appbar.systemOverlayStyle} + /// + /// This property is used to configure an [AppBar]. + final SystemUiOverlayStyle? systemOverlayStyle; + + @override + State createState() => _SliverAppBarState(); +} + +class _SliverAppBarState extends State + with TickerProviderStateMixin { + FloatingHeaderSnapConfiguration? _snapConfiguration; + OverScrollHeaderStretchConfiguration? _stretchConfiguration; + PersistentHeaderShowOnScreenConfiguration? _showOnScreenConfiguration; + + void _updateSnapConfiguration() { + if (widget.snap && widget.floating) { + _snapConfiguration = FloatingHeaderSnapConfiguration( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + ); + } else { + _snapConfiguration = null; + } + + _showOnScreenConfiguration = widget.floating & widget.snap + ? const PersistentHeaderShowOnScreenConfiguration( + minShowOnScreenExtent: double.infinity) + : 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(TranslucentSliverAppBar 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 bottomHeight = widget.bottom?.preferredSize.height ?? 0.0; + final double topPadding = + widget.primary ? MediaQuery.of(context).padding.top : 0.0; + final double collapsedHeight = + (widget.pinned && widget.floating && widget.bottom != null) + ? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding + : (widget.collapsedHeight ?? widget.toolbarHeight) + + bottomHeight + + topPadding; + + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: SliverPersistentHeader( + floating: widget.floating, + pinned: widget.pinned, + delegate: _SliverAppBarDelegate( + vsync: this, + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + title: widget.title, + actions: widget.actions, + flexibleSpace: widget.flexibleSpace, + bottom: widget.bottom, + elevation: widget.elevation, + shadowColor: widget.shadowColor, + surfaceTintColor: widget.surfaceTintColor, + forceElevated: widget.forceElevated, + backgroundColor: widget.backgroundColor, + scrolledUnderBackgroundColor: widget.scrolledUnderBackgroundColor, + foregroundColor: widget.foregroundColor, + brightness: widget.brightness, + iconTheme: widget.iconTheme, + actionsIconTheme: widget.actionsIconTheme, + textTheme: widget.textTheme, + primary: widget.primary, + centerTitle: widget.centerTitle, + excludeHeaderSemantics: widget.excludeHeaderSemantics, + titleSpacing: widget.titleSpacing, + expandedHeight: widget.expandedHeight, + collapsedHeight: collapsedHeight, + topPadding: topPadding, + floating: widget.floating, + pinned: widget.pinned, + shape: widget.shape, + snapConfiguration: _snapConfiguration, + stretchConfiguration: _stretchConfiguration, + showOnScreenConfiguration: _showOnScreenConfiguration, + toolbarHeight: widget.toolbarHeight, + leadingWidth: widget.leadingWidth, + backwardsCompatibility: widget.backwardsCompatibility, + toolbarTextStyle: widget.toolbarTextStyle, + titleTextStyle: widget.titleTextStyle, + systemOverlayStyle: widget.systemOverlayStyle, + ), + ), + ); + } +} + +class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { + _SliverAppBarDelegate({ + required this.leading, + required this.automaticallyImplyLeading, + required this.title, + required this.actions, + required this.flexibleSpace, + required this.bottom, + required this.elevation, + required this.shadowColor, + required this.surfaceTintColor, + required this.forceElevated, + required this.backgroundColor, + required this.scrolledUnderBackgroundColor, + required this.foregroundColor, + required this.brightness, + required this.iconTheme, + required this.actionsIconTheme, + required this.textTheme, + required this.primary, + required this.centerTitle, + required this.excludeHeaderSemantics, + required this.titleSpacing, + required this.expandedHeight, + required this.collapsedHeight, + required this.topPadding, + required this.floating, + required this.pinned, + required this.vsync, + required this.snapConfiguration, + required this.stretchConfiguration, + required this.showOnScreenConfiguration, + required this.shape, + required this.toolbarHeight, + required this.leadingWidth, + required this.backwardsCompatibility, + required this.toolbarTextStyle, + required this.titleTextStyle, + required this.systemOverlayStyle, + }) : assert(primary || topPadding == 0.0), + assert( + !floating || + (snapConfiguration == null && + showOnScreenConfiguration == null) || + vsync != null, + 'vsync cannot be null when snapConfiguration or showOnScreenConfiguration is not null, and floating is true', + ), + _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 Color? shadowColor; + final Color? surfaceTintColor; + final bool forceElevated; + final Color? backgroundColor; + final Color? scrolledUnderBackgroundColor; + final Color? foregroundColor; + final Brightness? brightness; + final IconThemeData? iconTheme; + final IconThemeData? actionsIconTheme; + final TextTheme? textTheme; + final bool primary; + final bool? centerTitle; + final bool excludeHeaderSemantics; + final double? titleSpacing; + final double? expandedHeight; + final double collapsedHeight; + final double topPadding; + final bool floating; + final bool pinned; + final ShapeBorder? shape; + final double? toolbarHeight; + final double? leadingWidth; + final bool? backwardsCompatibility; + final TextStyle? toolbarTextStyle; + final TextStyle? titleTextStyle; + final SystemUiOverlayStyle? systemOverlayStyle; + final double _bottomHeight; + + @override + double get minExtent => collapsedHeight; + + @override + double get maxExtent => math.max( + topPadding + + (expandedHeight ?? (toolbarHeight ?? kToolbarHeight) + _bottomHeight), + minExtent); + + @override + final TickerProvider vsync; + + @override + final FloatingHeaderSnapConfiguration? snapConfiguration; + + @override + final OverScrollHeaderStretchConfiguration? stretchConfiguration; + + @override + final PersistentHeaderShowOnScreenConfiguration? showOnScreenConfiguration; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + final double visibleMainHeight = maxExtent - shrinkOffset - topPadding; + final double extraToolbarHeight = math.max( + minExtent - + _bottomHeight - + topPadding - + (toolbarHeight ?? kToolbarHeight), + 0.0); + final double visibleToolbarHeight = + visibleMainHeight - _bottomHeight - extraToolbarHeight; + + final bool isScrolledUnder = + overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent); + final bool isPinnedWithOpacityFade = + pinned && floating && bottom != null && extraToolbarHeight == 0.0; + final double toolbarOpacity = !pinned || isPinnedWithOpacityFade + ? (visibleToolbarHeight / (toolbarHeight ?? 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, + isScrolledUnder: isScrolledUnder, + child: Stack( + children: [ + SizedBox( + width: double.infinity, + height: math.max(minExtent, maxExtent - shrinkOffset), + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + child: const ColoredBox( + color: Colors.transparent, + ), + ), + ), + ), + AppBar( + leading: leading, + automaticallyImplyLeading: automaticallyImplyLeading, + title: title, + actions: actions, + flexibleSpace: (title == null && + flexibleSpace != null && + !excludeHeaderSemantics) + ? Semantics( + header: true, + child: flexibleSpace, + ) + : flexibleSpace, + bottom: bottom, + elevation: forceElevated || isScrolledUnder ? elevation : 0.0, + scrolledUnderElevation: 0, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + backgroundColor: isScrolledUnder + ? scrolledUnderBackgroundColor + : backgroundColor, + foregroundColor: foregroundColor, + brightness: brightness, + iconTheme: iconTheme, + actionsIconTheme: actionsIconTheme, + textTheme: textTheme, + primary: primary, + centerTitle: centerTitle, + excludeHeaderSemantics: excludeHeaderSemantics, + titleSpacing: titleSpacing, + shape: shape, + toolbarOpacity: toolbarOpacity, + bottomOpacity: pinned + ? 1.0 + : ((visibleMainHeight / _bottomHeight).clamp(0.0, 1.0)), + toolbarHeight: toolbarHeight, + leadingWidth: leadingWidth, + backwardsCompatibility: backwardsCompatibility, + toolbarTextStyle: toolbarTextStyle, + titleTextStyle: titleTextStyle, + systemOverlayStyle: systemOverlayStyle, + ), + ], + ), + ); + return appBar; + } + + @override + bool shouldRebuild(covariant _SliverAppBarDelegate 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 || + shadowColor != oldDelegate.shadowColor || + backgroundColor != oldDelegate.backgroundColor || + scrolledUnderBackgroundColor != + oldDelegate.scrolledUnderBackgroundColor || + foregroundColor != oldDelegate.foregroundColor || + 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 || + vsync != oldDelegate.vsync || + snapConfiguration != oldDelegate.snapConfiguration || + stretchConfiguration != oldDelegate.stretchConfiguration || + showOnScreenConfiguration != oldDelegate.showOnScreenConfiguration || + forceElevated != oldDelegate.forceElevated || + toolbarHeight != oldDelegate.toolbarHeight || + leadingWidth != oldDelegate.leadingWidth || + backwardsCompatibility != oldDelegate.backwardsCompatibility || + toolbarTextStyle != oldDelegate.toolbarTextStyle || + titleTextStyle != oldDelegate.titleTextStyle || + systemOverlayStyle != oldDelegate.systemOverlayStyle; + } + + @override + String toString() { + return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)'; + } +}