Flutter 自定义BottomNavigationBar

初学Flutter自定义BottomNavigationBar,支持选中Lottie动画、图片、文字等。

看效果:
SSBottomNavBar.gif
下面是所有代码:
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';

class SSBottomNavBar extends StatefulWidget {
  SSBottomNavBar({
    super.key,
    this.selectedIndex = 0,
    this.iconSize = 24,
    this.containerHeight = 56,
    this.backgroundColor,
    this.shadowColor,
    this.onItemTap,
    this.onItemDoubleTap,
    required this.items,
  });

  final int selectedIndex;
  final double iconSize;
  final double containerHeight;
  final Color? backgroundColor;
  final Color? shadowColor;
  final List<SSBottomNavBarItem> items;
  final ValueChanged<int>? onItemTap;
  final ValueChanged<int>? onItemDoubleTap;

  @override
  State<SSBottomNavBar> createState() => _SSBottomNavBarState();
}

class _SSBottomNavBarState extends State<SSBottomNavBar> {

  late int _currentIndex;

  @override
  void initState() {
    super.initState();
    SSBottomNavBarListener().items = widget.items;
    _currentIndex = widget.selectedIndex;
  }

  void _onTap(SSBottomNavBarItem item) {
    SSBottomNavBarItem currentItem = widget.items[_currentIndex];
    if (currentItem.itemId != item.itemId) {
      widget.onItemTap != null ? widget.onItemTap!(item.index) : null;
      setState(() {
        _currentIndex = item.index;
      });
      SSBottomNavBarListener().currentItemId = item.index;
      //SSBottomNavBarListener().tabBadgeChangeNotifier(item.itemId, false);
    }
  }

  void _onDoubleTap(SSBottomNavBarItem item) {
    SSBottomNavBarItem currentItem = widget.items[_currentIndex];
    widget.onItemDoubleTap != null ? widget.onItemDoubleTap!(currentItem.index) : null;
  }

  @override
  Widget build(BuildContext context) {

    final bgColor = widget.backgroundColor ??
        (Theme.of(context).bottomAppBarTheme.color ?? Colors.white);
    const double itemMerge = 4.0;

    return  Container(
      decoration: BoxDecoration(
        color: bgColor,
        boxShadow: widget.shadowColor != null ? [
          BoxShadow(
            color: widget.shadowColor!,
            blurRadius: 4,
          ),
        ] : null,
      ),
      child: SafeArea(
        child: Container(

          width: double.infinity,
          height: widget.containerHeight,
          padding: const EdgeInsets.symmetric(vertical: 0, horizontal: itemMerge),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: widget.items.map((item) {
              var index = widget.items.indexOf(item);
              item.index = index;
              if (item.itemId == 0) item.itemId = index;
              SSBottomNavBarItem currentItem = widget.items[_currentIndex];
              item.isSelected = (index == _currentIndex);

              return SSNavBarItemWidget(
                itemWidth: (MediaQuery.of(context).size.width - itemMerge*2) / widget.items.length,
                iconSize: widget.iconSize,
                item: item,
                backgroundColor: bgColor,
                onTap: _onTap,
                onDoubleTap: _onDoubleTap,
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

/// item
class SSNavBarItemWidget extends StatefulWidget {
  SSNavBarItemWidget({
    super.key,
    required this.itemWidth,
    required this.iconSize,
    required this.item,
    required this.backgroundColor,
    this.onTap,
    this.onDoubleTap,
  });

  final double itemWidth;
  final double iconSize;
  final SSBottomNavBarItem item;
  final Color backgroundColor;
  final Function(SSBottomNavBarItem)? onTap;
  final Function(SSBottomNavBarItem)? onDoubleTap;

  @override
  State<SSNavBarItemWidget> createState() => _SSNavBarItemWidgetState();
}

class _SSNavBarItemWidgetState extends State<SSNavBarItemWidget> with SingleTickerProviderStateMixin {

  late AnimationController _animationController;
  late bool _lottieCompleted;

  @override
  void initState() {
    super.initState();
    _lottieCompleted = false;

    widget.item.addListener(_itemStateChange);

    _animationController = AnimationController(vsync: this);
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _lottieCompleted = true;
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    widget.item.removeListener(_itemStateChange);
    _animationController.dispose();
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(covariant SSNavBarItemWidget oldWidget) {
    _lottieCompleted = false;
    super.didUpdateWidget(oldWidget);
  }

  void _itemStateChange() {
    setState(() {});
    if (widget.item.notifierType == 2) {
      if (widget.onTap != null) {
        widget.onTap!(widget.item);
      }
    }
  }

  Widget _tabIcon() {
    return SizedBox(
      width: widget.iconSize,
      height: widget.iconSize,
      child: FittedBox(
        fit: BoxFit.contain,
        child: _customIcon(),
      ),
    );
  }

  Widget _customIcon() {
    iconTheme() {
      return
        widget.item.isSelected ?
        (widget.item.selectIcon ?? widget.item.unselectIcon) :
        widget.item.unselectIcon;
      // return IconTheme(
      //   data: IconThemeData(
      //     size: widget.iconSize,
      //     color: widget.isSelected ? widget.item.selectTextColor : widget.item.unselectTextColor,
      //   ),
      //   child: widget.isSelected ?
      //     (widget.item.selectIcon ?? widget.item.unselectIcon)
      //       : widget.item.unselectIcon,
      // );
    }

    if (widget.item.isSelected) {
      if (widget.item.lottiePath is String && widget.item.lottiePath!.isNotEmpty) {
        if (!_lottieCompleted) {
          return Lottie.asset(
            widget.item.lottiePath!,
            controller: _animationController,
            onLoaded: (composition) {
              _animationController
                ..duration = composition.duration
                ..forward(from: 0.0);
            },
          );
        } else {
          return iconTheme();
        }
      } else {
        return iconTheme();
      }
    } else {
      return iconTheme();
    }
  }

  Widget _tabText() {
    return Flexible(
      child: Text(
          widget.item.title,
          maxLines: 1,
          textAlign: widget.item.textAlign,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(
            color: widget.item.isSelected ? widget.item.selectTextColor : widget.item.unselectTextColor,
            fontSize: widget.item.isSelected ? widget.item.selectFontSize : widget.item.unselectFontSize,
            height: 1.2,
            fontWeight: FontWeight.w500,
          )),
    );
  }

  Widget _tabBadge() {
    return widget.item.showBadge ? Positioned(
        top: 5,
        left: widget.itemWidth/2.0 + 15,
        child: ClipOval(
          child: Container(
            color: widget.item.badgeColor,
            width: 7,
            height: 7,
          ),
        )
    ) : Container();
  }

  @override
  Widget build(BuildContext context) {

    Semantics semantic = Semantics(
        container: true,
        selected: widget.item.isSelected,
        child: GestureDetector(
          onTap: () {
            if (widget.onTap != null) {
              widget.onTap!(widget.item);
            }
          },
          onDoubleTap: widget.item.isSelected ? () {
            if (widget.onDoubleTap != null) {
              widget.onDoubleTap!(widget.item);
            }
          } : null,
          child: Stack(
            children: [
              Container(
                width: widget.itemWidth,
                height: 56,
                decoration: BoxDecoration(
                  color: widget.item.isSelected ? widget.item.selectTextColor.withOpacity(0.2) : null,
                ),
                child: Container(
                    width: widget.itemWidth,
                    padding: const EdgeInsets.symmetric(horizontal: 4),
                    child: Column(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.center,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        _tabIcon(),
                        _tabText()
                      ],
                    )
                ),
              ),
              // 红点
              _tabBadge(),
            ],
          ),
        )
    );
    return widget.item.tooltipText == null
        ? semantic
        : Tooltip(
      message: widget.item.tooltipText!,
      child: semantic,
    );
  }
}

/// 数据model
class SSBottomNavBarItem extends ChangeNotifier {
  SSBottomNavBarItem({
    required this.unselectIcon,
    this.selectIcon,
    required this.title,
    this.textAlign = TextAlign.center,
    this.tooltipText,
    this.selectTextColor = Colors.green,
    this.unselectTextColor = Colors.black,
    this.selectFontSize = 12.0,
    this.unselectFontSize = 12.0,
    this.badgeColor = Colors.red,
    this.showBadge = false,
    this.index = 0,
    this.itemId = 0,
    this.lottiePath,
    this.isSelected = false,
    this.notifierType = 0,
  });

  final Widget? selectIcon;
  final Widget unselectIcon;
  final String title;
  final TextAlign? textAlign;
  final String? tooltipText;
  String? lottiePath;

  final Color selectTextColor;
  final Color unselectTextColor;
  final double selectFontSize;
  final double unselectFontSize;

  final Color? badgeColor;
  bool showBadge;

  int index;
  int itemId;
  bool isSelected;
  int notifierType; // 通知类型,1:badge更新,2:下标更新

  void postNotify() {
    notifyListeners();
  }
}


/// badge状态管理 单例
class SSBottomNavBarListener {
  // 工厂方法构造函数 - 通过UserModel()获取对象1
  factory SSBottomNavBarListener() => _getInstance();
  // instance的getter方法 - 通过UserModel.instance获取对象2
  static SSBottomNavBarListener get instance => _getInstance();
  // 静态变量_instance,存储唯一对象
  static SSBottomNavBarListener? _instance;
  // 获取唯一对象
  static SSBottomNavBarListener _getInstance() {
    _instance ??= SSBottomNavBarListener._internal();
    return _instance!;
  }

  //初始化...
  SSBottomNavBarListener._internal() {
    //初始化其他操作...
  }

  late List<SSBottomNavBarItem> items;
  int currentItemId = 0;

  void tabBadgeChangeNotifier(int itemId, bool isShowBadge) {
    for (SSBottomNavBarItem item in items) {
      item.notifierType = 1;
      if (item.itemId == itemId && item.showBadge != isShowBadge) {
        item.showBadge = isShowBadge;
        item.postNotify();
      }
    }
  }

  void tabSelectItemId(int itemId) {
    for (SSBottomNavBarItem item in items) {
      item.notifierType = 2;
      if (itemId == item.itemId && !item.isSelected) {
        item.isSelected = true;
        item.postNotify();
      }
    }
  }
}
使用
SSBottomNavBar _bottomNavBar() {
    List<String> titles = ['首页', '视频', '赛程', '数据', '会员'];
    List<SSBottomNavBarItem> items = List.generate(titles.length, (index) {
      String title = titles[index];
      Image selectImage = SSAssets.images.common.rightMarkGreen.image();
      Image unselectIcon = SSAssets.images.common.netError.image();
      String? lottie;
      bool showBadge = false;
      if (index == 0) {
        lottie = SSAssets.lotties.homeYearAnimation.path;
      }
      if (index == 1) {
        unselectIcon = SSAssets.images.common.dataEmpty.image();
      } else if (index == 2) {
        unselectIcon = SSAssets.images.common.headerFootball2x.image();
      } else if (index == 3) {
        unselectIcon = SSAssets.images.common.dataError.image();
      } else if (index == 4) {
        unselectIcon = SSAssets.images.common.rightMarkRed.image();
        showBadge = true;
      }
      return SSBottomNavBarItem(
        title: title,
        selectIcon: selectImage,
        unselectIcon: unselectIcon,
        lottiePath: lottie,
        showBadge: showBadge,
      );
    });

    return SSBottomNavBar(
      selectedIndex: _selectIndex,
      iconSize: 35,
      backgroundColor: Colors.white,
      onItemTap: (index) {
        setState(() {
          _selectIndex = index;
          print('index = $index');
        });
      },
      items: items,
    );
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容