效果如下:
支持自定义图片大小
支持设置覆盖比例
支持设置最大展示数量
支持设置缩放动画比例
支持自定义动画时长、以及动画延迟时长
支持当图片List长度小于或者登录设置的最大展示数量时禁用滚动动画。
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),
),
),
);
}
}
使用
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),
),