Flutter - Custom sliver app bar with search bar

3,289

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;
}
Share:
3,289
Simon B
Author by

Simon B

Updated on December 23, 2022

Comments

  • Simon B
    Simon B over 1 year

    I want to have a custom sliver appBar with a search bar in it. I made a normal app bar that looks like this :enter image description here But I want that when we scroll down, the app bar looks like that :

    enter image description here

    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 welcome.

    • pskink
      pskink over 3 years
    • Simon B
      Simon B over 3 years
      It doesn't help me because I want a rounded corner app bar with a search bar... I know how to do a simple sliver app bar with a text and picture in background...
    • pskink
      pskink over 3 years
      use SliverPersistentHeader then