首页 > 其他分享 >flutter入门系列教程<一>:tab组件的灵活妙用

flutter入门系列教程<一>:tab组件的灵活妙用

时间:2025-01-22 15:58:09浏览次数:3  
标签:妙用 TabBar TabBarView tabs children tab child 组件 flutter

文章目录

说明

前提:假设你已初步了解了flutter和dart语言,并且知道怎么创建一个简单的项目;
学习本文后,你将掌握:

  • tab组件的用法;
  • 组件的封装;

区分

tab页在上面的是TabBarView组件,形如下图:
在这里插入图片描述

tab页在底部的是BottomNavigationBar组件,形如下图:
在这里插入图片描述

TabBarView组件

TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。

注:下面的示例源于:https://book.flutterchina.club/chapter6/tabview.html

TabBarView

TabBarView 封装了 PageView,它的构造方法很简单

 TabBarView({
  Key? key,
  required this.children, // tab 页
  this.controller, // TabController
  this.physics,
  this.dragStartBehavior = DragStartBehavior.start,
}) 

TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。

TabBar

TabBar 为 TabBarView 的导航标题,如图6-20所示:
图6-20
TabBar 有很多配置参数,通过这些参数我们可以定义 TabBar 的样式,很多属性都是在配置 indicator 和 label,拿上图来举例,Label 是每个Tab 的文本,indicator 指 “历史” 下面的白色下划线。

const TabBar({
  Key? key,
  required this.tabs, // 具体的 Tabs,需要我们创建
  this.controller,
  this.isScrollable = false, // 是否可以滑动
  this.padding,
  this.indicatorColor,// 指示器颜色,默认是高度为2的一条下划线
  this.automaticIndicatorColorAdjustment = true,
  this.indicatorWeight = 2.0,// 指示器高度
  this.indicatorPadding = EdgeInsets.zero, //指示器padding
  this.indicator, // 指示器
  this.indicatorSize, // 指示器长度,有两个可选值,一个tab的长度,一个是label长度
  this.labelColor, 
  this.labelStyle,
  this.labelPadding,
  this.unselectedLabelColor,
  this.unselectedLabelStyle,
  this.mouseCursor,
  this.onTap,
  ...
}) 

TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:

const Tab({
  Key? key,
  this.text, //文本
  this.icon, // 图标
  this.iconMargin = const EdgeInsets.only(bottom: 10.0),
  this.height,
  this.child, // 自定义 widget
})

注意,text 和 child 是互斥的,不能同时制定。

实例

下面我们看一个例子:

class TabViewRoute1 extends StatefulWidget {
  @override
  _TabViewRoute1State createState() => _TabViewRoute1State();
}

class _TabViewRoute1State extends State<TabViewRoute1>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  List tabs = ["新闻", "历史", "图片"];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("App Name"),
        bottom: TabBar(
          controller: _tabController,
          tabs: tabs.map((e) => Tab(text: e)).toList(),
        ),
      ),
      body: TabBarView( //构建
        controller: _tabController,
        children: tabs.map((e) {
          return KeepAliveWrapper(
            child: Container(
              alignment: Alignment.center,
              child: Text(e, textScaleFactor: 5),
            ),
          );
        }).toList(),
      ),
    );
  }
  
  @override
  void dispose() {
    // 释放资源
    _tabController.dispose();
    super.dispose();
  }
}

运行后效果如图6-21所示:
在这里插入图片描述

滑动页面时顶部的 Tab 也会跟着动,点击顶部 Tab 时页面也会跟着切换。为了实现 TabBar 和 TabBarView 的联动,我们显式创建了一个 TabController,由于 TabController 又需要一个 TickerProvider (vsync 参数), 我们又混入了 SingleTickerProviderStateMixin;由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。综上,我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。我们修改后的实现如下:

class TabViewRoute2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    List tabs = ["新闻", "历史", "图片"];
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text("App Name"),
          bottom: TabBar(
            tabs: tabs.map((e) => Tab(text: e)).toList(),
          ),
        ),
        body: TabBarView( //构建
          children: tabs.map((e) {
            return KeepAliveWrapper(
              child: Container(
                alignment: Alignment.center,
                child: Text(e, textScaleFactor: 5),
              ),
            );
          }).toList(),
        ),
      ),
    );
  }
}

可以看到我们无需去手动管理 Controller 的生命周期,也不需要提供 SingleTickerProviderStateMixin,同时也没有其他的状态需要管理,也就不需要用 StatefulWidget 了,这样简单很多。

需求升级

上述的代码示例,仅能把tabbar设置在顶部,而且是写在appBar中的,这就限制了它的写法及样式布局;如果我要是把tabbar写在中间位置呢,该怎么办?

写在中间的tabbar组件

其实,上文页提到了DefaultTabController的关键字,用该组件,可以灵活地设置tabbar的位置。
首先要明确tabbar组件的要素

  1. TabBarTabBarView组成;
  2. 明确指定tabs的个数,即length属性;
  3. 指定DefaultTabController或关联TabController

封装组件

我把封装的组件写在下面,以供参考:


import 'package:flutter/material.dart';

// 自由布局的tabBar,上面或下面还有其他组件
class FreeTabBar extends StatefulWidget {
  final List tabs;
  final List<Widget> children;
  final Widget? topWidget;
  final Widget? bottomWidget;
  final double maxHeight;
  const FreeTabBar({
    super.key,
    this.topWidget, // 可选的顶部组件
    this.bottomWidget, // 可选的底部组件
    this.maxHeight = 300.0, // 可选的最大高度
    required this.tabs,
    required this.children,
  });

  @override
  State<FreeTabBar> createState() => _FreeTabBarState();
}

class _FreeTabBarState extends State<FreeTabBar> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: widget.tabs.length,
      child: Column(
        children: [
          if(widget.topWidget != null)
            widget.topWidget as Widget,
          TabBar(
            tabs: widget.tabs.map((e) => Tab(text: e)).toList(),
          ),
          ConstrainedBox(
            constraints: BoxConstraints(maxHeight: widget.maxHeight), // 设置一个最大高度
            child: TabBarView(
              children: widget.children.map((cell) {
                return SingleChildScrollView(
                  child: cell,
                );
              }).toList(),
            ),
          ),
          /// 以下为其他写法
          // Expanded
          // Expanded(
          //   child: TabBarView(
          //     children: widget.children.map((cell) => cell).toList(),
          //   ),
          // ),
          // SizedBox(
          //   height: 300,
          //   child: TabBarView(
          //     children: widget.children.map((cell) {
          //       return SingleChildScrollView(
          //         child: cell,
          //       );
          //     }).toList(),
          //   ),
          // ),
          // Flexible(
          //   child: TabBarView(
          //     children: widget.children.map((cell) => cell).toList(),
          //   ),
          // ),
          if(widget.bottomWidget != null)
            widget.bottomWidget as Widget,
        ],
      ),
    );
  }
}

组件说明

上面的组件用了好几种写法来包裹TabBarView,因为TabBarView组件需要有明确的高度;
上面的ExpandedConstrainedBoxSizedBoxFlexible这几种写法,都限定了其高度

  • 显式设置高度值height;
  • 尽可能大的占据垂直空间

友情提醒:不设置高度会报错

上述组件也示例了封装组件的参数、参数的类型、参数的默认值,大家封装组件是可参考该格式;

组件用法示例

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import '/utils/platform_check.dart';
import '/components/custom/tabBarFree.dart';
import '/model/mine/poetry.dart';
import 'package:intl/intl.dart';

// 我的 - 随机诗词 - iTab诗词源
class ITabPoetryPage extends StatefulWidget {
  const ITabPoetryPage({super.key});

  @override
  State<ITabPoetryPage> createState() => _ITabPoetryPageState();
}

class _ITabPoetryPageState extends State<ITabPoetryPage> {
  PoetryResponse? myResult;

  // 如果是web端,则不设置请求头,否则设置
  final dio = Dio(BaseOptions(
      headers: {
        if(!PlatformCheck.isWeb)
          'Content-Type': 'application/json',
        if(!PlatformCheck.isWeb)
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
      }
  ));

  @override
  void initState() {
    super.initState();
    debugPrint('init初始化,是否为web端:${PlatformCheck.isWeb}');
    getHttp();
  }

  Future<void> getHttp() async {
    try {
      // const apiUrl = 'https://v1.jinrishici.com/all.json';
      const apiUrl = 'https://api.codelife.cc/todayShici?lang=cn';
      final response = await dio.get(apiUrl);
      debugPrint('接口返回值: $response');
      setState(() {
        myResult = PoetryResponse.fromJson(response.data);
      });
    } catch (e) {
      debugPrint('报错啦啦啦啦:$e');
    }
  }

  Widget paddingWidget(Widget child) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: child,
    );
  }

  // 构建内容
  Widget buildText(String? data) {
    return data != null ? paddingWidget(Text(data)) : const Text('暂无数据');
  }

  // 刷新按钮
  Widget refreshBtn() {
    return OutlinedButton(
        onPressed: getHttp,
        child: const Text('今日诗词刷新')
    );
  }

  // 格式化时间
  String formatDateTime(String? dateTimeString) {
    if (dateTimeString == null) return '暂无时间数据';
    try {
      final dateTime = DateTime.parse(dateTimeString);
      final formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
      return formatter.format(dateTime);
    } catch (e) {
      debugPrint('日期格式化错误: $e');
      return '日期格式化错误';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: FreeTabBar(
        topWidget: myResult != null
          ? Column(
              children: [
                refreshBtn(),
                const SizedBox(height: 10,),
                // 标题
                Text(myResult!.data.title),
                // 朝代 - 作者
                Text('${myResult!.data.dynasty} · ${myResult!.data.author}',),
                // 内容
                Text(myResult!.data.content),
              ],
            ) : refreshBtn(),
        bottomWidget: Column(
          children: [
            // 创建时间
            Text(formatDateTime(myResult?.data.createTime)),
          ],
        ),
        tabs: const ['译文', '注释', '引言', '评语', '引言'],
        children: [
          // 译文
          buildText(myResult?.data.translate),
          // 注释
          buildText(myResult?.data.annotation),
          // 引言
          buildText(myResult?.data.preface),
          // 评语
          buildText(myResult?.data.reviews),
          // 引语
          buildText(myResult?.data.quotes),
        ],
      ),
    );
  }
}

上述代码可直接看FreeTabBar组件内部,其余可忽略;
由于组件外层还有一层其他组件,所以整体效果如下(红框内为主要内容)
在这里插入图片描述

常规的tabbar封装

import 'package:flutter/material.dart';
import '/utils/setting.dart';

class CustomTabBar extends StatefulWidget {
  final List tabs;
  final List<Widget> children;
  const CustomTabBar({
    super.key,
    required this.tabs,
    required this.children,
  });

  @override
  State<CustomTabBar> createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State<CustomTabBar> with SingleTickerProviderStateMixin {
  late TabController _tabController;

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

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TabBar(
          indicatorColor: DefaultColor.success,
          labelColor: DefaultColor.success,
          controller: _tabController,
          tabs: widget.tabs.map((e) => Tab(text: e)).toList()
        ),
        Expanded(
          child: TabBarView(
            controller: _tabController,
            children: widget.children.map((cell) {
              return SingleChildScrollView(
                child: cell,
              );
            }).toList(),
          ),
        ),
      ],
    );
  }
}

常规用法

可直接看 CustomTabBar组件内的代码

import 'package:flutter/material.dart';
import '/components/page/common.dart';
import '/components/custom/tabBar.dart';
import './api1.dart';
import './api2.dart';

// 我的 - 随机诗词
class PoetryPage extends StatelessWidget {
  final String title;
  const PoetryPage({
    super.key,
    required this.title,
  });

  @override
  Widget build(BuildContext context) {
    return CommonPage(
      title: title,
      isShowLine: true,
      child: const CustomTabBar(
        tabs: ['iTab诗词', '今日诗词'],
        children: [
          ITabPoetryPage(),
          JRSCPage(),
        ],
      ),
    );
  }
}

效果图如下:
在这里插入图片描述

标签:妙用,TabBar,TabBarView,tabs,children,tab,child,组件,flutter
From: https://blog.csdn.net/yan1915766026/article/details/145303205

相关文章

  • vxe-table 实现双击自适应行高,行高拖拽调整高度
    vxe-table实现双击自适应行高,行高拖拽调整高度官网:https://vxetable.cn通过row-config.resizable和row-resize启用行高拖拽功能以及resizable-config.isDblclickAutoHeight启用双击自适应行高<template><div><vxe-gridv-bind="gridOptions"></vxe-grid><......
  • 【SD零基础教程】Stable Diffusion如何图生图?2025最新SD保姆级教学,新手建议收藏!
    今天想要跟大家分享的是如何利用StableDiffusion图生图,图生图说白了就是根据已有的一张图片给它变化成不同的风格,三次元图片变成二次元图片,二次元变成三次元图片等等。那么具体该如何操作呢?跟着我一步步来吧。首先第一步我们需要有我们的开源软件StableDiffusion,在这里跟......
  • list和datatable相互转化
    ///<summary>///list转datatable///</summary>///<typeparamname="T"></typeparam>///<paramname="collection"></param>///<returns></returns>......
  • Stable Diffusion整合包(sd安装包)免费下载,解压即用,三分钟入门AI绘画!
    StableDiffusion(简称SD)是一款地表最强AI绘图工具(AIGC)之一,StableDiffusion下载后就像PS一样在本地电脑安装后即可使用!SD只需输入提示词,就能通过AI算法迅速生成你想要的图片****。有需要stablediffusion整合包以及提示词插件,可以扫描下方,免费获取安装步骤**步骤一:**......
  • 精通Stable Diffusion画图,理解LoRA、Dreambooth、Hypernetworks四大模型差异
    随着生成型AI技术的能力提升,越来越多的同行开始将注意力放在了通过AI模型提升研发效率上。业内比较火的AI模型有很多,比如画图神器Midjourney、用途多样的StableDiffusion,以及OpenAI此前刚刚迭代的DALL-E2,除了后者使用人数有限之外,前两个都有很多的开发者尝试。不过,对于研......
  • 如何在页面中插入TAB符号(制表符)
    在前端开发中,直接在HTML中插入TAB符号(制表符)通常不会得到你期望的视觉效果,因为HTML会将连续的空白字符(包括空格、制表符和换行符)合并为一个空格。但是,你可以通过以下几种方法来实现类似TAB符号的效果:使用CSS的text-indent或padding-left:如果你想在文本的开始处创建类似TAB的效......
  • Python进阶:深入理解import机制与importlib的妙用
    目录一、Pythonimport机制概述1.1import语句的基本用法1.2模块缓存机制1.3导入搜索路径1.4导入钩子和查找器二、importlib的妙用2.1动态模块导入2.2使用importlib实现插件系统2.3重新加载模块三、总结在Python编程的世界里,import语句是开发者最常用的工......
  • 妙用编辑器:文本编辑器高手必须知道的跳转功能
    1妙用编辑器:文本编辑器高手必须知道的跳转功能  在成为编辑器高手的路上,一些常用的跳转功能是必须要掌握的,本文中的跳转功能,你都知道吗?1.1通用跳转功能  使用通用跳转功能时,如果按住Shift键,便会选中到跳转位置,读者可以自行实践。1.1.1方向键跳转  左右方向键......
  • 如果通过 vxe-table 实现 Excel,可以和 Excel、WPS 复制粘贴
    如果通过vxe-table实现Excel,可以和Excel、WPS复制粘贴官网:https://vxetable.cn<template><div><vxe-gridref="gridRef"v-bind="gridOptions"@cell-area-selection-end="cellAreaSelectionEndEvent"......
  • 题解:CF140A New Year Table
    CF140ANewYearTable思路注意到题目中提到了大圆与小圆相切,我们可以计算由两条经过小圆周长与大圆圆心的切线组成的圆心角的度数。但是这个角度其实不好算,所以我们可以求出它一半的正弦值,也就是\(b\div(a-b)\),然后用asin函数求出其度数(以弧度为单位)。最后答案就是判断\(......