flutter实现头像覆盖轮播滚动组件

效果如下:

支持自定义图片大小

支持设置覆盖比例

支持设置最大展示数量

支持设置缩放动画比例

支持自定义动画时长、以及动画延迟时长

支持当图片List长度小于或者登录设置的最大展示数量时禁用滚动动画。

Dart 复制代码
import '../../library.dart';

class CircularImageList extends StatefulWidget {
  final List<String> imageUrls;
  final int maxDisplayCount;
  final double overlapRatio;
  final double height;
  final Duration animDuration;
  final Duration delayedDuration;
  final double minScale;

  const CircularImageList({
    super.key,
    required this.imageUrls,
    required this.maxDisplayCount,
    required this.overlapRatio,
    required this.height,
    this.minScale = 0.8,
    this.animDuration = const Duration(milliseconds: 500),
    this.delayedDuration = const Duration(seconds: 1),
  });

  @override
  CircularImageListState createState() => CircularImageListState();
}

class CircularImageListState extends State<CircularImageList> with SingleTickerProviderStateMixin {
  int _currentIndex = 0;
  late List<String> _currentImages;
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _currentImages = _initializeCurrentImages();

    _animationController = AnimationController(
      duration: widget.animDuration,
      vsync: this,
    );

    _animation = Tween<double>(begin: 0, end: 1).animate(_animationController)..addStatusListener(_onAnimationComplete);

    if (_needsAnimation()) {
      _startAnimationWithDelay();
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  List<String> _initializeCurrentImages() {
    return _needsAnimation() ? widget.imageUrls.take(widget.maxDisplayCount + 1).toList() : widget.imageUrls;
  }

  bool _needsAnimation() {
    return widget.imageUrls.length > widget.maxDisplayCount;
  }

  void _startAnimationWithDelay() {
    Future.delayed(widget.delayedDuration, () {
      _animationController.forward();
    });
  }

  void _onAnimationComplete(AnimationStatus status) {
    if (status == AnimationStatus.completed) {
      setState(() {
        _currentIndex = (_currentIndex + 1) % widget.imageUrls.length;
        _currentImages.removeAt(0);
        _currentImages.add(widget.imageUrls[_currentIndex]);
      });
      _animationController.reset();
      _startAnimationWithDelay();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.none,
      width: _calculateWrapWidth(),
      height: widget.height,
      child: Stack(
        clipBehavior: Clip.none,
        children: _buildImageStack(),
      ),
    );
  }

  double _calculateWrapWidth() {
    int imageCount = _needsAnimation() ? widget.maxDisplayCount : widget.imageUrls.length;
    return widget.height * (1 + widget.overlapRatio * (imageCount - 1));
  }

  List<Widget> _buildImageStack() {
    return List.generate(_currentImages.length, (index) {
      double leftOffset = index * widget.height * widget.overlapRatio;
      return _buildPositionedImage(index, leftOffset);
    });
  }

  Widget _buildPositionedImage(int index, double leftOffset) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Positioned(
          left: leftOffset - (_needsAnimation() ? _animation.value * widget.height * widget.overlapRatio : 0),
          child: Opacity(
            opacity: _calculateOpacity(index),
            child: Transform.scale(
              scale: _calculateScale(index),
              child: child,
            ),
          ),
        );
      },
      child: _buildCircularImage(_currentImages[index]),
    );
  }

  double _calculateOpacity(int index) {
    if (_needsAnimation()) {
      if (index == 0) {
        return 1 - _animation.value;
      } else if (index == _currentImages.length - 1) {
        return _animation.value;
      }
      return 1.0;
    } else {
      return 1.0;
    }
  }

  double _calculateScale(int index) {
    if (_needsAnimation()) {
      if (index == 0) {
        return 1.0 - ((1 - widget.minScale) * _animation.value);
      } else if (index == _currentImages.length - 1) {
        return widget.minScale + ((1 - widget.minScale) * _animation.value);
      }
      return 1.0;
    } else {
      return 1.0;
    }
  }

  Widget _buildCircularImage(String imageUrl) {
    return Container(
      width: widget.height,
      height: widget.height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(widget.height),
        color: AppColors.white,
      ),
      child: Center(
        child: ImageView(
          imageUrl,
          width: widget.height - 4.w,
          height: widget.height - 4.w,
          borderRadius: BorderRadius.circular(widget.height - 4.w),
        ),
      ),
    );
  }
}

使用

Dart 复制代码
List<String> list=[
        "202007/L1574359/icon/8ce91de76e0545b5a5574a90ffd79e86.png", //截图
        'https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641', //海鸥
        'http://gips3.baidu.com/it/u=3886271102,3123389489&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960', //美女
        'https://inews.gtimg.com/om_bt/O6SG7dHjdG0kWNyWz6WPo2_3v6A6eAC9ThTazwlKPO1qMAA/641', //樱花
        // 'https://img2.baidu.com/it/u=2814429148,2262424695&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1422', //🐢

        // "202409/M1595954/icon/af5d915f3a414217aa8e6d2c5a097d31.png",
        // 'https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641',
        // 'https://img2.baidu.com/it/u=1544882228,2394903552&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500',
      ]

CircularImageList(
  minScale: 0.3,
  imageUrls: list,
  maxDisplayCount: 3,
  overlapRatio: 0.5,
  height: 56.w,
  animDuration: const Duration(milliseconds: 500),
  delayedDuration: const Duration(milliseconds: 500),
),