我想在点击时实现材料设计卡的行为。当我点击它时,它应该展开全屏显示其他内容/新页面。我该如何实现?
https://material.io/design/components/cards.html#behavior
我尝试使用Navigator.of(context).push()来显示新页面并播放Hero动画以将卡片背景移动到新的Scaffold,但是由于新页面没有显示,这似乎是不可行的方法从卡本身,或者我不能做到。我正在尝试实现与上面介绍的material.io相同的行为。您能以某种方式指导我吗?
谢谢
答案 0 :(得分:4)
前一段时间,我尝试复制该确切的页面/转换,虽然我没有使它看起来完全像它,但我确实非常接近。请记住,这些操作组合起来很快,并没有真正遵循最佳实践或其他任何方法。
重要的部分是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: 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)进行解释。
基本上,当页面之间的过渡开始时,执行过渡的基础机制(或多或少的Navigator的一部分)会在当前页面和新页面中查找任何“英雄”窗口小部件。如果找到英雄,则会针对每个页面计算其大小和位置。
在页面之间执行过渡时,新页面中的英雄将移动到与旧英雄相同的位置上的叠加层,然后将其大小和位置朝其最终大小和位置进行动画处理。 (请注意,如果您需要做一些工作,可以进行更改-有关更多信息,请参见this blog。)
这是OP试图实现的目标:
点击卡片时,其背景颜色会扩展并变为带有Appbar的脚手架的背景颜色。
最简单的方法是将脚手架本身放入英雄中。在过渡期间,其他任何东西都会使AppBar变得模糊,因为在进行英雄过渡时,它处于覆盖中。请参见下面的代码。请注意,我已经添加了一个类,以使过渡过程变慢,因此您可以看到发生了什么,因此要以正常速度查看它,请更改将SlowMaterialPageRoute推回MaterialPageRoute的部分。
That looks something like this:
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%的不透明度),并且还具有应用栏和您想要的其他任何内容。
可能有比这更好的方法-例如,您可以使用the blog I linked to中提到的方法分别指定英雄。
答案 1 :(得分:0)
我通过使用Flutter Hero Animation Widget实现了这一目标。为此,您将需要:
现在让我们看下面的示例(您可以找到完整的项目代码here):
首先,让我们创建一个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(),
);
}
}
接下来,我们将创建主页。这将是一个简单的无状态小部件,并将包含将在Cards的ListView中显示的项目列表。手势检测器用于在点击时扩展卡。扩展只是导航到详细信息页面,但是使用Hero动画,看起来就像扩展了Card。
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,
),
),
),
],
),
),
),
);
}
}
就是这样,现在您可以根据需要自定义它。希望我能帮上忙。