0

我试图在 Scrollview 中嵌套一个 tabview,但找不到完成任务的好方法。

如下图所示:

功能图

所需的功能是拥有一个普通的可滚动页面,其中一个条是具有不同大小(和动态调整大小)选项卡的选项卡视图。

不幸的是,尽管查看了几个资源和颤振文档,我还没有遇到任何好的解决方案。

这是我尝试过的:

  • SingleChildScrollView带有子列,TabBarView 包装在 IntrinsicHeight 小部件中(未绑定约束)
  • CustomScrollView变体,TabBarView 用 a 包裹,SliverFillRemaining页眉和页脚分别用SliverToBoxAdapter. 在所有情况下,如果内容更小,则内容被迫扩展到视口的完整大小(就像使用SliverFillViewport视口分数为 1.0 的 Sliver),或者如果更大,则在空间内创建嵌套滚动/溢出(见下文)
    • 如果 TabBarView 的子项是可滚动的小部件,则带有标签栏的条子的高度等于 ViewPort (1.0),并且任何剩余空间都是空的。
    • 如果子项不可滚动,则它们会被强制扩展以适应较小的大小,或者如果较大则给出溢出错误。
  • NestedScrollView最接近,但仍然受到先前实现的不良影响(参见下面的代码示例)
  • 各种其他非正统方法(例如删除 TabBarView 并尝试AnimatedSwitcher与 TabBar 上的侦听器结合使用以在“选项卡”之间设置动画,但这不是可滑动的,并且动画卡顿并且切换的小部件重叠)

迄今为止“最佳”实现的代码如下所示,但并不理想。

有谁知道有什么方法可以做到这一点?

先感谢您。

// best (more "Least-bad") solution code
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      routes: {
        'root': (context) => const Scaffold(
              body: ExamplePage(),
            ),
      },
      initialRoute: 'root',
    );
  }
}

class ExamplePage extends StatefulWidget {
  const ExamplePage({
    Key? key,
  }) : super(key: key);

  @override
  State<ExamplePage> createState() => _ExamplePageState();
}

class _ExamplePageState extends State<ExamplePage>
    with TickerProviderStateMixin {
  late TabController tabController;

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

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

  @override
  Widget build(BuildContext context) => Scaffold(
        resizeToAvoidBottomInset: true,
        backgroundColor: Colors.grey[100],
        appBar: AppBar(),
        body: NestedScrollView(
          floatHeaderSlivers: false,
          physics: const AlwaysScrollableScrollPhysics(),
          headerSliverBuilder: (BuildContext context, bool value) => [
            SliverToBoxAdapter(
              child: Padding(
                padding: const EdgeInsets.only(
                  left: 16.0,
                  right: 16.0,
                  bottom: 24.0,
                  top: 32.0,
                ),
                child: Column(
                  children: [
                    // TODO: Add scan tab thing
                    Container(
                      height: 94.0,
                      width: double.infinity,
                      color: Colors.blueGrey,
                      alignment: Alignment.center,
                      child: Text('A widget with information'),
                    ),
                    const SizedBox(height: 24.0),
                    GenaricTabBar(
                      controller: tabController,
                      tabStrings: const [
                        'Tab 1',
                        'Tab 2',
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ],
          body: CustomScrollView(
            slivers: [
              SliverFillRemaining(
                child: TabBarView(
                  physics: const AlwaysScrollableScrollPhysics(),
                  controller: tabController,
                  children: [
                    // Packaging Parts
                    SingleChildScrollView(
                      child: Container(
                        height: 200,
                        color: Colors.black,
                      ),
                    ),
                    // Symbols
                    SingleChildScrollView(
                      child: Column(
                        children: [
                          Container(
                            color: Colors.red,
                            height: 200.0,
                          ),
                          Container(
                            color: Colors.orange,
                            height: 200.0,
                          ),
                          Container(
                            color: Colors.amber,
                            height: 200.0,
                          ),
                          Container(
                            color: Colors.green,
                            height: 200.0,
                          ),
                          Container(
                            color: Colors.blue,
                            height: 200.0,
                          ),
                          Container(
                            color: Colors.purple,
                            height: 200.0,
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
              SliverToBoxAdapter(
                child: ElevatedButton(
                  child: Text('Button'),
                  onPressed: () => print('pressed'),
                ),
              ),
            ],
          ),
        ),
      );
}

class GenaricTabBar extends StatelessWidget {
  final TabController? controller;
  final List<String> tabStrings;

  const GenaricTabBar({
    Key? key,
    this.controller,
    required this.tabStrings,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => Container(
        decoration: BoxDecoration(
          color: Colors.grey,
          borderRadius: BorderRadius.circular(8.0),
        ),
        padding: const EdgeInsets.all(4.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // if want tab-bar, uncomment
            TabBar(
              controller: controller,
              indicator: ShapeDecoration.fromBoxDecoration(
                BoxDecoration(
                  borderRadius: BorderRadius.circular(6.0),
                  color: Colors.white,
                ),
              ),
              tabs: tabStrings
                  .map((String s) => _GenaricTab(tabString: s))
                  .toList(),
            ),
          ],
        ),
      );
}

class _GenaricTab extends StatelessWidget {
  final String tabString;

  const _GenaricTab({
    Key? key,
    required this.tabString,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => Container(
        child: Text(
          tabString,
          style: const TextStyle(
            color: Colors.black,
          ),
        ),
        height: 32.0,
        alignment: Alignment.center,
      );
}

以上适用于 Dartpad (dartpad.dev),不需要任何外部库

4

1 回答 1

0

理想情况下,某处有更好的答案。但是,在它到来之前,这就是我解决这个问题的方法:

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      // darkTheme: Themes.darkTheme,
      // Language support
      // Routes will keep track of all of the possible places to go.
      routes: {
        'root': (context) => const Scaffold(
              body: ExamplePage(),
            ),
      },
      initialRoute: 'root', // See below.
    );
  }
}

class ExamplePage extends StatefulWidget {
  const ExamplePage({
    Key? key,
  }) : super(key: key);

  @override
  State<ExamplePage> createState() => _ExamplePageState();
}

class _ExamplePageState extends State<ExamplePage>
    with TickerProviderStateMixin {
  late TabController tabController;
  late PageController scrollController;
  late int _pageIndex;

  @override
  void initState() {
    super.initState();
    _pageIndex = 0;
    tabController = TabController(length: 2, vsync: this);
    scrollController = PageController();
    tabController.addListener(() {
      if (_pageIndex != tabController.index) {
        animateToPage(tabController.index);
      }
    });
  }

  void animateToPage([int? target]) {
    if (target == null || target == _pageIndex) return;
    scrollController.animateToPage(
      target,
      duration: const Duration(milliseconds: 250),
      curve: Curves.easeInOut,
    );
    setState(() {
      _pageIndex = target;
    });
  }

  void animateTabSelector([int? target]) {
    if (target == null || target == tabController.index) return;
    tabController.animateTo(
      target,
      duration: const Duration(
        milliseconds: 100,
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) => Scaffold(
        resizeToAvoidBottomInset: true,
        backgroundColor: Colors.grey[100],
        appBar: AppBar(),
        body: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
              child: Padding(
                padding: const EdgeInsets.only(
                  left: 16.0,
                  right: 16.0,
                  bottom: 24.0,
                  top: 32.0,
                ),
                child: Column(
                  children: [
                    // TODO: Add scan tab thing
                    Container(
                      height: 94.0,
                      width: double.infinity,
                      color: Colors.blueGrey,
                      alignment: Alignment.center,
                      child: Text('A widget with information'),
                    ),
                    const SizedBox(height: 24.0),
                    GenaricTabBar(
                      controller: tabController,
                      tabStrings: const [
                        'Tab 1',
                        'Tab 2',
                      ],
                    ),
                  ],
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                height: 200,
                color: Colors.black,
              ),
            ),
            SliverToBoxAdapter(
              child: NotificationListener<ScrollNotification>(
                onNotification: (ScrollNotification notification) {
                  // if page more than 50% to other page, animate tab controller
                  double diff = notification.metrics.extentBefore -
                      notification.metrics.extentAfter;
                  if (diff.abs() < 50 && !tabController.indexIsChanging) {
                    animateTabSelector(diff >= 0 ? 1 : 0);
                  }
                  if (notification.metrics.atEdge) {
                    if (notification.metrics.extentBefore == 0.0) {
                      // Page 0 (1)
                      if (_pageIndex != 0) {
                        setState(() {
                          _pageIndex = 0;
                        });
                        animateTabSelector(_pageIndex);
                      }
                    } else if (notification.metrics.extentAfter == 0.0) {
                      // Page 1 (2)
                      if (_pageIndex != 1) {
                        setState(() {
                          _pageIndex = 1;
                        });
                        animateTabSelector(_pageIndex);
                      }
                    }
                  }
                  return false;
                },
                child: SingleChildScrollView(
                  controller: scrollController,
                  scrollDirection: Axis.horizontal,
                  physics: const PageScrollPhysics(),
                  child: Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      // 1. Parts
                      SizedBox(
                        width: MediaQuery.of(context).size.width,
                        child: Container(
                          color: Colors.teal,
                          height: 50,
                        ),
                      ),
                      // 2. Symbols
                      SizedBox(
                        width: MediaQuery.of(context).size.width,
                        child: Container(
                          color: Colors.orange,
                          height: 10000,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Column(
                children: [
                  Container(
                    color: Colors.red,
                    height: 200.0,
                  ),
                  Container(
                    color: Colors.orange,
                    height: 200.0,
                  ),
                  Container(
                    color: Colors.amber,
                    height: 200.0,
                  ),
                  Container(
                    color: Colors.green,
                    height: 200.0,
                  ),
                  Container(
                    color: Colors.blue,
                    height: 200.0,
                  ),
                  Container(
                    color: Colors.purple,
                    height: 200.0,
                  ),
                ],
              ),
            ),
          ],
        ),
      );
}

class GenaricTabBar extends StatelessWidget {
  final TabController? controller;
  final List<String> tabStrings;

  const GenaricTabBar({
    Key? key,
    this.controller,
    required this.tabStrings,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => Container(
        decoration: BoxDecoration(
          color: Colors.grey,
          borderRadius: BorderRadius.circular(8.0),
        ),
        padding: const EdgeInsets.all(4.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // if want tab-bar, uncomment
            TabBar(
              controller: controller,
              indicator: ShapeDecoration.fromBoxDecoration(
                BoxDecoration(
                  borderRadius: BorderRadius.circular(6.0),
                  color: Colors.white,
                ),
              ),
              tabs: tabStrings
                  .map((String s) => _GenaricTab(tabString: s))
                  .toList(),
            ),
          ],
        ),
      );
}

class _GenaricTab extends StatelessWidget {
  final String tabString;

  const _GenaricTab({
    Key? key,
    required this.tabString,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) => Container(
        child: Text(
          tabString,
          style: const TextStyle(
            color: Colors.black,
          ),
        ),
        height: 32.0,
        alignment: Alignment.center,
      );
}

(准备好飞镖)

基本思想是根本不使用 Tabview,而是使用嵌套在可滚动区域中的水平滚动视图。

通过对水平滚动使用页面物理特性并使用 PageController 而不是普通的 ScrollController,我们可以在水平区域中的两个小部件之间实现滚动效果,从而捕捉到正确的页面。

通过使用通知监听器,我们可以监听滚动视图的变化并相应地更新选项卡视图。

限制:

上面的代码假设只有两个选项卡,因此需要更多考虑优化更多选项卡,特别是在 NotificationListener 函数中。

这对于大型选项卡也可能不起作用,因为两个选项卡都在构建中,即使一个选项卡不在视野范围内。

最后,每个选项卡的垂直高度相同;所以一个更大的选项卡会导致另一个选项卡有很多空白的垂直空间。

希望这对处于类似情况的任何人有所帮助,并对改进建议持开放态度。

于 2022-02-12T21:15:39.697 回答