前言
在布局的过程中,有时候一行无法显示全部内容,而又不想换行时,就需要实现一个流水灯式的控件效果。借助此文所介绍的Marquee控件,可以轻松得给任何控件添加流水灯效果。
使用
例如,要给文字添加流水灯效果,可以这样做:
Marquee(
child: Text(
"下面是专辑封面 下面是专辑封面 下面是专辑封面 下面是专辑封面",
maxLines: 1,
overflow: TextOverflow.visible,
style: TextStyle(fontSize: 30),
),
),
在一行无法展示所有文字的时候,Marquee中的child会自动开始循环滚动,而且Marquee会根据child的宽度,自动计算滚动的速度,以此符合人的阅读习惯。 当然,Marquee也可以给其他控件添加流水灯效果,比如图片:
Marquee(
child:Row(
children: [
Image.network("https://c-ssl.duitang.com/uploads/item/201601/15/20160115224508_TfuGA.jpeg"),
SizedBox(width:20),
Image.network("http://p2.music.126.net/fHyz7zYjnIaUTKoiEkgAbA==/109951164194349109.jpg"),
SizedBox(width:20),
Image.network("https://hbimg.huabanimg.com/9758e2248a413d3cc0cb3b05c9447ab51ae0b78c979d6c-Ym5Ehm_fw658/format/webp"),
SizedBox(width:20),
Image.network("https://hbimg.huabanimg.com/db2dbd8d46be21c206c11848125d7c7587e9dafa4b71e-LWeH0F_fw658/format/webp"),
],
)
),
运行效果
原理
先使用CustomMultiChildLayout打破父组件的布局约束,计算好子组件的大小,将子组件的大小信息回调给父组件,以此确定是否启用滚动,控制滚动偏移。借助AnimatedWidget,简化动画部分的代码。借助ClipRect则可以将溢出的部分裁剪掉。
完整代码
import 'package:flutter/material.dart';
class Marquee extends StatefulWidget {
final Widget child;
const Marquee({
required this.child,
Key? key}) : super(key: key);
@override
_MarqueeState createState() => _MarqueeState();
}
class _MarqueeState extends State<Marquee> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MarqueeTransition(
child: widget.child,
offset: _controller,
onScroll: (double childWidth) {
// 无需滚动时终止动画
if(0 == childWidth){
_controller.animateTo(0,duration: Duration(seconds:1));
return;
}
// 优化,防止重复调用repeat函数
if(_controller.duration == Duration(seconds: childWidth~/50)){
return;
}
_controller.repeat(period: Duration(seconds: childWidth~/50));
});
}
}
class MarqueeTransition extends AnimatedWidget{
final int _childId = 0;
final Widget child;
Animation<double> get offset => listenable as Animation<double>;
/// 获取到[child]的大小后,回调给父类,根据[child]的长度调整滚动速度
final Function(double width) onScroll;
const MarqueeTransition({
Key? key,
required this.child,
required Animation<double> offset,
required this.onScroll,
}) : assert(offset != null),
super(key: key, listenable: offset);
@override
Widget build(BuildContext context) {
return ClipRect(
child: CustomMultiChildLayout(
delegate: CustomLayout(childId: _childId,scrollOffset:offset.value,onScroll: onScroll),
children: [
LayoutId(
id: _childId,
child: Row(
children: [
child
],
),
),
LayoutId(
id: _childId + 1,
child: Row(
children: [
child
],
),
),
],
),
);
}
}
class CustomLayout extends MultiChildLayoutDelegate{
final int childId;
final double scrollOffset;
static const internal = 30;
/// 获取到[child]的大小后,回调给父类,根据[child]的长度调整滚动速度
final Function(double width) onScroll;
CustomLayout(
{required this.childId,
required this.scrollOffset,
required this.onScroll,
});
@override
void performLayout(Size size) {
if(hasChild(childId)){
final childSize = layoutChild(childId, BoxConstraints.loose(Size(double.infinity,size.height)));
layoutChild(childId + 1, BoxConstraints.loose(Size(double.infinity,size.height)));
// 文字未溢出,无需滚动
if(childSize.width < size.width){
positionChild(childId, Offset.zero);
onScroll.call(0);
return;
}
// 文字溢出,滚动
positionChild(childId, Offset(-(childSize.width + internal)*scrollOffset,0));
positionChild(childId + 1, Offset(-(childSize.width + internal)*scrollOffset + (childSize.width + internal),0));
// 开启滚动
onScroll.call(childSize.width);
}
}
@override
bool shouldRelayout(covariant CustomLayout oldCustomLayout) {
return oldCustomLayout.scrollOffset != scrollOffset;
}
}