如何在拍打中扩展卡片? [英] How to expand a card on tap in flutter?

查看:100
本文介绍了如何在拍打中扩展卡片?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在点击时实现材料设计卡的行为.当我点击它时,它应该展开全屏显示其他内容/新页面.我该如何实现?

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

我尝试使用Navigator.of(context).push()来显示新页面并播放Hero动画以将卡片背景移动到新的Scaffold,但是由于新页面没有显示,因此这似乎是不可行的方法从卡本身开始,否则我做不到.我正在尝试实现与上面介绍的material.io相同的行为.您能以某种方式指导我吗?

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. I am trying to achieve the same behavior as in the material.io that I presented above. Would you please guide me somehow?

谢谢

推荐答案

前一段时间,我尝试复制该确切的页面/转换,虽然我没有使它看起来很像,但确实做到了.请记住,这些操作很快就放在一起了,实际上并没有遵循最佳做法或其他任何规定.

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.

重要的部分是Hero小部件,尤其是与它们一起使用的标签-如果它们不匹配,则不会这样做.

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.

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).

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).

基本上,当页面之间的过渡开始时,执行过渡的基础机制(或多或少的Navigator的一部分)会在当前页面和新页面中查找任何英雄"窗口小部件.如果找到英雄,则会针对每个页面计算其大小和位置.

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).

这是OP试图实现的目标:

This is what the OP was trying to achieve:

点击卡片时,其背景颜色会扩展并变为带有Appbar的脚手架的背景颜色.

When you tap on a Card, its background color expands and becomes a background color of a Scaffold with an Appbar.

最简单的方法是将脚手架本身放入英雄中.在过渡期间,其他任何东西都会使AppBar变得晦涩难懂,因为在进行英雄过渡时,它位于叠加层中.请参见下面的代码.请注意,我已经添加了一个类,以使过渡过程变慢,因此您可以看到发生了什么,因此要以正常速度查看它,请更改将SlowMaterialPageRoute推回MaterialPageRoute的部分.

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. 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.

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%不透明度的背景),并且还具有应用程序栏和您想要的其他任何内容.

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' - i.e. 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 (i.e. has a background with 100% opacity) that also has an app bar and whatever else you want.

可能有比这更好的方法-例如,您可以使用

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.

这篇关于如何在拍打中扩展卡片?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆