简体   繁体   English

如何在颤动中扩展卡片?

[英]How to expand a card on tap in flutter?

I would like to achieve the material design card behavior on tap.我想立即实现材料设计卡的行为。 When I tap it, it should expand fullscreen and reveal additional content/new page.当我点击它时,它应该展开全屏并显示其他内容/新页面。 How do I achieve it?我如何实现它?

https://material.io/design/components/cards.html#behavior https://material.io/design/components/cards.html#behavior

I tried with Navigator.of(context).push() to reveal new page and play with Hero animations to move the card background to new Scaffold, however it seems it is not the way to go since new page is not revealing from the card itself, or I cannot make it to.我尝试使用 Navigator.of(context).push() 来显示新页面并使用 Hero 动画将卡片背景移动到新的 Scaffold,但是这似乎不是方法,因为新页面没有从卡片中显示出来本身,否则我无法做到。 I am trying to achieve the same behavior as in the material.io that I presented above.我正在尝试实现与我上面介绍的 material.io 中相同的行为。 Would you please guide me somehow?你能以某种方式指导我吗?

Thank you谢谢

A while ago I tried replicating that exact page/transition and while I didn't get it to look perfectly like it, I did get fairly close.不久前,我尝试复制那个确切的页面/过渡,虽然我没有让它看起来很像,但我确实很接近。 Keep in mind that this was put together quickly and doesn't really follow best practices or anything.请记住,这是快速组合在一起的,并没有真正遵循最佳实践或任何东西。

The important part is the Hero widgets, and especially the tags that go along with them - if they don't match, it won't do it.重要的部分是 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,
          )
        ],
      ),
    ]);
  }
}

EDIT: in response to a comment, I'm going to write an explanation of how Hero works (or at least how I think it works =D).编辑:为了回应评论,我将写一篇关于 Hero 工作原理的解释(或者至少我认为它是如何工作的 =D)。

Basically, when a transition between pages is started, the underlying mechanism that performs the transition (part of the Navigator more or less) looks for any 'hero' widgets in the current page and the new page.基本上,当页面之间的转换开始时,执行转换的底层机制(或多或少是导航器的一部分)在当前页面和新页面中查找任何“英雄”小部件。 If a hero is found, its size and position is calculated for each of the pages.如果找到英雄,则为每个页面计算其大小和位置。

As the transition between the pages is performed, the hero from the new page is moved to an overlay in the same place as the old hero, and then its size and position is animated towards its final size and position in the new page.在执行页面之间的转换时,新页面中的英雄会移动到与旧英雄相同位置的叠加层,然后其大小和位置会朝着其在新页面中的最终大小和位置进行动画处理。 (Note that you can change if you want with a bit of work - see this blog for more information about that). (请注意,如果您愿意,您可以通过一些工作进行更改 - 有关更多信息,请参阅此博客)。

This is what the OP was trying to achieve:这就是 OP 试图实现的目标:

When you tap on a Card, its background color expands and becomes a background color of a Scaffold with an Appbar.当您点击卡片时,其背景颜色会扩展并成为带有 Appbar 的 Scaffold 的背景颜色。

The easiest way to do this is to simply put the scaffold itself in the hero.最简单的方法是简单地将脚手架本身放在英雄中。 Anything else will obscure the AppBar during the transition, as while it's doing the hero transition it is in an overlay.其他任何东西都会在过渡期间遮住 AppBar,因为它在进行英雄过渡时处于叠加层中。 See the code below.请参阅下面的代码。 Note that I've added in a class to make the transition happen slower so you can see what's going on, so to see it at normal speed change the part where it pushes a SlowMaterialPageRoute back to a MaterialPageRoute.请注意,我添加了一个类以使转换发生得更慢,因此您可以看到发生了什么,因此要以正常速度查看它,请更改将 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);
}

However, there are situations in which it might not be optimal to have the entire scaffold doing the transition - maybe it has a lot of data, or is designed to fit in a specific amount of space.但是,在某些情况下,让整个脚手架进行转换可能不是最佳选择 - 可能它有很多数据,或者被设计为适合特定的空间量。 In that case, an option to make a version of whatever you want to do the hero transition that is essentially a 'fake' - ie have a stack with two layers, one which is the hero and has a background colour, scaffold, and whatever else you want to show up during the transition, and another layer on top which completely obscures the bottom layer (ie has a background with 100% opacity) that also has an app bar and whatever else you want.在这种情况下,可以选择制作任何你想做的英雄过渡版本,这本质上是“假的”——即有一个包含两层的堆栈,一层是英雄,有背景颜色、脚手架等等否则你想在过渡期间出现,顶部的另一个层完全遮挡底层(即具有 100% 不透明度的背景),它还有一个应用程序栏和任何你想要的。

There are probably better ways of doing it than that - for example, you could specify the hero separately using the method mentioned in the blog I linked to .可能有比这更好的方法 - 例如,您可以使用我链接到的博客中提到的方法单独指定英雄。

I achieved this by using the Flutter Hero Animation Widget .我通过使用Flutter Hero Animation Widget实现了这一点。 In order to do that you will need:为此,您将需要:

  1. A source page where you start from and that contains the card you want to expand to full screen.您从中开始的源页面,其中包含您想要扩展到全屏的卡片。 Let's call it 'Home'让我们称之为“家”
  2. A destination page that will represent how your card will look like once expanded.一个目标页面,将代表您的卡片展开后的外观。 Let's call it 'Details'.我们称之为“细节”。
  3. (Optional) A data model to store data (可选)用于存储数据的数据模型

Now let's take a look at this example below (You can find the full project code here ):现在让我们看看下面的这个例子(你可以在这里找到完整的项目代码):

First, let's make an Item class (i will put it in models/item.dart) to store our data.首先,让我们创建一个 Item 类(我将把它放在 models/item.dart 中)来存储我们的数据。 Each item will have its own id, title, subtitle, details and image url :每个项目都有自己的 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});
}

Now, let's initialize our material app in the main.dart file :现在,让我们在 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(),
    );
  }
}

Next, we will make our home page.接下来,我们将制作我们的主页。 It'll be a simple stateless widget, and will contain a list of Items that will be displayed in a ListView of Cards.它将是一个简单的无状态小部件,并将包含将显示在卡片的 ListView 中的项目列表。 A gesture detector is used to expand the card when tapping it.手势检测器用于在点击卡片时展开卡片。 The expansion is just a navigation to the details page, but with the Hero animation, it looks like it just expanded the 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,
                      ),
                    ],
                  ),
                ),
              );
            }),
      ),
    );
  }
}

Finally, let's make the details page.最后,让我们制作详细信息页面。 It's also a simple stateless widget that will take the item's info as an input, and display them on full screen.它也是一个简单的无状态小部件,它将项目的信息作为输入,并在全屏上显示它们。 Note that we wrapped the image widget inside another Hero widget, and make sure that you use the same tags used in the source page(here, we used the id in the passed item for that) :请注意,我们将图像小部件包装在另一个 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,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

And that's it, now you can customize it as you wish.就是这样,现在您可以根据需要自定义它。 Hope i helped.希望我有所帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM