Flutter之自定义TabIndicator

概述

CustomTabIndicator 是一个Flutter自定义Tab指示器组件,用于在TabBar中显示自定义图片作为选中状态的指示器。该组件支持将图片显示在Tab的底部位置,并可以自定义图片的尺寸。

类结构

1. CustomTabIndicator 类

继承关系 : extends Decoration

功能: 自定义Tab指示器的主要类,负责创建绘制器

属性

| 属性名 | 类型 | 说明 |

|--------|------|------|

| imagePath | String | 指示器图片的路径(必需) |

| width | double | 指示器的宽度(必需) |

| height | double | 指示器的高度(必需) |

构造函数

dart 复制代码
CustomTabIndicator({

  required this.imagePath,

  required this.width,

  required this.height,

});

方法

  • createBoxPainter([VoidCallback? onChanged]): 创建自定义绘制器实例

2. _CustomTabIndicatorPainter 类**

继承关系 : extends BoxPainter

功能: 负责实际的绘制逻辑,是私有类

属性

CustomTabIndicator 相同的属性:

  • imagePath: 图片路径

  • width: 指示器宽度

  • height: 指示器高度

核心方法

paint(Canvas canvas, Offset offset, ImageConfiguration configuration)

这是绘制方法的核心实现:

  1. 计算绘制区域:

```dart

final rect = offset & configuration.size!;

```

  1. 计算指示器位置:

```dart

final indicatorRect = Rect.fromLTWH(

rect.left + (rect.width - width) / 2, // 水平居中

rect.bottom - height, // 贴近底部

width,

height,

);

```

  1. 绘制图片:
  • 使用 AssetImage 加载图片资源

  • 通过 ImageStreamListener 监听图片加载完成

  • 使用 canvas.drawImageRect 绘制图片到指定区域

使用方式

基本用法

dart 复制代码
TabBar(

  indicator: CustomTabIndicator(

    imagePath: 'assets/images/tab_indicator.png',

    width: 30.0,

    height: 4.0,

  ),

  tabs: [

    Tab(text: '首页'),

    Tab(text: '分类'),

    Tab(text: '我的'),

  ],

)

完整示例

dart 复制代码
import 'package:flutter/material.dart';

import 'package:your_app/common/widget/custom_tab_indicator.dart';

  


class MyTabPage extends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return DefaultTabController(

      length: 3,

      child: Scaffold(

        appBar: AppBar(

          title: Text('自定义Tab指示器'),

          bottom: TabBar(

            indicator: CustomTabIndicator(

              imagePath: 'assets/images/custom_indicator.png',

              width: 40.0,

              height: 3.0,

            ),

            tabs: [

              Tab(icon: Icon(Icons.home), text: '首页'),

              Tab(icon: Icon(Icons.category), text: '分类'),

              Tab(icon: Icon(Icons.person), text: '我的'),

            ],

          ),

        ),

        body: TabBarView(

          children: [

            Center(child: Text('首页内容')),

            Center(child: Text('分类内容')),

            Center(child: Text('我的内容')),

          ],

        ),

      ),

    );

  }

}

技术特点

1. 位置计算

  • 水平居中 : rect.left + (rect.width - width) / 2

  • 底部对齐 : rect.bottom - height

2. 图片加载

  • 使用 AssetImage 加载本地资源

  • 异步加载机制,通过 ImageStreamListener 处理加载完成事件

3. 绘制优化

  • 使用 drawImageRect 进行精确的图片绘制

  • 支持图片缩放和裁剪

注意事项

  1. 图片资源 : 确保 imagePath 指向的图片资源存在于 assets 目录中

  2. 尺寸设置 : widthheight 应该根据实际图片尺寸和设计需求进行设置

  3. 性能考虑: 图片加载是异步的,首次显示可能会有短暂延迟

  4. 资源管理: 图片资源会被自动缓存,无需手动管理

总结

CustomTabIndicator 提供了一个简单而灵活的方式来创建自定义的Tab指示器。通过继承 DecorationBoxPainter,实现了完全自定义的绘制逻辑,支持图片资源作为指示器,并提供了精确的位置控制。该组件适用于需要特殊视觉效果的应用场景。

代码

arduino 复制代码
import 'dart:ui' as ui;
import 'package:flutter/material.dart';

// 自定义 TabBarIndicator - 显示在底部
class CustomTabIndicator extends Decoration {
  final String imagePath;
  final double width;
  final double height;

  const CustomTabIndicator({
    required this.imagePath,
    required this.width,
    required this.height,
  });

  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
    return _CustomTabIndicatorPainter(
      imagePath: imagePath,
      width: width,
      height: height,
      onChanged: onChanged,
    );
  }
}

class _CustomTabIndicatorPainter extends BoxPainter {
  final String imagePath;
  final double width;
  final double height;
  ImageStream? _imageStream;
  ImageStreamListener? _imageStreamListener;
  ui.Image? _cachedImage;
  bool _isDisposed = false;

  _CustomTabIndicatorPainter({
    required this.imagePath,
    required this.width,
    required this.height,
    VoidCallback? onChanged,
  }) : super(onChanged);

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    if (_isDisposed) return;
    
    final rect = offset & configuration.size!;
    
    // 计算 indicator 的位置,贴近底部
    final indicatorRect = Rect.fromLTWH(
      rect.left + (rect.width - width) / 2, // 水平居中
      rect.bottom - height, // 贴近底部
      width,
      height,
    );
    
    // 如果已经有缓存的图片,直接绘制
    if (_cachedImage != null) {
      try {
        canvas.drawImageRect(
          _cachedImage!,
          Rect.fromLTWH(0, 0, _cachedImage!.width.toDouble(), _cachedImage!.height.toDouble()),
          indicatorRect,
          Paint(),
        );
        return;
      } catch (e) {
        // 如果绘制失败,清除缓存并重新加载
        _cachedImage = null;
      }
    }
    
    // 加载图片
    _loadImage(configuration);
  }

  void _loadImage(ImageConfiguration configuration) {
    if (_isDisposed) return;
    
    // 清理之前的监听器
    _disposeImageStream();
    
    final image = AssetImage(imagePath);
    _imageStream = image.resolve(configuration);
    
    _imageStreamListener = ImageStreamListener(
      (ImageInfo info, bool synchronousCall) {
        if (_isDisposed) return;
        
        try {
          _cachedImage = info.image;
          // 触发重绘
          onChanged?.call();
        } catch (e) {
          // 忽略绘制错误
          debugPrint('Error drawing custom tab indicator: $e');
        }
      },
      onError: (dynamic exception, StackTrace? stackTrace) {
        debugPrint('Error loading custom tab indicator image: $exception');
      },
    );
    
    _imageStream!.addListener(_imageStreamListener!);
  }

  void _disposeImageStream() {
    if (_imageStream != null && _imageStreamListener != null) {
      _imageStream!.removeListener(_imageStreamListener!);
    }
    _imageStream = null;
    _imageStreamListener = null;
  }

  @override
  void dispose() {
    _isDisposed = true;
    _disposeImageStream();
    _cachedImage = null;
    super.dispose();
  }
}
相关推荐
code_YuJun几秒前
管理系统——应用初始化 Loading 动画
前端
oak隔壁找我2 分钟前
JavaScript 模块化演进历程:问题与解决方案。
前端·javascript·架构
Elieal15 分钟前
AJAX 知识
前端·ajax·okhttp
sulikey35 分钟前
Qt 入门简洁笔记:从框架概念到开发环境搭建
开发语言·前端·c++·qt·前端框架·visual studio·qt框架
柿蒂1 小时前
聊聊SliverPersistentHeader优先消费滑动的设计
android·flutter
烛阴1 小时前
循环背后的魔法:Lua 迭代器深度解析
前端·lua
元拓数智1 小时前
现代前端状态管理深度剖析:从单一数据源到分布式状态
前端·1024程序员节
mapbar_front1 小时前
Electron 应用自动更新方案:electron-updater 完整指南
前端·javascript·electron
天一生水water2 小时前
three.js加载三维GLB文件,查看三维模型
前端·1024程序员节
无风听海2 小时前
HarmonyOS之启动应用内的UIAbility组件
前端·华为·harmonyos