我尝试在 Flutter 中创建一个多选项切换器。
看起来像这样:
当我创建选择指示器(图中的橙色矩形)时,我认为需要为选择指示器添加变换动画(选择另一个选项后移动到另一个选项),所以我创建了一个 Stack 并添加了 ButtonsRow 和给孩子们选择指示器,将选择指示器放在底部,如图所示。
但我很快发现我需要根据Button的宽度设置选择指示器的宽度,因为不同语言的文本长度不同,使用固定宽度可能会导致溢出错误。
我在谷歌上搜索了有关获取小部件宽度的信息,并找到了这篇post。我使用了最上面评论中的方法,将Stack替换为Overlay,然后利用Overlay的特性,先渲染ButtonsRow,等待渲染后插入选择指示器。
更换Stack后,看起来很完美(如图),但是当我点击另一个按钮时,发现没有翻译动画,而是直接跳转到另一个按钮。
实际效果演示:
代码(简化版)
class AttributeSwitcher extends StatefulWidget {
AttributeSwitcher(
{super.key, required this.selected, required this.onSelected}) {
if (selected < 0 || selected > 2) {
throw Exception('selected must be in [0, 2]');
}
}
final int selected;
final void Function(int) onSelected;
@override
State<AttributeSwitcher> createState() => _AttributeSwitcherState();
}
class _AttributeSwitcherState extends State<AttributeSwitcher> {
int _selected = 0;
@override
void initState() {
super.initState();
_selected = widget.selected;
}
void _updateSelected(int selected) {
setState(() {
_selected = selected;
});
widget.onSelected(selected);
}
Widget _createSticky({
required double posX,
required double posY,
required double height,
required double width,
required ColorScheme colorScheme,
}) {
return AnimatedPositioned(
duration: const Duration(milliseconds: 500),
top: posY,
left: posX,
child: UnconstrainedBox(
child: AnimatedContainer(
height: height,
width: width,
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(80),
),
duration: const Duration(milliseconds: 500),
),
));
}
OverlayEntry _createRow({required List<GlobalKey> btnKeys}) {
return OverlayEntry(
builder: (context) => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AttributeBtn(
btnKey: btnKeys[0], text: '专注', onPressed: () => _updateSelected(0)),
const AttributeSplitter(),
AttributeBtn(
btnKey: btnKeys[1],
text: '小休息',
onPressed: () => _updateSelected(1),
),
const AttributeSplitter(),
AttributeBtn(
btnKey: btnKeys[2],
text: '大休息',
onPressed: () => _updateSelected(2),
),
],
),
);
}
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
ColorScheme colorScheme = theme.colorScheme;
TextTheme textTheme = theme.textTheme;
List<GlobalKey> btnKeys = [GlobalKey(), GlobalKey(), GlobalKey()];
List<Offset> btnPos = [];
List<Size> btnSize = [];
GlobalKey overlayKey = GlobalKey();
OverlayEntry row = _createRow(btnKeys: btnKeys);
SchedulerBinding.instance.addPostFrameCallback((_) {
OverlayState state = overlayKey.currentState as OverlayState;
if (overlayKey.currentContext == null) {
throw Exception('overlayKey.currentContext is null');
}
var overlayPos =
(overlayKey.currentContext?.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero);
for (var element in btnKeys) {
if (element.currentContext == null) {
throw Exception('element.currentContext is null');
}
var readerBox = element.currentContext?.findRenderObject() as RenderBox;
var readerSize = readerBox.size;
var readerPos =
readerBox.localToGlobal(Offset(-overlayPos.dx, -overlayPos.dy));
btnPos.add(readerPos);
btnSize.add(readerSize);
}
state.insert(OverlayEntry(builder: (context) {
return _createSticky(
posX: btnPos[_selected].dx,
posY: btnPos[_selected].dy,
height: btnSize[_selected].height,
width: btnSize[_selected].width,
colorScheme: colorScheme,
);
}), below: row);
});
return SizedBox(
width: 300,
height: 50,
child: Overlay(key: overlayKey, initialEntries: [row]),
);
}
}
在发这篇文章之前我尝试了很多方法,导致代码有点乱。原本AttributeSwitcher是一个StatelessWidget,选择切换逻辑是在父Widget中处理的。如果您回答我的问题,我将不胜感激!
我希望当我点击另一个选项时,选择指示器移动到另一个选项,而不是直接跳到另一个选项。
也尝试为动画容器提供曲线和持续时间
这是我使用的代码示例,效果很好
AnimatedPositioned(
curve: Curves.easeOutQuart,
duration: const Duration(milliseconds: 800),
left: _isGiveaway ? 10.w : (context.width / 2),
top: 0,
bottom: 0,
right: _isGiveaway ? (context.width / 2) : 10.w,
child: AnimatedContainer(
curve: Curves.easeOutQuart,
duration: const Duration(milliseconds: 800),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: context.primaryColor,
),
),