Flutter项目实战-我的第一个Flutter项目-App主题色+常见tab页(四)

163 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

前言

常见的app基本上都是用tab动态切换页面,本篇文章我们通过简单地动手实现一个常见app+设置通用主题+字体来加深对Flutter的实践学习

image.png

在安卓中我们如果要实现上述效果,通常需要搭配底部的button list 或者是radiogroup 加上顶部的fragment实现状态的切换。

那么flutter是如何实现这一逻辑的呢?

一、BottomNavigationBar

BottomNavigationBar({
  super.key,
  required this.items,
  this.onTap,
  this.currentIndex = 0,
  this.elevation,
  this.type,
  Color? fixedColor,
  this.backgroundColor,
  this.iconSize = 24.0,
  Color? selectedItemColor,
  this.unselectedItemColor,
  this.selectedIconTheme,
  this.unselectedIconTheme,
  this.selectedFontSize = 14.0,
  this.unselectedFontSize = 12.0,
  this.selectedLabelStyle,
  this.unselectedLabelStyle,
  this.showSelectedLabels,
  this.showUnselectedLabels,
  this.mouseCursor,
  this.enableFeedback,
  this.landscapeLayout,
})

1.1 参数介绍

属性介绍
items必填项,设置各个按钮
onTap点击事件
currentIndex当前选中item下标
elevation控制阴影高度,默认为8.0
typeBottomNavigationBarType,默认 fixed,设置为 shifting 时,建议设置选中样式,和为选中样式,提供一个特殊动画
fixedColor选中item填充色
backgroundColor整个BottomNavigationBar背景色
iconSize图标大小,默认24.0
selectedItemColor选中title填充色
unselectedItemColor未选中title填充色
selectedIconTheme选中item图标主题
unselectedIconTheme未选中item图标主题
selectedFontSize选中 title 字体大小,默认14.0
unselectedFontSize未选中title字体大小,默认12.0
selectedLabelStyle选中 title 样式 TextStyle
unselectedLabelStyle未选中 title 样式 TextStyle
showSelectedLabels是否展示选中 title,默认为true
showUnselectedLabels是否展示未选中 title,默认为true
mouseCursor鼠标悬停,Web 开发可以了解

二、前期准备

实现状态页面的tab切换,我们需要提前准备好底部的tab图标,可以从iconFont网站自行搜索下载对应两种色值的图标(选中态+未选中态)。

将icon图标放入assets指定素材的所在目录,注意后续需要在pubspec.yaml中指定图标所在目录,同时使用pub get命令更新资源,如果图标文件是png格式,指定整个文件夹即可,如果图标文件是jpg格式,需要指定文件名及后缀。

assets:
  - images/
  - images/a.jpg
  - images/b.jpg

三、项目结构

3.1、四个状态页

首先我们需要对应建立四个状态页面对应tab的切换,由于逻辑较简单,四个tab页都是stateless的.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('我是主页'),
      ),
    );
  }
}

其他三个页面都利用中间的文本进行标识

3.2、AppHomePage

首页用于装载BottomNavigationBar控件,同时实现四个状态页的切换

import 'package:fftest1/tab_dynamic.dart';
import 'package:fftest1/tab_main.dart';
import 'package:fftest1/tab_mine.dart';
import 'package:fftest1/tab_msg.dart';
import 'package:flutter/material.dart';

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

  @override
  State<AppHomePage> createState() => _AppHomePageState();
}

class _AppHomePageState extends State<AppHomePage> {
  int _index = 0;

  List<Widget> _homeWidgets = [
    TabMainPage(),
    TabDynamicPage(),
    TabMsgPage(),
    TabMinePage(),
  ];

  void _onBottomNavigationBarTapped(index) {
    setState(() {
      _index = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter项目实战'),
      ),
      body: IndexedStack(
        index: _index,
        children: _homeWidgets,
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.shifting,
        currentIndex: _index,
        onTap: _onBottomNavigationBarTapped,
        items: [_getBottomNavItem('首页', 'images/ic_tab_1_unselect.png',
            'images/ic_tab_1_select.png', 0),
          _getBottomNavItem('动态', 'images/ic_tab_2_unselect.png',
              'images/ic_tab_2_select.png', 1),
          _getBottomNavItem('消息', 'images/ic_tab_3_unselect.png',
              'images/ic_tab_3_select.png', 2),
          _getBottomNavItem('我的', 'images/ic_tab_4_unselect.png',
              'images/ic_tab_4_select.png', 3),
        ],
      ),
    );
  }

  BottomNavigationBarItem _getBottomNavItem(
      String title, String normalIcon, String pressedIcon, int index) {
    return BottomNavigationBarItem(
        icon: _index == index
            ? Image.asset(
                pressedIcon,
                width: 32,
                height: 28,
              )
            : Image.asset(
                normalIcon,
                width: 32,
                height: 28,
              ),
        label: title);
  }
}

可以看到在主页组件里控制的状态变量是_index,首页的body使用了IndexedStack

3.3 IndexedStack

IndexedStack是一个管理页面显示层级的容器,使用index属性可以确定当前容器里哪个页面在最顶上.

IndexedStack的children要求是一个Widget数组,也就是我们提前声明的四个状态页数组

可以看到,_onBottomNavigationBarTapped方法中定义了_index的修改,当BarItem的图标被点击后,此时会回调onTap属性指定的方法,通过这个方式控制IndexedStack页面的层级切换.

3.4 MainApp

main.dart作为app的入口,指定了app的首页和一些基本配置,所以尽量这块的代码精简一些.

import 'package:flutter/material.dart';

import 'app_main.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App框架',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AppHomePage(),
    );
  }
}

四、一些小优化

为了实现app内一些色调和字体的统一,我们可以在main.dart里进行一些统一的配置.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'App框架',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        accentColor: Colors.blue[600],
        textTheme: TextTheme(
          headline1: TextStyle(
              fontSize: 36.0, fontWeight: FontWeight.bold, color: Colors.white),
          headline2: TextStyle(
              fontSize: 32.0, fontWeight: FontWeight.w400, color: Colors.white),
          headline3: TextStyle(
              fontSize: 28.0, fontWeight: FontWeight.w400, color: Colors.white),
          headline4: TextStyle(
              fontSize: 24.0, fontWeight: FontWeight.w400, color: Colors.white),
          headline6: TextStyle(
              fontSize: 14.0, fontWeight: FontWeight.w200, color: Colors.white),
          bodyText1: TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.w200,
          ),
        ),
        fontFamily: 'Georgia',
      ),
      home: AppHomePage(),
    );
  }
}

主要在MaterialApp的theme属性中,声明了字体颜色和字体大小等通用属性

  • brightness:app模式切换,包括dark和light,dark对应的是深色模式,light对应浅色模式。后面可以用这个参数实现app夜间模式切换
  • primaryColor:主色调,设置后导航栏就会变成主色调颜色.
  • accentColor:辅助色
  • textTheme:文字主体
  • fontFamily:字体族

此时就可以通过Theme.of(context)来获取app主题,再获得相应的字体样式了.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Text('我是动态页面', style: Theme.of(context).textTheme.bodyText1),
    ),
  );
}

color也可以同样声明

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Flutter项目实战'),
    ),
    body: IndexedStack(
      index: _index,
      children: _homeWidgets,
    ),
    bottomNavigationBar: BottomNavigationBar(
      type: BottomNavigationBarType.shifting,
      currentIndex: _index,
      onTap: _onBottomNavigationBarTapped,
      selectedItemColor: Theme.of(context).primaryColor,
      items: [_getBottomNavItem('首页', 'images/ic_tab_1_unselect.png',
          'images/ic_tab_1_select.png', 0),
        _getBottomNavItem('动态', 'images/ic_tab_2_unselect.png',
            'images/ic_tab_2_select.png', 1),
        _getBottomNavItem('消息', 'images/ic_tab_3_unselect.png',
            'images/ic_tab_3_select.png', 2),
        _getBottomNavItem('我的', 'images/ic_tab_4_unselect.png',
            'images/ic_tab_4_select.png', 3),
      ],
    ),
  );
}

当我们需要规范app内统一配置文件时,就可以通过在MaterialApp中统一进行配置.

eg.app主色调从蓝色切换成绿色

image.png image.png