Flutter Listview swipe to change TabBar index

Viewed 3114

I have implemented TabBar without TabBarView. I am using a single ListView as body since the layout after selecting a tab is same for all tabs.

What I want to achieve is, change the tab while swiping left / right in the listview. How can I do this?

TabBar

TabBar(
        indicatorWeight: 3,
        indicatorSize: TabBarIndicatorSize.label,
        onTap: (index) {
          categoryId = newsProvider.categories[index].id;
          page = 1;
          fetchPosts(newsProvider);
        },
        isScrollable: true,
        tabs: [
          for (Category category in newsProvider.categories)
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
              child: Text(
                category.name,
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
              ),
            ),
        ],
      ),

body

ListView.builder(
                    padding: EdgeInsets.only(bottom: 60),
                    physics: BouncingScrollPhysics(),
                    controller: _scrollController,
                    itemCount: newsProvider.posts.length,
                    itemBuilder: (context, index) {
                      return GestureDetector(
                        onTap: () {
                          Navigator.of(context).push(MaterialPageRoute(
                            builder: (BuildContext context) =>
                                HabaruDetails(newsProvider.posts[index]),
                          ));
                        },
                        child: Container(
                          height: 200,
                          margin: EdgeInsets.only(left: 10, right: 10, top: 10),
                          child: ClipRRect(
                            borderRadius: BorderRadius.circular(7),
                            child: Stack(
                              children: [
                                Positioned.fill(
                                  child: Hero(
                                    tag: newsProvider.posts[index].id,
                                    child: FadeInImage.memoryNetwork(
                                        placeholder: kTransparentImage,
                                        fit: BoxFit.cover,
                                        image: newsProvider
                                            .posts[index].betterFeaturedImage.mediumLarge),
                                  ),
                                ),
                                Container(
                                  height: 200,
                                  decoration: BoxDecoration(
                                      color: Colors.white,
                                      gradient: LinearGradient(
                                          begin: FractionalOffset.topCenter,
                                          end: FractionalOffset.bottomCenter,
                                          colors: [
                                            Colors.black.withOpacity(0.0),
                                            Colors.black.withOpacity(0.95),
                                          ],
                                          stops: [
                                            0.0,
                                            1.0
                                          ])),
                                ),
                                Positioned.fill(
                                  child: Align(
                                    alignment: Alignment.bottomCenter,
                                    child: Padding(
                                      padding: const EdgeInsets.symmetric(
                                          horizontal: 16, vertical: 30),
                                      child: Text(
                                        newsProvider.posts[index].title.rendered,
                                        textAlign: TextAlign.center,
                                        textDirection: TextDirection.rtl,
                                        style: TextStyle(
                                          fontSize: 24,
                                          fontWeight: FontWeight.bold,
                                          color: Colors.white,
                                          height: 1.7,
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                      );
                    },
                  )
3 Answers

I think you still must use a TabBarView, but you can generate its children dynamically based on the categories list like below.

@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: [...],
          ),
        ),
        body: TabBarView(
          children: newsProvider.categories.map(
                (e) => ListView.builder(...).toList(),
          ),
        ),
      ),
    ),
  );
}

From what I can tell, I think you're trying to create a tab bar that moves when the user scrolls down on the listview? If so, I have created an example of a class that uses a selection of tabs you can implement as a TabBar for your title in your AppBar whose controller is set to the TabBarController I have created above the build method. The listview is then set to the controller of the scrollcontroller which listens to the state of the tabbarcontroller.

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,

        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {

  TabController _tabController;
  ScrollController _scrollController;


  void _scrollListener() {
    var index = (_scrollController.offset / 70).round();
    if(index >= choices.length){
      index = choices.lastIndexOf(choices.last);
    }
    if(index <= 0){
      index = 0;
    }
    if (index == choices.length)
      index = choices.length;
    if(mounted){
      setState(() {
        _tabController.animateTo(index, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
      });
    }

  }


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

    _tabController = TabController(vsync: this, length: choices.length);
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);

  }

  @override
  void dispose() {
    super.dispose();
    _tabController.dispose();
    _scrollController.dispose();
  }



  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(

        title: TabBar(
          labelColor: Colors.black,
          unselectedLabelColor: Colors.black45,
          labelStyle: TextStyle(fontWeight: FontWeight.w700, fontFamily: 'Valera'),
          controller: _tabController,
          isScrollable: true,
          indicator: BoxDecoration(
              borderRadius: BorderRadius.circular(50),
              color: Colors.transparent),
          onTap: (int index){
            setState(() {
              _scrollController.animateTo(index*80.0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
            });
          },

          tabs: choices.map((Choice choice) {
            return Padding(
              padding: const EdgeInsets.only(top: 20.0),
              child: Tab(
                text: choice.title,
              ),
            );
          }).toList(),
        ),
      ),
      body: Container(

        child: ListView.builder(
          controller: _scrollController,
            itemBuilder: (context, index){


            return ListTile(

            );

            }),

      ),

    );
  }
}



class Choice {
  const Choice({this.title});

  final String title;
}

const List<Choice> choices = const <Choice>[
  const Choice(title: 'First Tab'),
  const Choice(title: 'Second Tab'),
  const Choice(title: 'Third Tab'),
  const Choice(title: 'Fourth Tab'),
  const Choice(title: 'Fifth Tab'),
  const Choice(title: 'Sixth Tab'),
];

class ChoiceCard extends StatelessWidget {
  const ChoiceCard({Key key, this.choice}) : super(key: key);

  final Choice choice;

  @override
  Widget build(BuildContext context) {
    return Text(choice.title, style: TextStyle(
      color: Colors.black,
    ),
    );
  }
}

You can do this by TabController and Listener

First you can define a TabController. It need to be in StatefulWidget.

TabController _tabController;

@override
void initState() {
  super.initState();
  _tabController = TabController(length: tabLenght, vsync: this);
}

TabBar(
  ...
  controller: _tabController,
  ...
)

Then you can add Listener class to the ListView. Save the clicked position and swiping distance in onPointerDown,onPointerMove,onPointerUp and onPointerCancel (same as onPointerUp).

Change the tab by control the offset and index. Notice offset is in range (-1.0,1.0). You should animate by yourself if it reach the next index.

double startX;
double sensitivity = 0.01;

    ...
    child: Listener(
      onPointerDown: (event) {
        startX = event.position.dx;
      },

      onPointerMove: (event) {
        double newX = event.position.dx;
        double offset = ((startX - newX) * sensitivity).clamp(-1.0, 1.0);
        if (!_tabController.indexIsChanging)
          _tabController.offset = offset;
        if (offset == 1.0 &&
            _tabController.index < _tabController.length - 1) {
          _tabController.animateTo(_tabController.index + 1);
          startX = newX;
        }
        if (offset == -1.0 && _tabController.index > 0) {
          _tabController.animateTo(_tabController.index - 1);
          startX = newX;
        }
      },

      onPointerUp: (_) {
        if (_tabController.offset > 0.5 &&
            _tabController.index < _tabController.length - 1)
          _tabController.animateTo(_tabController.index + 1);
        else if (_tabController.offset < -0.5 && _tabController.index > 0)
          _tabController.animateTo(_tabController.index - 1);
        else {
          if (!_tabController.indexIsChanging)
            _tabController.offset = 0;
        }
      },

      onPointerCancel: (_) {
        if (_tabController.offset > 0.5 &&
            _tabController.index < _tabController.length - 1)
          _tabController.animateTo(_tabController.index + 1);
        else if (_tabController.offset < -0.5 && _tabController.index > 0)
          _tabController.animateTo(_tabController.index - 1);
        else {
          if (!_tabController.indexIsChanging)
            _tabController.offset = 0;
        }
      },
      child: ListView.builder(
      ...

I think you want the drag effect like TabBarView? You can also check the source code inside TabBarView.

Related