🍻 flutter 在不同 fit 图片上的 hero 动画
!本篇文章过于久远,其中观点和内容可能已经不准确,请见谅!~
想分享的是只需要一点点的数学计算就能实现 Flutter 中不同 fitMode 的图片的 Hero 转换动画。
我们经常在很多 app 都见到过在列表页的小图,点击后小图连续的动画,变成大图展示在顶部,特别的流畅。这就是 flutter 里的 hero 动画,也就是共享元素转换,像一个超人一样飞来飞去的组件。
例如下面的代码(需要科学上网):
import 'package:flutter/material.dart';
Future main() async {
runApp(new MaterialApp(home: PageA()));
}
class PageA extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('tap the box')),
body: Center(
child: GestureDetector(
child: HeroBox(Colors.green, 100),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => PageB()),
);
},
),
),
);
}
}
class PageB extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('tap the box')),
backgroundColor: Colors.lightBlueAccent,
body: GestureDetector(
child: Container(
alignment: Alignment.topCenter,
child: HeroBox(Colors.red, 200),
),
onTap: () {
Navigator.of(context).pop();
},
),
);
}
}
class HeroBox extends StatelessWidget {
final Color color;
final double size;
HeroBox(this.color, this.size);
Widget build(BuildContext context) {
return Hero(
tag: 'test-hero',
child: Container(
color: color,
width: size,
height: size,
child: Image.network('https://dartpad.dev/dart-192.png',
fit: BoxFit.contain),
),
);
}
}
实现的方式就是两个页面中用 Hero 组件展示同样的组件,然后在页面转场的时候从前一页平滑过渡到后一页。
过渡的过程中如果包裹的两个组件不同,就会出现渐变、突变等很突兀的变化,所以经常是相同组件仅修改尺寸和位置,比如图片的展示,也是改变容器的大小来实现过渡。
但是图片不仅有大小的属性,还有 fit 的属性,fit=cover 会填满容器,fit=contain 会展示全部图片内容,Hero 没办法处理这个属性的过渡,所以就出现了问题,也就是下面这个 issue 的问题:
解决办法呢就是使用 cover 属性,但是大小改变的时候来模拟 contain 的样式,具体实现如下:
import 'dart:math';
import 'dart:ui' as ui show window;
import 'package:flutter/material.dart';
Future main() async {
runApp(new MaterialApp(home: PageA()));
}
class PageA extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('tap the box')),
body: Center(
child: GestureDetector(
child: BgHeroContainWidget(
'test-hero-2',
'https://flutter.dev/assets/homepage/carousel/slide_2-bg-ec22a39fb182cb26ec8fb88b30064bc08707d4e87b0e7415c2f077c20d3e6102.jpg',
100,
170,
'cover',
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => PageB()),
);
},
),
),
);
}
}
class PageB extends StatelessWidget {
Widget build(BuildContext context) {
double screenWidth = MediaQueryData.fromWindow(ui.window).size.width;
return Scaffold(
appBar: AppBar(title: Text('tap the box')),
backgroundColor: Colors.lightBlueAccent,
body: GestureDetector(
child: Container(
alignment: Alignment.topCenter,
child: BgHeroContainWidget(
'test-hero-2',
'https://flutter.dev/assets/homepage/carousel/slide_2-bg-ec22a39fb182cb26ec8fb88b30064bc08707d4e87b0e7415c2f077c20d3e6102.jpg',
screenWidth,
300,
'contain',
),
),
onTap: () {
Navigator.of(context).pop();
},
),
);
}
}
class HeroBox extends StatelessWidget {
final Color color;
final double size;
HeroBox(this.color, this.size);
Widget build(BuildContext context) {
return Hero(
tag: 'test-hero',
child: Container(
color: color,
width: size,
height: size,
child: Image.network(
'https://flutter.dev/assets/homepage/carousel/slide_2-bg-ec22a39fb182cb26ec8fb88b30064bc08707d4e87b0e7415c2f077c20d3e6102.jpg',
fit: BoxFit.contain),
),
);
}
}
class BgHeroContainWidget extends StatefulWidget {
final String src;
final String tag;
final double width;
final double height;
final String type;
final String srcCuted;
BgHeroContainWidget(this.tag, this.src, this.width, this.height, this.type,
{this.srcCuted});
@override
BgHeroContainWidgetState createState() => BgHeroContainWidgetState();
}
class BgHeroContainWidgetState extends State<BgHeroContainWidget> {
NetworkImage image;
NetworkImage image2;
double coverWidth;
double coverHeight;
double containWidth;
double containHeight;
@override
void initState() {
getImage();
super.initState();
}
void getImage() {
image = NetworkImage(
'https://flutter.dev/assets/homepage/carousel/slide_2-bg-ec22a39fb182cb26ec8fb88b30064bc08707d4e87b0e7415c2f077c20d3e6102.jpg');
ImageStreamListener listener =
ImageStreamListener((ImageInfo info, bool _) {
double containerRatio = widget.width / widget.height;
double imgRatio = info.image.width / info.image.height;
// contain
if (containerRatio == imgRatio) {
containWidth = widget.width;
containHeight = widget.height;
} else if (containerRatio > imgRatio) {
// 高度缩放到容器大小
containWidth = widget.height * imgRatio;
containHeight = widget.height;
} else if (containerRatio < imgRatio) {
// 宽度缩放到容器大小
containWidth = widget.width;
containHeight = widget.width / imgRatio;
}
// cover
if (containerRatio == imgRatio) {
coverWidth = widget.width;
coverHeight = widget.height;
} else if (containerRatio > imgRatio) {
// 宽度缩放到容器大小
coverWidth = widget.width;
coverHeight = widget.width / imgRatio;
} else if (containerRatio < imgRatio) {
double screenWidth = MediaQueryData.fromWindow(ui.window).size.width;
// 高度缩放到容器大小
coverWidth = min(widget.height * imgRatio,
screenWidth); // 如果特别宽的,需要限制高度,避免变更 contain 之后超过 screen.width
coverHeight = coverWidth / imgRatio;
}
if (mounted) setState(() {});
});
ImageStream stream = image.resolve(new ImageConfiguration());
if (widget.srcCuted != null) {
// 有缓存,先计算,避免大图影响加载时间
image2 = NetworkImage(widget.srcCuted);
stream = image2.resolve(new ImageConfiguration());
}
stream.addListener(listener);
}
@override
Widget build(BuildContext context) {
double width = widget.type == 'contain' ? containWidth : coverWidth;
double height = widget.type == 'contain' ? containHeight : coverHeight;
return Container(
width: widget.width,
height: widget.height,
child: AnimatedOpacity(
opacity: coverWidth == null ? 0 : 1,
duration: Duration(milliseconds: 200),
child: Hero(
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero hero = flightDirection == HeroFlightDirection.push
? toHeroContext.widget
: fromHeroContext.widget;
// print(flightDirection == HeroFlightDirection.push
// ? 'small to big'
// : 'big to small');
return hero.child;
},
tag: widget.tag,
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: [
image2 != null
? Container(
width: width,
height: height,
child: Image(image: image2, fit: BoxFit.cover),
)
: Container(),
image != null
? Container(
width: width,
height: height,
child: Image(image: image, fit: BoxFit.cover),
)
: Container(),
],
),
),
),
);
}
}
包含了前后大小的计算和改变,以及转换过程中的变动。
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/flutter-hero-with-image-fit)