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();
  }
}
相关推荐
每天吃饭的羊3 小时前
state和ref
前端·javascript·react.js
GEO_YScsn3 小时前
Vite:Next-Gen Frontend Tooling 的高效之道——从原理到实践的性能革命
前端·javascript·css·tensorflow
GISer_Jing3 小时前
滴滴二面(准备二)
前端·javascript·vue·reactjs
ningmengjing_3 小时前
webpack打包方式
前端·爬虫·webpack·node.js·逆向
Yuner20003 小时前
Webpack开发:从入门到精通
前端·webpack·node.js
GISer_Jing3 小时前
滴滴二面准备(一)
前端·javascript·面试·ecmascript
lecepin3 小时前
AI Coding 资讯 2025-09-10
前端·javascript·面试
RestCloud3 小时前
PostgreSQL大表同步优化:如何避免网络和内存瓶颈?
前端·数据库·api
RestCloud3 小时前
iPaaS 与传统 ESB 的区别,企业该如何选择?
前端·架构