Некоторое время назад я пытался воспроизвести эту точную страницу / переход, и, хотя я не получал, чтобы она выглядела идеально, я все же довольно близко подошел. Имейте в виду, что это было сделано быстро и на самом деле не следует передовым методам или чему-либо еще.
Важной частью являются виджеты Героев, и особенно теги, которые сопровождают их - если они не совпадают, это не будет сделано.
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).
По сути, когда начинается переход между страницами, базовый механизм, который выполняет переход (более или менее часть Навигатора), ищет любые «геройские» виджеты на текущей странице и новой странице. Если герой найден, его размер и положение рассчитываются для каждой из страниц.
Когда выполняется переход между страницами, герой с новой страницы перемещается в оверлей в том же месте, что и старый герой, а затем его размер и положение анимируются в соответствии с его окончательным размером и положением на новой странице. , (Обратите внимание, что вы можете изменить его, если хотите немного поработать - см. этот блог для получения дополнительной информации об этом).
Вот что ОП пытался достичь:
Когда вы нажимаете на Карту, цвет ее фона расширяется и становится цветом фона Эшафота с панелью приложения.
Самый простой способ сделать это - просто поместить саму эшафот в героя. Все остальное будет скрывать панель приложений во время перехода, так как во время перехода героя он находится в оверлее. Смотрите код ниже. Обратите внимание, что я добавил в класс, чтобы сделать переход более медленным, чтобы вы могли видеть, что происходит, поэтому, чтобы увидеть, как это происходит с нормальной скоростью, измените часть, в которой он возвращает 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%), который также имеет панель приложения и все, что вы хотите.
Возможно, есть лучшие способы сделать это, например, вы можете указать героя отдельно, используя метод, упомянутый в блоге, на который я ссылаюсь .