如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们
Posted
技术标签:
【中文标题】如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们【英文标题】:How to animate the position of the items in a SliverAppBar to move them around the title when closed 【发布时间】:2021-06-23 18:43:45 【问题描述】:我对 Appbar 有这些要求,但我找不到解决这些问题的方法。
拉伸时,AppBar 必须显示两个图像一个在另一个之上,并且标题必须隐藏。 关闭时,AppBar 必须显示标题,并且两个图像在滚动时必须按比例缩小并移动到标题的两侧。滚动时标题可见。我创建了几个模型来帮助获得所需的结果。
这是拉伸时的 Appbar:
这是关闭时的应用栏:
【问题讨论】:
【参考方案1】:您可以通过扩展SliverPersistentHeaderDelegate
创建自己的SliverAppBar
。
平移、缩放和不透明度更改将在 build(...)
方法中完成,因为这将在范围更改期间调用(通过滚动)minExtent <-> maxExtent
。
这是一个示例代码。
import 'dart:math';
import 'package:flutter/material.dart';
void main()
runApp(MyApp());
class MyApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.blue,
),
home: HomePage(),
);
class HomePage extends StatelessWidget
@override
Widget build(BuildContext context)
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
delegate: MySliverAppBar(
title: 'Sample',
minWidth: 50,
minHeight: 25,
leftMaxWidth: 200,
leftMaxHeight: 100,
rightMaxWidth: 100,
rightMaxHeight: 50,
shrinkedTopPos: 10,
),
pinned: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, int i) => Container(
height: 50,
color: Color.fromARGB(
255,
Random().nextInt(255),
Random().nextInt(255),
Random().nextInt(255),
),
),
childCount: 50,
),
),
],
),
);
class MySliverAppBar extends SliverPersistentHeaderDelegate
MySliverAppBar(
required this.title,
required this.minWidth,
required this.minHeight,
required this.leftMaxWidth,
required this.leftMaxHeight,
required this.rightMaxWidth,
required this.rightMaxHeight,
this.titleStyle = const TextStyle(fontSize: 26),
this.shrinkedTopPos = 0,
);
final String title;
final TextStyle titleStyle;
final double minWidth;
final double minHeight;
final double leftMaxWidth;
final double leftMaxHeight;
final double rightMaxWidth;
final double rightMaxHeight;
final double shrinkedTopPos;
final GlobalKey _titleKey = GlobalKey();
double? _topPadding;
double? _centerX;
Size? _titleSize;
double get _shrinkedTopPos => _topPadding! + shrinkedTopPos;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
)
if (_topPadding == null)
_topPadding = MediaQuery.of(context).padding.top;
if (_centerX == null)
_centerX = MediaQuery.of(context).size.width / 2;
if (_titleSize == null)
_titleSize = _calculateTitleSize(title, titleStyle);
double percent = shrinkOffset / (maxExtent - minExtent);
percent = percent > 1 ? 1 : percent;
return Container(
color: Colors.red,
child: Stack(
children: <Widget>[
_buildTitle(shrinkOffset),
_buildLeftImage(percent),
_buildRightImage(percent),
],
),
);
Size _calculateTitleSize(String text, TextStyle style)
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.size;
Widget _buildTitle(double shrinkOffset) => Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: _topPadding!),
child: Opacity(
opacity: shrinkOffset / maxExtent,
child: Text(title, key: _titleKey, style: titleStyle),
),
),
);
double getScaledWidth(double width, double percent) =>
width - ((width - minWidth) * percent);
double getScaledHeight(double height, double percent) =>
height - ((height - minHeight) * percent);
/// 20 is the padding between the image and the title
double get shrinkedHorizontalPos =>
(_centerX! - (_titleSize!.width / 2)) - minWidth - 20;
Widget _buildLeftImage(double percent)
final double topMargin = minExtent;
final double rangeLeft =
(_centerX! - (leftMaxWidth / 2)) - shrinkedHorizontalPos;
final double rangeTop = topMargin - _shrinkedTopPos;
final double top = topMargin - (rangeTop * percent);
final double left =
(_centerX! - (leftMaxWidth / 2)) - (rangeLeft * percent);
return Positioned(
left: left,
top: top,
child: Container(
width: getScaledWidth(leftMaxWidth, percent),
height: getScaledHeight(leftMaxHeight, percent),
color: Colors.black,
),
);
Widget _buildRightImage(double percent)
final double topMargin = minExtent + (rightMaxHeight / 2);
final double rangeRight =
(_centerX! - (rightMaxWidth / 2)) - shrinkedHorizontalPos;
final double rangeTop = topMargin - _shrinkedTopPos;
final double top = topMargin - (rangeTop * percent);
final double right =
(_centerX! - (rightMaxWidth / 2)) - (rangeRight * percent);
return Positioned(
right: right,
top: top,
child: Container(
width: getScaledWidth(rightMaxWidth, percent),
height: getScaledHeight(rightMaxHeight, percent),
color: Colors.white,
),
);
@override
double get maxExtent => 300;
@override
double get minExtent => _topPadding! + 50;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
【讨论】:
好一个!它已经足够接近我想要的了,但是我想要盒子,当 appbar 被拉伸时,就在靠近标题的地方,而不是在角落里。无论标题有多长。 对不起,我不太明白。 “当 appbar 被拉伸时”是指在标题可见期间? “刚刚接近标题”的意思是shrinkedLeftPos
和shrinkedRightPos
应该计算?
对不起,我的错,我在手机上写得很快。我的意思是“缩小”,而不是“拉伸”。我想说的是,当 AppBar 在 minExtent 中时,最终布局必须是(伪代码)行(对齐:中心,孩子:[box,little padding,title,little padding,box])。但在您的解决方案中,框将被放置在应用栏的角落。
OK2.明白了。我稍后会修改答案。 (晚餐先:D)
@Mou 答案已更新。请参考shrinkedHorizontalPos
。【参考方案2】:
它的公式有点乱,但这里是关于动画的所有计算的方法:
UPD:添加到代码变量中以使图像在扩展时产生 Y 轴偏移。
完整代码重现:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MaterialApp(
title: 'Material App',
home: Body(),
);
class Body extends StatefulWidget
const Body(
Key key,
) : super(key: key);
@override
_BodyState createState() => _BodyState();
class _BodyState extends State<Body>
double _collapsedHeight = 60;
double _expandedHeight = 200;
double
extentRatio; // Value to control SliverAppBar widget sizes, based on BoxConstraints and
double minH1 = 40; // Minimum height of the first image.
double minW1 = 30; // Minimum width of the first image.
double minH2 = 20; // Minimum height of second image.
double minW2 = 25; // Minimum width of second image.
double maxH1 = 60; // Maximum height of the first image.
double maxW1 = 60; // Maximum width of the first image.
double maxH2 = 40; // Maximum height of second image.
double maxW2 = 50; // Maximum width of second image.
double textWidth = 70; // Width of a given title text.
double extYAxisOff = 10.0; // Offset on Y axis for both images when sliver is extended.
@override
Widget build(BuildContext context)
return Scaffold(
body: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled)
return <Widget>[
SliverAppBar(
collapsedHeight: _collapsedHeight,
expandedHeight: _expandedHeight,
floating: true,
pinned: true,
flexibleSpace: LayoutBuilder(
builder:
(BuildContext context, BoxConstraints constraints)
extentRatio =
(constraints.biggest.height - _collapsedHeight) /
(_expandedHeight - _collapsedHeight);
double xAxisOffset1 = (-(minW1 - minW2) -
textWidth +
(textWidth + maxW1) * extentRatio) /
2;
double xAxisOffset2 = (-(minW1 - minW2) +
textWidth +
(-textWidth - maxW2) * extentRatio) /
2;
double yAxisOffset2 = (-(minH1 - minH2) -
(maxH1 - maxH2 - (minH1 - minH2)) *
extentRatio) /
2 -
extYAxisOff * extentRatio;
double yAxisOffset1 = -extYAxisOff * extentRatio;
print(extYAxisOff);
// debugPrint('constraints=' + constraints.toString());
// debugPrint('Scale ratio is $extentRatio');
return FlexibleSpaceBar(
titlePadding: EdgeInsets.all(0),
// centerTitle: true,
title: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: extentRatio < 1 ? 1 : 0,
child: Padding(
padding: const EdgeInsets.only(top: 30.0),
child: Container(
color: Colors.indigo,
width: textWidth,
alignment: Alignment.center,
height: 20,
child: Text(
"TITLE TEXT",
style: TextStyle(
color: Colors.white,
fontSize: 12.0,
),
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
transform: Matrix4(
1,0,0,0,
0,1,0,0,
0,0,1,0,
xAxisOffset1,yAxisOffset1,0,1),
width:
minW1 + (maxW1 - minW1) * extentRatio,
height:
minH1 + (maxH1 - minH1) * extentRatio,
color: Colors.red,
),
Container(
transform: Matrix4(
1,0,0,0,
0,1,0,0,
0,0,1,0,
xAxisOffset2,yAxisOffset2,0,1),
width:
minW2 + (maxW2 - minW2) * extentRatio,
height:
minH2 + (maxH2 - minH2) * extentRatio,
color: Colors.purple,
),
],
),
),
],
),
);
,
)),
];
,
body: Center(
child: Text("Sample Text"),
),
),
),
);
【讨论】:
看起来不错!但是标题可以有可变长度,并且不能使用 AnimatedContainer,因为元素必须由滚动本身插入,而不是由“外部”动画插入。 @Mou 但元素是由滚动插入的,随时计算 是的,你是对的,但我们可以避免使用 AnimatedContainer,因为插值已经由滚动生成。另外,你还有一个固定的标题长度:) 是的,你对容器的看法是正确的,修复了它,关于标题的保留原样。可能有人会像那样需要它。这个要求不在问题之列。以上是关于如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 NestedScrollView 滚动以在 SliverAppBar 上使用堆栈圆形头像?
如何在您的Flutter应用程序中添加SliverAppBar
如何修改 Sliverappbar 以便在向上滚动时可以看到带有 FlexibleSpaceBar 的背景图像?