Instagram Profile Header Layout In Flutter
Asked Answered
C

2

19

I've been investigating SliverAppBar, CustomScrollView, NestedScrollView, SliverPersistentHeader, and more. I cannot find a way to build something like the Instagram user profile screen's header where only the tab bar is pinned. The main body of the screen is a TabBarView and each pane has a scrollable list.

With SliverAppBar, it is easy to add the TabBar in the bottom parameter. But I want to have an extra widget of unknown/variable height above that TabBar. The extra widget should scroll out of the way when the page is scrolled and and then the TabBar is what is pinned at the top of the screen.

enter image description here

All I could manage was a fixed content before the tab bar and a fixed tab bar. I cannot get the header to scroll up and stick the TabBar at the top just just below the AppBar.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text("pabloaleko"),
        ),
        body: CustomScrollView(
          physics: const BouncingScrollPhysics(),
          slivers: <Widget>[
            SliverToBoxAdapter(
              child: SafeArea(
                child: Text("an unknown\namount of content\n goes here in the header"),
              ),
            ),
            SliverToBoxAdapter(
              child: TabBar(
                tabs: [
                  Tab(child: Text('Days', style: TextStyle(color: Colors.black))),
                  Tab(child: Text('Months', style: TextStyle(color: Colors.black))),
                ],
              ),
            ),
            SliverFillRemaining(
              child: TabBarView(
                children: [
                  ListView(
                    children: <Widget>[
                      ListTile(title: Text('Sunday 1')),
                      ListTile(title: Text('Monday 2')),
                      ListTile(title: Text('Tuesday 3')),
                      ListTile(title: Text('Wednesday 4')),
                      ListTile(title: Text('Thursday 5')),
                      ListTile(title: Text('Friday 6')),
                      ListTile(title: Text('Saturday 7')),
                      ListTile(title: Text('Sunday 8')),
                      ListTile(title: Text('Monday 9')),
                      ListTile(title: Text('Tuesday 10')),
                      ListTile(title: Text('Wednesday 11')),
                      ListTile(title: Text('Thursday 12')),
                      ListTile(title: Text('Friday 13')),
                      ListTile(title: Text('Saturday 14')),
                    ],
                  ),
                  ListView(
                    children: <Widget>[
                      ListTile(title: Text('January')),
                      ListTile(title: Text('February')),
                      ListTile(title: Text('March')),
                      ListTile(title: Text('April')),
                      ListTile(title: Text('May')),
                      ListTile(title: Text('June')),
                      ListTile(title: Text('July')),
                      ListTile(title: Text('August')),
                      ListTile(title: Text('September')),
                      ListTile(title: Text('October')),
                      ListTile(title: Text('November')),
                      ListTile(title: Text('December')),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

enter image description here

Cutthroat answered 24/1, 2020 at 22:56 Comment(0)
E
53

You can achieve this behaviour using NestedScrollView with Scaffold.

As we need the widgets between the AppBar and TabBar to be dynamically built and scrolled until TabBar reaches AppBar, use the appBar property of the Scaffold to build your AppBar and use headerSliverBuilder to build other widgets of unknown heights. Use the body property of NestedScrollView to build your tab views.

This way the elements of the headerSliverBuilder would scroll away till the body reaches the bottom of the AppBar.

Might be a little confusing to understand with mere words, here is an example for you.

Code:

// InstaProfilePage
class InstaProfilePage extends StatefulWidget {
  @override
  _InstaProfilePageState createState() => _InstaProfilePageState();
}

class _InstaProfilePageState extends State<InstaProfilePage> {
  double get randHeight => Random().nextInt(100).toDouble();

  List<Widget> _randomChildren;

  // Children with random heights - You can build your widgets of unknown heights here
  // I'm just passing the context in case if any widgets built here needs  access to context based data like Theme or MediaQuery
  List<Widget> _randomHeightWidgets(BuildContext context) {
    _randomChildren ??= List.generate(3, (index) {
      final height = randHeight.clamp(
        50.0,
        MediaQuery.of(context).size.width, // simply using MediaQuery to demonstrate usage of context
      );
      return Container(
        color: Colors.primaries[index],
        height: height,
        child: Text('Random Height Child ${index + 1}'),
      );
    });

    return _randomChildren;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Persistent AppBar that never scrolls
      appBar: AppBar(
        title: Text('AppBar'),
        elevation: 0.0,
      ),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          // allows you to build a list of elements that would be scrolled away till the body reached the top
          headerSliverBuilder: (context, _) {
            return [
              SliverList(
                delegate: SliverChildListDelegate(
                  _randomHeightWidgets(context),
                ),
              ),
            ];
          },
          // You tab view goes here
          body: Column(
            children: <Widget>[
              TabBar(
                tabs: [
                  Tab(text: 'A'),
                  Tab(text: 'B'),
                ],
              ),
              Expanded(
                child: TabBarView(
                  children: [
                    GridView.count(
                      padding: EdgeInsets.zero,
                      crossAxisCount: 3,
                      children: Colors.primaries.map((color) {
                        return Container(color: color, height: 150.0);
                      }).toList(),
                    ),
                    ListView(
                      padding: EdgeInsets.zero,
                      children: Colors.primaries.map((color) {
                        return Container(color: color, height: 150.0);
                      }).toList(),
                    )
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Output:

Output Image

Hope this helps!

Ephedrine answered 30/1, 2020 at 7:56 Comment(5)
This is a nice solution.Why are "A" and "B" tab text showing up as white on white in my app but black on white in your app?Cutthroat
Hey, sorry about that. I had my Theme's primary color set to Colors.white. It basically depends on the primaryColor and accentColor of your . ThemeEphedrine
Hey!! @HemanthRaj I tried this method, but I am stuck with this issue if you please could check it out Bottom overflow due to bottom navigation bar and tab Bar https://mcmap.net/q/665285/-bottom-overflow-due-to-bottom-navigation-bar-and-tab-bar/13406343?sem=2Pyelitis
How can you implement swipe refresh for the top NestedScrollView??Perspiration
The scrolling still happens even if tab view elements don't cover up the whole screen. Any solution for this?Henbit
K
4

Another solution is that you could use a pinned SliverAppBar with FlexibleSpaceBar within DefaultTabController. Sample codes:

  Scaffold(
    body: DefaultTabController(
      length: 2,
      child: NestedScrollView(
        headerSliverBuilder: (context, value) {
          return [
            SliverAppBar(
              floating: true,
              pinned: true,
              bottom: TabBar(
                tabs: [
                  Tab(text: "Posts"),
                  Tab(text: "Likes"),
                ],
              ),
              expandedHeight: 450,
              flexibleSpace: FlexibleSpaceBar(
                collapseMode: CollapseMode.pin,
                background: Profile(), // This is where you build the profile part
              ),
            ),
          ];
        },
        body: TabBarView(
          children: [
            Container(
              child: ListView.builder(
                itemCount: 100,
                itemBuilder: (context, index) {
                  return Container(
                    height: 40,
                    alignment: Alignment.center,
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: Text('List Item $index'),
                  );
                },
              ),
            ),
            Container(
              child: ListView.builder(
                itemCount: 100,
                itemBuilder: (context, index) {
                  return Container(
                    height: 40,
                    alignment: Alignment.center,
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: Text('List Item $index'),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    ),
  ),

Before scrolling:

enter image description here

After scrolling:

enter image description here

Khufu answered 5/9, 2020 at 8:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.