Skip to content

Instantly share code, notes, and snippets.

@jixiaoyong
Last active August 14, 2022 10:52
Show Gist options
  • Save jixiaoyong/8b6584e73abe430d3c1ae926b80a86bd to your computer and use it in GitHub Desktop.
Save jixiaoyong/8b6584e73abe430d3c1ae926b80a86bd to your computer and use it in GitHub Desktop.
hero动画示例
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:math' as math;
/// @author : jixiaoyong
/// @description : Hero动画的进阶使用
/// 这个示例中,First和Second页面中的正方形Hero.child内部被ClipOval和ClipRect相交部分裁剪
/// ClipRect的边长为也即Hero.child最大边长,是与直径为maxClipOvalDiameter的圆的内切正方形的边长
/// 也即:maxClipOvalDiameter / 2 * 根号2
/// 而Hero.child最小的时候,则为半径为minClipOvalDiameter/2的圆
/// @email : jixiaoyong1995@gmail.com
/// @date : 8/14/2022
main() => runApp(const MaterialApp(
home: FirstHeroPage(),
));
const Size maxClipOvalDiameter = Size.square(200);
const Size minClipOvalDiameter = Size.square(100);
class FirstHeroPage extends StatelessWidget {
const FirstHeroPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 放慢30倍
timeDilation = 30;
return Scaffold(
appBar: AppBar(
title: const Text("First Hero Page"),
),
body: Stack(
children: [
// 只是为了标记下面的Hero.child的实际大小
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: minClipOvalDiameter.width,
height: minClipOvalDiameter.height,
color: Colors.cyanAccent,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Hero(
tag: "HeroTag",
child: HeroClippedChildWidget(
size: minClipOvalDiameter,
name: "First",
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const SecondHeroPage();
}));
},
)),
)
],
),
);
}
}
class SecondHeroPage extends StatelessWidget {
const SecondHeroPage({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Second Hero Page"),
),
body: Stack(
children: [
// 只是为了标记下面的Hero.child的实际大小
Align(
alignment: Alignment.topCenter,
child: Container(
width: maxClipOvalDiameter.width,
height: maxClipOvalDiameter.height,
color: Colors.cyanAccent,
),
),
Align(
alignment: Alignment.topCenter,
child: Hero(
tag: "HeroTag",
child: HeroClippedChildWidget(
size: maxClipOvalDiameter,
name: "Second",
onTap: () {
Navigator.pop(context);
},
)),
)
],
),
);
}
}
/// 原理 https://flutter.cn/assets/images/docs/ui/animations/radial-hero-animation.png
/// ClipOval的大小根据Hero的切换而变化,ClipRect的大小则一直是maxRadius的内切正方形大小
/// 从而当Hero从大变小的时候,ClipOval变小,ClipRect不变,看起来是图片逐渐变小并且变圆
/// 当Hero从小变大的时候,ClipOval变大,ClipRect不变,看起来是图片逐渐变大,当ClipOval变得与
/// ClipRect内切并更大时,图片就会从圆逐渐变为ClipRect的形状
class HeroClippedChildWidget extends StatelessWidget {
const HeroClippedChildWidget(
{Key? key, required this.size, required this.name, required this.onTap})
: super(key: key);
final Size size;
final String name;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
// 与圆内切的正方形边长s = 圆半径 * 根号2 = 直径 * 根号2 / 2
var clipRectSize = maxClipOvalDiameter.width * math.sqrt2 / 2;
return SizedBox(
height: size.height,
width: size.width,
child: GestureDetector(
onTap: onTap,
// 当Hero动画变到最小时,ClipOval与ClipRect相交部分是ClipOval形状
child: ClipOval(
child: Center(
// 当Hero动画变到最大时,ClipOval与ClipRect相交部分是ClipRect形状
child: ClipRect(
child: SizedBox(
height: clipRectSize,
width: clipRectSize,
child: Container(
color: Colors.blueAccent,
child: Center(
child: Text(name),
),
),
),
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// @author : jixiaoyong
/// @description : 一个简单的Hero动画使用示例
/// 这个示例中,Hero的child是HeroChildWidget,其中有文本,因为Hero动画实现其实是将child放到
/// overlay中,所以文本如果没有被包裹Material的话,就会出现展示样式异常
/// @email : jixiaoyong1995@gmail.com
/// @date : 8/14/2022
main() => runApp(const MaterialApp(
home: FirstHeroPage(),
));
class FirstHeroPage extends StatelessWidget {
const FirstHeroPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 放慢30倍
timeDilation = 30;
return Scaffold(
appBar: AppBar(
title: const Text("First Hero Page"),
),
body: Stack(
children: [
Align(
alignment: Alignment.bottomLeft,
child: Hero(
tag: "HeroTag",
child: HeroChildWidget(
size: const Size.square(100),
name: "First",
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const SecondHeroPage();
}));
},
)),
)
],
),
);
}
}
class SecondHeroPage extends StatelessWidget {
const SecondHeroPage({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Second Hero Page"),
),
body: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: Hero(
tag: "HeroTag",
child: HeroChildWidget(
size: const Size.square(200),
name: "Second",
onTap: () {
Navigator.pop(context);
},
)),
)
],
),
);
}
}
class HeroChildWidget extends StatelessWidget {
const HeroChildWidget(
{Key? key, required this.size, required this.name, required this.onTap})
: super(key: key);
final Size size;
final String name;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: size.height,
width: size.width,
color: Colors.blueAccent,
child: Center(
child: Text(name),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment