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();
  }
}
相关推荐
遝靑2 分钟前
Flutter 3.20+ 全平台开发实战:从状态管理到跨端适配(含源码解析)
flutter
翔云 OCR API5 分钟前
企业工商信息查验API-快速核验企业信息-营业执照文字识别接口
前端·数据库·人工智能·python·mysql
500846 分钟前
存量 Flutter 项目鸿蒙化:模块化拆分与插件替换实战
java·人工智能·flutter·华为·ocr
小明记账簿_微信小程序10 分钟前
js实现页面全屏展示
前端
wordbaby10 分钟前
秒懂 Headless:为什么现在的软件都要“去头”?
前端
茄汁面12 分钟前
实现紧贴边框的高亮流光动画效果(长方形适配)
前端·javascript·css
松莫莫12 分钟前
Vue 3 项目搭建完整流程(Windows 版 · 避坑指南)
前端·vue.js·windows
纸人特工19 分钟前
开源一个 Nuxt 4 导航站模板,功能完整,拿来即用!
前端·开源
JarvanMo22 分钟前
终于来了!Flutter 拥有了一个可用的液态玻璃解决方案!
前端
b***748844 分钟前
前端技术的边界正在消失:迈向体验统一与智能化驱动的新阶段
前端