Cannot set dynamic height for TabBarView in flutter
Asked Answered
P

3

13

I am trying to create TabBar which would be located in the middle of the page (Description widget must be at the top).

The problem is that I have to manually set the height of the Container widget which contains TabBarView. If I leave it without this height, I get error Horizontal viewport was given unbounded height..

Top level widget:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CustomAppBar(),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[Description(), Tabs()],
        ),
      ),
    );
  }

Tabs widget:

class Tabs extends StatelessWidget {
  final _tabs = [
    Tab(
      icon: Icon(Icons.menu),
      text: 'Menu',
    ),
    Tab(
      icon: Icon(Icons.mode_comment),
      text: 'Reviews',
    )
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: _tabs.length,
        child: Column(
          children: <Widget>[
            TabBar(
              labelColor: PickColors.black,
              indicatorSize: TabBarIndicatorSize.tab,
              tabs: _tabs,
            ),
            Container(
              width: double.infinity,
              height: 200, // I need to remove this and make height dynamic
              child: TabBarView(
                children: <Widget>[MenuTab(), ReviewsTab()],
              ),
            ),
          ],
        ));
  }
}

Since Tabs content will be dynamic, the height will also be. I cannot use static height here.

Is there an alternative for the Container with static height? How can I make my tabs' height dynamic?

Penthouse answered 7/9, 2019 at 22:37 Comment(0)
P
3

I have fixed this issue by changing SingleChildScrollView into ListView and writing my own TabView widget which contains tabs in Stack wrapper.

Top level widget body wrappers changed from Column and SingleChildScrollView to ListView:

  Widget build(BuildContext context) {
    SizeConfig().init(context);
    return Scaffold(
      appBar: RestaurantInfoAppBar(),
      body: ListView(
        children: <Widget>[Description(), Tabs()],
      ),
    );
  }

Tabs widget - removed Container with a static width wrapper:

  Widget build(BuildContext context) {
    return DefaultTabController(
        length: _tabs.length,
        child: Column(
          children: <Widget>[
            TabBar(
              labelColor: PickColors.black,
              indicatorSize: TabBarIndicatorSize.tab,
              tabs: _tabs,
            ),
            TabsView(
              tabIndex: _tabIndex,
              firstTab: MenuTab(),
              secondTab: ReviewsTab(),
            )
          ],
        ));
  }

New custom TabsView component currently handles only two tabs (since I only need two) but can be easily changed to handle dynamic numbers of tabs:

class TabsView extends StatelessWidget {
  TabsView(
      {Key key,
      @required this.tabIndex,
      @required this.firstTab,
      @required this.secondTab})
      : super(key: key);

  final int tabIndex;
  final Widget firstTab;
  final Widget secondTab;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedContainer(
          child: firstTab,
          width: SizeConfig.screenWidth,
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
          transform: Matrix4.translationValues(
              tabIndex == 0 ? 0 : -SizeConfig.screenWidth, 0, 0),
          duration: Duration(milliseconds: 300),
          curve: Curves.easeIn,
        ),
        AnimatedContainer(
          child: secondTab,
          width: SizeConfig.screenWidth,
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
          transform: Matrix4.translationValues(
              tabIndex == 1 ? 0 : SizeConfig.screenWidth, 0, 0),
          duration: Duration(milliseconds: 300),
          curve: Curves.easeIn,
        )
      ],
    );
  }
}

P.S. SizeConfig is the same as MediaQuery.of(context).size.width.

Hope this helps someone like me! :)

Penthouse answered 10/9, 2019 at 12:51 Comment(6)
any other way? this is a custom widget that not support swype :)Recha
How to change _tabIndex when tabs change in the above example?Hamlett
The TabsView is taking the height of the tab with bigger content, due to this the other tab with less content has an empty space. Any solution to it?Hamlett
you can change the _tabIndexby using the TabController _tabController. i solved it by using the saple at the begining. https://mcmap.net/q/340385/-cannot-set-dynamic-height-for-tabbarview-in-flutterNiggardly
Great idea AnimatedContainer. Works like a charmIdeate
The custom TabView is not swipeable like the TabBarView. Any solution to add this feature to the custom TabView?Capitulary
B
1

I found this working fine with me

import 'package:cdc/ui/shared/app_color.dart';
import 'package:flutter/material.dart';

class OrderDetails extends StatefulWidget {
  @override
  _OrderDetailsState createState() => _OrderDetailsState();
}

class _OrderDetailsState extends State<OrderDetails>
    with SingleTickerProviderStateMixin {
  final List<Widget> myTabs = [
    Tab(text: 'one'),
    Tab(text: 'two'),
  ];

  TabController _tabController;
  int _tabIndex = 0;

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

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

  _handleTabSelection() {
    if (_tabController.indexIsChanging) {
      setState(() {
        _tabIndex = _tabController.index;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Order Detials'),
        backgroundColor: kPrimaryColor,
      ),
      body: ListView(
        padding: EdgeInsets.all(15),
        children: <Widget>[
          Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(14.0),
              color: const Color(0xffffffff),
              boxShadow: [
                BoxShadow(
                  color: const Color(0x2e000000),
                  offset: Offset(0, 0),
                  blurRadius: 10,
                ),
              ],
            ),
            child: Column(
              children: <Widget>[
                TabBar(
                  controller: _tabController,
                  labelColor: Colors.redAccent,
                  tabs: myTabs,
                ),
                Container(
                  child: [
                    Text('First tab'),
                    Column(
                      children:
                          List.generate(20, (index) => Text('line: $index'))
                              .toList(),
                    ),
                  ][_tabIndex],
                ),
              ],
            ),
          ),
          Container(child: Text('another component')),
        ],
      ),
    );
  }
}
Bathypelagic answered 9/7, 2020 at 11:11 Comment(0)
B
1

Actually, TabBarView is implemented using a Viewport, which has does not "bubble up" its height. Without implementing a fully custom TabBarView/PageView, it's quite hard to achieve dynamic height.

I am having a solution here that's also not perfect, but mostly glitch-free. It uses a Stack with an invisible hidden current tab widget to measure and animate the size and the actual TabBarView filling the Stack.

It works quite well, but the actual downside of this approach is that the current table is built and laid out twice. That shouldn't cause any issues as long as the widgets don't use global keys or hidden side-effects.

class ShrinkWrappingTabBarView extends StatelessWidget {
  const ShrinkWrappingTabBarView({
    super.key,
    required this.tabController,
    required this.children,
  });

  final TabController tabController;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Opacity(
          opacity: 0.0,
          child: AnimatedSize(
            duration: const Duration(milliseconds: 250),
            curve: Curves.easeOutExpo,
            child: SizedBox(
              width: double.infinity, // always fill horizontally
              child: CurrentTabControllerWidget(
                tabController: tabController,
                children: children,
              ),
            ),
          ),
        ),
        Positioned.fill(
          child: TabBarView(
            controller: tabController,
            children: children
                .map(
                  (e) => OverflowBox(
                    alignment: Alignment.topCenter,
                    // avoid shrinkwrapping to animated height
                    minHeight: 0,
                    maxHeight: double.infinity,
                    child: e,
                  ),
                )
                .toList(),
          ),
        ),
      ],
    );
  }
}

class CurrentTabControllerWidget extends StatefulWidget {
  const CurrentTabControllerWidget({
    super.key,
    required this.tabController,
    required this.children,
  });

  final TabController tabController;
  final List<Widget> children;

  @override
  State<CurrentTabControllerWidget> createState() => _CurrentTabControllerWidgetState();
}

class _CurrentTabControllerWidgetState extends State<CurrentTabControllerWidget> {
  @override
  void initState() {
    super.initState();
    widget.tabController.addListener(_tabUpdated);
    widget.tabController.animation?.addListener(_tabUpdated);
  }

  @override
  void dispose() {
    super.dispose();
    widget.tabController.removeListener(_tabUpdated);
    widget.tabController.animation?.removeListener(_tabUpdated);
  }

  @override
  void didUpdateWidget(covariant CurrentTabControllerWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.tabController != widget.tabController) {
      oldWidget.tabController.removeListener(_tabUpdated);
      widget.tabController.addListener(_tabUpdated);
      oldWidget.tabController.animation?.removeListener(_tabUpdated);
      widget.tabController.animation?.addListener(_tabUpdated);
      setState(() {});
    }
  }

  void _tabUpdated() => setState(() {});

  @override
  Widget build(BuildContext context) =>
      widget.children[widget.tabController.animation?.value.round() ?? widget.tabController.index];
}
Bozovich answered 23/1, 2023 at 11:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.