【Flutter】实现自定义TabBar主题色配置

5,483 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

需求背景

首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColorunselectedLabelColor两者。因此若需要自定义实现支持配置主题色TabBar组件。

Video_20220820_045352_279.gif

改造实现详解

TabBar切换字体抖动问题解决

这在此之前文章中有提到过解决方案,主要实现逻辑是将原先切换动画替换为缩放实现,规避了动画实现出现的抖动问题。

解决方案

TabBar切换字体主题色实现

  1. TabBar入参提供每个Tab的颜色配置: final List labelColors;
  2. 找到TabBar切换逻辑代码【_TabBarState】:【_buildStyledTab】

_buildStyledTab中TabStyle方法负责构建每个Tab样式,调整该方法增加构建当前TabStylePositioncurrentPosition,分别为对应Tab的样式和当前选中Tab的样式

Widget _buildStyledTab(Widget child,int position,int currentPosition, bool selected, Animation<double> animation,TabController controller) {
 		Color labelColor;
    Color unselectedLabelColor;
    labelColor = widget.labelColors[position];
    unselectedLabelColor = widget.labelColors[currentPosition];
  	return _TabStyle(
      animation: animation,
      selected: selected,
      labelColors: widget.labelColors,
      labelColor: labelColor,
      unselectedLabelColor: unselectedLabelColor,
      labelStyle: widget.labelStyle,
      unselectedLabelStyle: widget.unselectedLabelStyle,
      tabController:controller,
      child: child,
    );
}
  1. 调整_TabStyle方法内部逻辑

增加以下代码逻辑通过TabController获取当前选中Tab定位并且增加渐变透明度调整

// 判断是否是临近的下一个Tab
    bool isNext = false;
    // 透明度不好计算呀
    double opacity = 0.5;
    // 当前选中的Tab
    int selectedValue = tabController.index;
    selectedColor = labelColors[selectedValue];
    // 当前偏移方向
    if (tabController.offset > 0) {
      unselectedColor = labelColors[selectedValue + 1];
      isNext = false;
    } else if (tabController.offset < 0) {
      isNext = true;
      unselectedColor = labelColors[selectedValue - 1];
    } else {
      unselectedColor = selectedColor;
    }
    if (unselectedColor != Color(0xFF333333)) {
      opacity = 0.9;
    }

    final Color color = selected
        ? Color.lerp(selectedColor, unselectedColor.withOpacity(opacity),
        colorAnimation.value)
        : unBuild
        ? Color.lerp(selectedColor.withOpacity(opacity),
        unselectedColor.withOpacity(opacity), colorAnimation.value)
        : Color.lerp(
        selectedColor.withOpacity(opacity),
        unselectedColor.withOpacity(isNext ? 1 : opacity),
        colorAnimation.value);
  1. CustomPaint组件同样也需要增加选中色值设置
    Color labelColor;
    Color unselectedLabelColor;
    labelColor = widget.labelColors[_currentIndex];
    unselectedLabelColor = widget.labelColors[_currentIndex];
    final Animation<double> animation = _ChangeAnimation(_controller);
   
    Widget magicTabBar = CustomPaint(
      painter: _indicatorPainter,
      child: _TabStyle(
        animation: animation,
        selected: false,
        unBuild: true,
        labelColor: labelColor,
        unselectedLabelColor: unselectedLabelColor,
        labelColors: widget.labelColors,
        labelStyle: widget.labelStyle,
        unselectedLabelStyle: widget.unselectedLabelStyle,
        tabController: widget.controller,
        child: _TabLabelBar(
          onPerformLayout: _saveTabOffsets,
          children: wrappedTabs,
        ),
      ),
    );

TabBar指示器自定义

官方提供TabBar的选中指示器长度是跟随Tab宽度不能做到固定宽度,且当改造TabBar主题色之后也期望指示器支持跟随主题色变化。

  1. 自定义指示器继承Decoration增加三个入参TabControllerList<Color>width
  2. _UnderlinePainter增加当前选中Tab逻辑来确定主题色选择。
    double page = 0;
    int realPage = 0;
    page =  pageController.index + pageController.offset ?? 0;
    realPage = pageController.index +  pageController.offset?.floor() ?? 0;
    double opacity = 1 - (page - realPage).abs();
    Color thisColor = labelColors[realPage];
    thisColor = thisColor;
    Color nextColor = labelColors[
    realPage + 1 < labelColors.length ? realPage + 1 : realPage];
    nextColor =  nextColor;
  1. _indicatorRectFor方法修改指示器宽度方法,计算出Tab的中心位置再根据设置宽度绘制最终偏移量位置信息。
final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
    double midValue = (indicator.right - indicator.left) / 2 + indicator.left;
    return Rect.fromLTWH(
      midValue - width / 2,
      indicator.bottom - borderSide.width,
      width,
      borderSide.width,
    );

最终效果

🚀详细代码看这里🚀

Video_20220820_045414_26.gif