flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.02k stars 27.19k forks source link

Add option to only expand floating SliverAppBar when scrolled to the top #146822

Open madmini opened 5 months ago

madmini commented 5 months ago

Use case

Having a SliverAppBar with pinned: false, floating: true and a not-null expandedHeight, the AppBar is collapsed and hidden when scrolling down and pops in and is expanded again when scrolling up.

However, I would like for it to stay collapsed until the scroll view is scrolled all the way to the top again, at which point it should expand again.

See also this StackOverflow post asking how to achieve this, including two flawed workarounds in the answers.

Proposal

Implement this behavior, with a corresponding SliverAppBar parameter (for instance bool expandOnlyAtTop = false) that enables it.

darshankawar commented 5 months ago

Having a SliverAppBar with pinned: false, floating: true and a not-null expandedHeight, the AppBar is collapsed and hidden when scrolling down and pops in and is expanded again when scrolling up.

@madmini Can you provide a runnable code sample that shows the current behavior and what's the expected behavior you are looking for having the proposed option ?

madmini commented 5 months ago

current behavior

scroll far down: appbar is hidden scroll up a bit: appbar is shown and expands

runnable code for current behavior: test in dartpad (inconsistent behavior of scrollbar in web on desktop (only use scroll wheel), open dartpad on android for best experience)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(
        body: Center(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

class MyStatefulWidget extends StatelessWidget {
  const MyStatefulWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            floating: true,
            expandedHeight: 200,
            actions: [
              TextButton(
                onPressed: () {},
                child: const Text('test'),
              ),
            ],
            flexibleSpace: FlexibleSpaceBar(
              background: const FlutterLogo(),
            ),
          ),
          SliverList.list(
            children: [
              for (var i = 0; i < 50; i++)
                Container(
                  height: kToolbarHeight,
                  width: double.infinity,
                  color: i.isEven ? Colors.green : Colors.blue,
                ),
            ],
          ),
        ],
      ),
    );
  }
}

intended behavior

scroll far down: appbar is hidden scroll up a bit: appbar is shown but does not expand scroll to the top: appbar expands

runnable code (workaround) for intended behavior: test in dartpad (see also my answer on stackoverflow. also borked on desktop, use android for best experience)

import 'package:flutter/material.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(
        body: Center(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  late ScrollController _scrollController;
  // variable height passed to SliverAppBar expanded height
  late double? _expandedHeight;

  // constant more height that is given to the expandedHeight
  // of the SliverAppBar
  final double _moreHeight = 200;

  @override
  initState() {
    super.initState();
    // initialize and add scroll listener
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListen);
    // initially expanded height is full
    _expandedHeight = _moreHeight;
  }

  @override
  dispose() {
    // dispose the scroll listener and controller
    _scrollController.removeListener(_scrollListen);
    _scrollController.dispose();
    super.dispose();
  }

  void _scrollListen() {
    final pos = _scrollController.position;
    final offset = pos.pixels;
    if (_expandedHeight == null) {
      if (offset == 0) {
        // AppBar is collapsed and user scrolls to top => enable expansion
        setState(() => _expandedHeight = _moreHeight);
        // but reset scroll position to avoid jump
        pos.correctPixels(_moreHeight - kToolbarHeight);
      }
    } else {
      if (offset > _moreHeight - kToolbarHeight) {
        // AppBar is expandable and user has collapsed it by scrolling => disable expansion
        setState(() => _expandedHeight = null);
        // but reset scroll position to avoid jump
        pos.correctPixels(0);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: _scrollController,
        slivers: <Widget>[
          SliverAppBar(
            floating: true,
            expandedHeight: _expandedHeight,
            actions: [
              TextButton(
                onPressed: () {},
                child: const Text('test'),
              ),
            ],
            flexibleSpace: FlexibleSpaceBar(
              // animate the opacity offset when expanded height is changed
              background: AnimatedOpacity(
                opacity: _expandedHeight != null
                    ? _expandedHeight! / _moreHeight
                    : 0,
                duration: const Duration(milliseconds: 300),
                child: const FlutterLogo(),
              ),
            ),
          ),
          SliverList.list(
            children: [
              for (var i = 0; i < 50; i++)
                Container(
                  height: kToolbarHeight,
                  width: double.infinity,
                  color: i.isEven ? Colors.green : Colors.blue,
                ),
            ],
          ),
        ],
      ),
    );
  }
}
darshankawar commented 5 months ago

Thanks for the update @madmini Can you take a look at https://stackoverflow.com/questions/57572037/my-sliverappbar-doesnt-expand-when-i-start-scrolling-back-up-it-only-expands-w and see if it helps in your case or not ? Also, the official documentation has examples with floating, pinned and snap properties that you can take a look and see if it helps further in your case or not. https://api.flutter.dev/flutter/material/SliverAppBar-class.html. (look for Animated Examples section)

madmini commented 5 months ago

I have looked at these properties and know their respective effects. I do not want the appbar to hide when scrolling away from the appbar and reappear when scrolling towards it, and I do not want it to snap. The only combination which achieves this is floating = true, pinned = false, snap = false. However, in addition to this I also want the appbar to only fully expand at the top, like sketched in the runnable code example for the intended behavior in my comment above.

darshankawar commented 5 months ago

Thanks for the update.

Piinks commented 4 months ago

cc @HansMuller