Search code examples
iosflutterdartappbarflutter-sliver

Flutter - Custom sliver app bar with search bar


I want to have a custom Sliver App Bar with a search bar in it. I made an App Bar that looks like this:

app with search bar

But I want that when we scroll down, the app bar will look like this:

app with no search bar, but title remains

Actually, the code of the normal app bar is just a green AppBar of elevation: 0 and just below I add my Header(). Here's the code of my Header :

class Header extends StatefulWidget {
  String title;
  IconData icon;

  Header({@required this.title, @required this.icon});

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

class _HeaderState extends State<Header> {
  TextEditingController _editingController;


  @override
  void initState() {
    super.initState();
    _editingController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return PreferredSize(
      preferredSize: size,
      child: Container(
        margin: EdgeInsets.only(bottom: kDefaultPadding * 2.5),
        height: size.height*0.2,
        child: Stack(
          children: [
            Container(
              height: size.height*0.2-27,
              width: size.width,
              decoration: BoxDecoration(
                  color: Theme.of(context).primaryColor,
                  borderRadius: BorderRadius.only(
                    bottomLeft: Radius.circular(36),
                    bottomRight: Radius.circular(36),
                  )
              ),
              child: Align(
                  alignment: Alignment.topCenter,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(widget.title, style: Theme.of(context).textTheme.headline4.copyWith(color: Colors.white, fontWeight: FontWeight.bold)),
                      SizedBox(width: 20,),
                      Icon(widget.icon, size: 40, color: Colors.white,)
                    ],
                  )),
            ),
            Positioned(
              bottom: 0,
              left: 0,
              right: 0,
              child: Container(
                alignment: Alignment.center,
                margin: EdgeInsets.symmetric(horizontal: kDefaultPadding),
                padding: EdgeInsets.symmetric(horizontal: kDefaultPadding),
                height: 54,
                decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(20),
                    boxShadow: [BoxShadow(
                      offset: Offset(0, 10),
                      blurRadius: 50,
                      color: Theme.of(context).primaryColor.withOpacity(0.23),
                    )]
                ),
                child: Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: _editingController,
                        textAlignVertical: TextAlignVertical.center,
                        onChanged: (_) => setState(() {}),
                        decoration: InputDecoration(
                            hintText: 'Search',
                            hintStyle: TextStyle(color: Theme.of(context).primaryColor.withOpacity(0.5)),
                            enabledBorder: InputBorder.none,
                            focusedBorder: InputBorder.none,
                        ),
                      ),
                    ),
                    _editingController.text.trim().isEmpty ? IconButton(
                        icon: Icon(Icons.search, color: Theme.of(context).primaryColor.withOpacity(0.5)),
                        onPressed: null) :
                    IconButton(
                        highlightColor: Colors.transparent,
                        splashColor: Colors.transparent,
                        icon: Icon(Icons.clear, color: Theme.of(context).primaryColor.withOpacity(0.5)),
                        onPressed: () => setState(() {
                          _editingController.clear();
                        })),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _editingController.dispose();
    super.dispose();
  }
}

Any help to build this is welcomed.


Solution

  • I've made a simple example to show the main logic.

    Create your own SliverPersistentHeaderDelegate and calculate shrinkFactor.

    enter image description here

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white70,
          body: CustomScrollView(
            slivers: [
              SliverPersistentHeader(
                pinned: true,
                floating: false,
                delegate: SearchHeader(
                  icon: Icons.terrain,
                  title: 'Trees',
                  search: _Search(),
                ),
              ),
              SliverFillRemaining(
                hasScrollBody: true,
                child: ListView(
                  physics: NeverScrollableScrollPhysics(),
                  children: [
                    Text('some text'),
                    Placeholder(
                      color: Colors.red,
                      fallbackHeight: 200,
                    ),
                    Container(
                      color: Colors.blueGrey,
                      height: 500,
                    )
                  ],
                ),
              )
            ],
          ),
        );
      }
    }
    
    class _Search extends StatefulWidget {
      _Search({Key key}) : super(key: key);
    
      @override
      __SearchState createState() => __SearchState();
    }
    
    class __SearchState extends State<_Search> {
      TextEditingController _editingController;
    
      @override
      void initState() {
        super.initState();
        _editingController = TextEditingController();
      }
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.only(left: 20, right: 5),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Expanded(
                child: TextField(
                  controller: _editingController,
                  // textAlignVertical: TextAlignVertical.center,
                  onChanged: (_) => setState(() {}),
                  decoration: InputDecoration(
                    hintText: 'Search',
                    hintStyle: TextStyle(
                        color: Theme.of(context).primaryColor.withOpacity(0.5)),
                    enabledBorder: InputBorder.none,
                    focusedBorder: InputBorder.none,
                  ),
                ),
              ),
              _editingController.text.trim().isEmpty
                  ? IconButton(
                      icon: Icon(Icons.search,
                          color: Theme.of(context).primaryColor.withOpacity(0.5)),
                      onPressed: null)
                  : IconButton(
                      highlightColor: Colors.transparent,
                      splashColor: Colors.transparent,
                      icon: Icon(Icons.clear,
                          color: Theme.of(context).primaryColor.withOpacity(0.5)),
                      onPressed: () => setState(
                        () {
                          _editingController.clear();
                        },
                      ),
                    ),
            ],
          ),
        );
      }
    }
    
    class SearchHeader extends SliverPersistentHeaderDelegate {
      final double minTopBarHeight = 100;
      final double maxTopBarHeight = 200;
      final String title;
      final IconData icon;
      final Widget search;
    
      SearchHeader({
        @required this.title,
        this.icon,
        this.search,
      });
    
      @override
      Widget build(
        BuildContext context,
        double shrinkOffset,
        bool overlapsContent,
      ) {
        var shrinkFactor = min(1, shrinkOffset / (maxExtent - minExtent));
    
        var topBar = Positioned(
          top: 0,
          left: 0,
          right: 0,
          child: Container(
            alignment: Alignment.center,
            height:
                max(maxTopBarHeight * (1 - shrinkFactor * 1.45), minTopBarHeight),
            width: 100,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(title,
                    style: Theme.of(context).textTheme.headline4.copyWith(
                        color: Colors.white, fontWeight: FontWeight.bold)),
                SizedBox(
                  width: 20,
                ),
                Icon(
                  icon,
                  size: 40,
                  color: Colors.white,
                )
              ],
            ),
            decoration: BoxDecoration(
                color: Colors.green,
                borderRadius: BorderRadius.only(
                  bottomLeft: Radius.circular(36),
                  bottomRight: Radius.circular(36),
                )),
          ),
        );
        return Container(
          height: max(maxExtent - shrinkOffset, minExtent),
          child: Stack(
            fit: StackFit.loose,
            children: [
              if (shrinkFactor <= 0.5) topBar,
              Align(
                alignment: Alignment.bottomCenter,
                child: Padding(
                  padding: EdgeInsets.only(
                    bottom: 10,
                  ),
                  child: Container(
                    alignment: Alignment.center,
                    child: search,
                    width: 200,
                    height: 50,
                    decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(20),
                        color: Colors.white,
                        boxShadow: [
                          BoxShadow(
                            offset: Offset(0, 10),
                            blurRadius: 10,
                            color: Colors.green.withOpacity(0.23),
                          )
                        ]),
                  ),
                ),
              ),
              if (shrinkFactor > 0.5) topBar,
            ],
          ),
        );
      }
    
      @override
      double get maxExtent => 230;
    
      @override
      double get minExtent => 100;
    
      @override
      bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
    }