Andrii Turkovskyi
Andrii Turkovskyi

Reputation: 29438

How to change SliverAppBar height dynamically

I show image from network, which shrink on scrolling. I want to show whole image without paddings or crops. But if I comment line with expandedHeight - there is no image - only appbar with its height. Is there any widget, which can change its size according to size of uploaded image?

CustomScrollView(
  controller: controller,
  key: listKey,
  slivers: <Widget>[
    SliverAppBar(
    //  expandedHeight: 200.0,
      flexibleSpace: FlexibleSpaceBar(
        background: getHeroWidget(
            _conference.dbId,
            FadeInImage.assetNetwork(
              placeholder: conf_img_placeholder,
              image: _conference.info.image,
              fit: BoxFit.cover,
            )),
        title: Text(conference_title),
        centerTitle: true,
      ),
      pinned: true,
    ),

Upvotes: 4

Views: 9257

Answers (3)

Christian X
Christian X

Reputation: 2187

late answer, but I also had the same requirement for a dynamic height sliver app bar. I finally found a workaround which was working for nearly every situation I needed by faking a sliverappbar, calculating its size and then displaying a real sliverapp bar (so I get access to its cool animations). Enjoy:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:weighlos/ui/shared/app_bar/invisible_expanded_header.dart';

class DynamicSliverAppBar extends StatefulWidget {
  const DynamicSliverAppBar({
    this.flexibleSpace,
    super.key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.bottom,
    this.elevation,
    this.scrolledUnderElevation,
    this.shadowColor,
    this.surfaceTintColor,
    this.forceElevated = false,
    this.backgroundColor,
    this.backgroundGradient,
    this.foregroundColor,
    this.iconTheme,
    this.actionsIconTheme,
    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 + 20,
    this.leadingWidth,
    this.toolbarTextStyle,
    this.titleTextStyle,
    this.systemOverlayStyle,
    this.forceMaterialTransparency = false,
    this.clipBehavior,
    this.appBarClipper,
  });

  final Widget? flexibleSpace;
  final Widget? leading;
  final bool automaticallyImplyLeading;
  final Widget? title;
  final List<Widget>? actions;
  final PreferredSizeWidget? bottom;
  final double? elevation;
  final double? scrolledUnderElevation;
  final Color? shadowColor;
  final Color? surfaceTintColor;
  final bool forceElevated;
  final Color? backgroundColor;

  /// If backgroundGradient is non null, backgroundColor will be ignored
  final LinearGradient? backgroundGradient;
  final Color? foregroundColor;
  final IconThemeData? iconTheme;
  final IconThemeData? actionsIconTheme;
  final bool primary;
  final bool? centerTitle;
  final bool excludeHeaderSemantics;
  final double? titleSpacing;
  final double? expandedHeight;
  final double? collapsedHeight;
  final bool floating;
  final bool pinned;
  final ShapeBorder? shape;
  final double toolbarHeight;
  final double? leadingWidth;
  final TextStyle? toolbarTextStyle;
  final TextStyle? titleTextStyle;
  final SystemUiOverlayStyle? systemOverlayStyle;
  final bool forceMaterialTransparency;
  final Clip? clipBehavior;
  final bool snap;
  final bool stretch;
  final double stretchTriggerOffset;
  final AsyncCallback? onStretchTrigger;
  final CustomClipper<Path>? appBarClipper;

  @override
  _DynamicSliverAppBarState createState() => _DynamicSliverAppBarState();
}

class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
  final GlobalKey _childKey = GlobalKey();

  // As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
  // to calculate dynamically the size for the sliver app bar
  double _height = 0;

  @override
  void initState() {
    super.initState();
    _updateHeight();
  }

  @override
  void didUpdateWidget(covariant DynamicSliverAppBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    _updateHeight();
  }

  void _updateHeight() {
    // Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
    // otherwise this will throw an error
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (_childKey.currentContext == null) return;
      setState(() {
        _height = (_childKey.currentContext!.findRenderObject()! as RenderBox).size.height;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    //Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
    if (_height == 0) {
      return SliverToBoxAdapter(
        child: Stack(
            children: [
              Padding(
                // Padding which centers the flexible space within the app bar
                padding: EdgeInsets.symmetric(vertical: MediaQuery.paddingOf(context).top / 2),
                child: Container(key: _childKey, child: widget.flexibleSpace ?? SizedBox(height: kToolbarHeight)),
              ),
              Positioned.fill(
                // 10 is the magic number which the app bar is pushed down within the sliver app bar. Couldnt find exactly where this number
                // comes from and found it through trial and error.
                top: 10,
                child: Align(
                  alignment: Alignment.topCenter,
                  child: AppBar(
                    backgroundColor: Colors.transparent,
                    elevation: 0,
                    leading: widget.leading,
                    actions: widget.actions,
                  ),
                ),
              )
            ],
          ),
      );
    }

    return SliverAppBar(
      leading: widget.leading,
      automaticallyImplyLeading: widget.automaticallyImplyLeading,
      title: widget.title,
      actions: widget.actions,
      bottom: widget.bottom,
      elevation: widget.elevation,
      scrolledUnderElevation: widget.scrolledUnderElevation,
      shadowColor: widget.shadowColor,
      surfaceTintColor: widget.surfaceTintColor,
      forceElevated: widget.forceElevated,
      backgroundColor: widget.backgroundColor,
      foregroundColor: widget.foregroundColor,
      iconTheme: widget.iconTheme,
      actionsIconTheme: widget.actionsIconTheme,
      primary: widget.primary,
      centerTitle: widget.centerTitle,
      excludeHeaderSemantics: widget.excludeHeaderSemantics,
      titleSpacing: widget.titleSpacing,
      collapsedHeight: widget.collapsedHeight,
      floating: widget.floating,
      pinned: widget.pinned,
      snap: widget.snap,
      stretch: widget.stretch,
      stretchTriggerOffset: widget.stretchTriggerOffset,
      onStretchTrigger: widget.onStretchTrigger,
      shape: widget.shape,
      toolbarHeight: widget.toolbarHeight,
      expandedHeight: _height,
      leadingWidth: widget.leadingWidth,
      toolbarTextStyle: widget.toolbarTextStyle,
      titleTextStyle: widget.titleTextStyle,
      systemOverlayStyle: widget.systemOverlayStyle,
      forceMaterialTransparency: widget.forceMaterialTransparency,
      clipBehavior: widget.clipBehavior,
      flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
    );
  }
}

Upvotes: 1

Azhar Ali
Azhar Ali

Reputation: 2456

I was looking for this problem, I find a solution with NestedScrollView and a plugin SliverStickyHeader

Here is how I did this,

Scaffold(
  body: SafeArea(
    child: NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[
          SliverStickyHeader(
            sticky: false,
            header: Container(
              color: Theme.of(context).primaryColor,
              child: Column(
                children: [
                  Text(
                    'Header',
                    style: Theme.of(context).textTheme.headline6,
                  ),
                  Text(
                    'Header',
                    style: Theme.of(context).textTheme.headline5,
                  ),
                  Text(
                    'Header',
                    style: Theme.of(context).textTheme.headline4,
                  ),
                  Text(
                    'Header',
                    style: Theme.of(context).textTheme.headline3,
                  ),
                ],
              ),
            ),
          )
        ];
      },
      body: Column(
        children: [
          AppBar(
            title: Text('My List items'),
          ),
          Expanded(
            child: ListView.builder(itemBuilder: (context, i) {
              return ListTile(
                leading: CircleAvatar(child: Text('$i')),
                title: Text('Appbar with dynamic height'),
              );
            }),
          ),
        ],
      ),
    ),
  ),
);

and it is fulfilling my requirement, Plugin also support fully Sliver functionality, but I use it with as header only. I hope it will be helpful.

Upvotes: 0

Andrii Turkovskyi
Andrii Turkovskyi

Reputation: 29438

Finally I created custom SliverAppBar. Another issue appears - width of status bar which have to be taken into consideration on calculation.

import 'dart:ui' as ui;

class _HeaderBar extends StatefulWidget {    
  @override
  State<StatefulWidget> createState() => _HeaderState();
}

class _HeaderState extends State<_HeaderBar> {
  @override
  Widget build(BuildContext context) {
    Image image = Image.network(...);
    Completer<ui.Image> completer = new Completer<ui.Image>();
    image.image.resolve(ImageConfiguration()).addListener((ImageInfo info, bool _) {
      completer.complete(info.image);
    });
    final double statusBarHeight = MediaQuery.of(context).padding.top;
    return FutureBuilder(
        future: completer.future,
        builder: (context, AsyncSnapshot<ui.Image> snapshot) {
          return SliverAppBar(
                  expandedHeight: snapshot.hasData
                ? MediaQuery.of(context).size.width / snapshot.data.width.toDouble() * snapshot.data.height.toDouble() -
                    statusBarHeight
                : 0.0,
...

Upvotes: 2

Related Questions