[英]How to expand a card on tap in flutter?
我想立即实现材料设计卡的行为。 当我点击它时,它应该展开全屏并显示其他内容/新页面。 我如何实现它?
https://material.io/design/components/cards.html#behavior
我尝试使用 Navigator.of(context).push() 来显示新页面并使用 Hero 动画将卡片背景移动到新的 Scaffold,但是这似乎不是方法,因为新页面没有从卡片中显示出来本身,否则我无法做到。 我正在尝试实现与我上面介绍的 material.io 中相同的行为。 你能以某种方式指导我吗?
谢谢
不久前,我尝试复制那个确切的页面/过渡,虽然我没有让它看起来很像,但我确实很接近。 请记住,这是快速组合在一起的,并没有真正遵循最佳实践或任何东西。
重要的部分是 Hero 小部件,尤其是伴随它们的标签——如果它们不匹配,它就不会匹配。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
),
body: ListView.builder(
itemBuilder: (context, index) {
return TileItem(num: index);
},
),
),
);
}
}
class TileItem extends StatelessWidget {
final int num;
const TileItem({Key key, this.num}) : super(key: key);
@override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Card(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(8.0),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
)
],
),
Positioned(
left: 0.0,
top: 0.0,
bottom: 0.0,
right: 0.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
await Future.delayed(Duration(milliseconds: 200));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return new PageItem(num: num);
},
fullscreenDialog: true,
),
);
},
),
),
),
],
),
),
);
}
}
class PageItem extends StatelessWidget {
final int num;
const PageItem({Key key, this.num}) : super(key: key);
@override
Widget build(BuildContext context) {
AppBar appBar = new AppBar(
primary: false,
leading: IconTheme(data: IconThemeData(color: Colors.white), child: CloseButton()),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.4),
Colors.black.withOpacity(0.1),
],
),
),
),
backgroundColor: Colors.transparent,
);
final MediaQueryData mediaQuery = MediaQuery.of(context);
return Stack(children: <Widget>[
Hero(
tag: "card$num",
child: Material(
child: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
),
Expanded(
child: Center(child: Text("Some more content goes here!")),
)
],
),
),
),
Column(
children: <Widget>[
Container(
height: mediaQuery.padding.top,
),
ConstrainedBox(
constraints: BoxConstraints(maxHeight: appBar.preferredSize.height),
child: appBar,
)
],
),
]);
}
}
编辑:为了回应评论,我将写一篇关于 Hero 工作原理的解释(或者至少我认为它是如何工作的 =D)。
基本上,当页面之间的转换开始时,执行转换的底层机制(或多或少是导航器的一部分)在当前页面和新页面中查找任何“英雄”小部件。 如果找到英雄,则为每个页面计算其大小和位置。
在执行页面之间的转换时,新页面中的英雄会移动到与旧英雄相同位置的叠加层,然后其大小和位置会朝着其在新页面中的最终大小和位置进行动画处理。 (请注意,如果您愿意,您可以通过一些工作进行更改 - 有关更多信息,请参阅此博客)。
这就是 OP 试图实现的目标:
当您点击卡片时,其背景颜色会扩展并成为带有 Appbar 的 Scaffold 的背景颜色。
最简单的方法是简单地将脚手架本身放在英雄中。 其他任何东西都会在过渡期间遮住 AppBar,因为它在进行英雄过渡时处于叠加层中。 请参阅下面的代码。 请注意,我添加了一个类以使转换发生得更慢,因此您可以看到发生了什么,因此要以正常速度查看它,请更改将 SlowMaterialPageRoute 推回 MaterialPageRoute 的部分。
看起来像这样:
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
),
body: ListView.builder(
itemBuilder: (context, index) {
return TileItem(num: index);
},
),
),
);
}
}
Color colorFromNum(int num) {
var random = Random(num);
var r = random.nextInt(256);
var g = random.nextInt(256);
var b = random.nextInt(256);
return Color.fromARGB(255, r, g, b);
}
class TileItem extends StatelessWidget {
final int num;
const TileItem({Key key, this.num}) : super(key: key);
@override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Card(
color: colorFromNum(num),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
type: MaterialType.transparency,
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
)
],
),
Positioned(
left: 0.0,
top: 0.0,
bottom: 0.0,
right: 0.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
await Future.delayed(Duration(milliseconds: 200));
Navigator.push(
context,
SlowMaterialPageRoute(
builder: (context) {
return new PageItem(num: num);
},
fullscreenDialog: true,
),
);
},
),
),
),
],
),
),
);
}
}
class PageItem extends StatelessWidget {
final int num;
const PageItem({Key key, this.num}) : super(key: key);
@override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Scaffold(
backgroundColor: colorFromNum(num),
appBar: AppBar(
backgroundColor: Colors.white.withOpacity(0.2),
),
),
);
}
}
class SlowMaterialPageRoute<T> extends MaterialPageRoute<T> {
SlowMaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
}) : super(builder: builder, settings: settings, fullscreenDialog: fullscreenDialog);
@override
Duration get transitionDuration => const Duration(seconds: 3);
}
但是,在某些情况下,让整个脚手架进行转换可能不是最佳选择 - 可能它有很多数据,或者被设计为适合特定的空间量。 在这种情况下,可以选择制作任何你想做的英雄过渡版本,这本质上是“假的”——即有一个包含两层的堆栈,一层是英雄,有背景颜色、脚手架等等否则你想在过渡期间出现,顶部的另一个层完全遮挡底层(即具有 100% 不透明度的背景),它还有一个应用程序栏和任何你想要的。
可能有比这更好的方法 - 例如,您可以使用我链接到的博客中提到的方法单独指定英雄。
我通过使用Flutter Hero Animation Widget实现了这一点。 为此,您将需要:
现在让我们看看下面的这个例子(你可以在这里找到完整的项目代码):
首先,让我们创建一个 Item 类(我将把它放在 models/item.dart 中)来存储我们的数据。 每个项目都有自己的 id、标题、副标题、详细信息和图片网址:
import 'package:flutter/material.dart';
class Item {
String title, subTitle, details, img;
int id;
Item({this.id, this.title, this.subTitle, this.details, this.img});
}
现在,让我们在 main.dart 文件中初始化我们的材料应用程序:
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/home.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
);
}
}
接下来,我们将制作我们的主页。 它将是一个简单的无状态小部件,并将包含将显示在卡片的 ListView 中的项目列表。 手势检测器用于在点击卡片时展开卡片。 展开只是一个导航到详细信息页面,但是在英雄动画中,它看起来就像只是展开了卡片。
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/details.dart';
import 'package:expanding_card_animation/models/item.dart';
class Home extends StatelessWidget {
List<Item> listItems = [
Item(
id: 1,
title: 'Title 1',
subTitle: 'SubTitle 1',
details: 'Details 1',
img:
'https://d1fmx1rbmqrxrr.cloudfront.net/cnet/i/edit/2019/04/eso1644bsmall.jpg'),
Item(
id: 2,
title: 'Title 2',
subTitle: 'SubTitle 2',
details: 'Details 2',
img:
'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__340.jpg'),
Item(
id: 3,
title: 'Title 3',
subTitle: 'SubTitle 3',
details: 'Details 3',
img: 'https://miro.medium.com/max/1200/1*mk1-6aYaf_Bes1E3Imhc0A.jpeg'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home screen'),
),
body: Container(
margin: EdgeInsets.fromLTRB(40, 10, 40, 0),
child: ListView.builder(
itemCount: listItems.length,
itemBuilder: (BuildContext c, int index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Details(listItems[index])),
);
},
child: Card(
elevation: 7,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey[400], width: 1.0),
borderRadius: BorderRadius.circular(10.0),
),
margin: EdgeInsets.fromLTRB(0, 0, 0, 20),
child: Column(
children: [
//Wrap the image widget inside a Hero widget
Hero(
//The tag must be unique for each element, so we used an id attribute
//in the item object for that
tag: '${listItems[index].id}',
child: Image.network(
"${listItems[index].img}",
scale: 1.0,
repeat: ImageRepeat.noRepeat,
fit: BoxFit.fill,
height: 250,
),
),
Divider(
height: 10,
),
Text(
listItems[index].title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 20,
),
],
),
),
);
}),
),
);
}
}
最后,让我们制作详细信息页面。 它也是一个简单的无状态小部件,它将项目的信息作为输入,并在全屏上显示它们。 请注意,我们将图像小部件包装在另一个 Hero 小部件中,并确保您使用源页面中使用的相同标签(在这里,我们使用了传递项目中的 id):
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/models/item.dart';
class Details extends StatelessWidget {
final Item item;
Details(this.item);
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
extendBodyBehindAppBar: true,
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Hero(
//Make sure you have the same id associated to each element in the
//source page's list
tag: '${item.id}',
child: Image.network(
"${item.img}",
scale: 1.0,
repeat: ImageRepeat.noRepeat,
fit: BoxFit.fitWidth,
height: MediaQuery.of(context).size.height / 3,
),
),
SizedBox(
height: 30,
),
ListTile(
title: Text(
item.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
subtitle: Text(item.subTitle),
),
Divider(
height: 20,
thickness: 1,
),
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
item.details,
style: TextStyle(
fontSize: 25,
),
),
),
],
),
),
),
);
}
}
就是这样,现在您可以根据需要自定义它。 希望我有所帮助。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.