带有条件小部件的 Flutter hero 事务容器

Flutter hero transaction container with conditional widgets

我正在尝试实施一个进展顺利的英雄交易,但我正在转换的容器有两个变体 (small/big)。

大:

小:

如您所见,小版本与大版本相同,只是少了一些元素。需要渲染的版本设置为属性 isSmall.

组件如下所示:

class TicPackage extends StatelessWidget {
  TicPackage({this.package, this.onTap, this.isSmall = false});

  final Package package;
  final bool isSmall;
  final Function() onTap;

  final NumberFormat currencyFormatter =
      NumberFormat.currency(locale: "nl", decimalDigits: 2, symbol: "€");

  @override
  Widget build(BuildContext context) {
    Widget titleText = Text(
      package.name,
      style: TextStyle(
          color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold),
    );

    return TicCard(
      color: package.color,
      elevation: 4,
      onTap: onTap,
      children: <Widget>[
        Row(
          children: <Widget>[
            isSmall
                ? titleText
                : Text("${package.eventCount} evenementen",
                    style:
                        TextStyle(color: Color.fromRGBO(255, 255, 255, 0.5))),
            Text(
              "${currencyFormatter.format(package.price)}",
              style: TextStyle(
                  color: Colors.white,
                  fontSize: 22,
                  fontWeight: FontWeight.bold),
            ),
          ],
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
        ),
        if (!isSmall)
          Padding(padding: EdgeInsets.only(top: 10), child: titleText),
        Padding(
            padding: EdgeInsets.only(top: 2),
            child: Text(package.description,
                style: TextStyle(color: Colors.white))),
        if (!isSmall)
          Padding(
              padding: EdgeInsets.only(top: 12),
              child: Text(package.goods,
                  style: TextStyle(
                      color: Colors.white, fontStyle: FontStyle.italic))),
        if (!isSmall)
          Padding(
              padding: EdgeInsets.only(top: 10),
              child: Container(
                child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 10, vertical: 3),
                    child: Text(
                      "${currencyFormatter.format(package.discount)} korting",
                      style: TextStyle(color: Colors.white),
                    )),
                decoration: BoxDecoration(
                    border:
                        Border.all(color: Color.fromRGBO(255, 255, 255, 0.5)),
                    borderRadius: BorderRadius.circular(100)),
              ))
      ],
    );
  }
}

屏幕 A:

Hero(
    tag: "package_${args.package.id}",
    child: TicPackage(
      isSmall: false,
      package: args.package
)))

屏幕 B:

Hero(
    tag: "package_${args.package.id}",
    child: TicPackage(
      isSmall: true,
      package: args.package
)))

现在转换如下所示:

如您所见,它工作得很好,但由于我在这里使用条件渲染,所以有点活泼。后向转换也报错:

A RenderFlex overflowed by 96 pixels on the bottom.

我想这是因为在返回的路上 space 突然溢出,因为那些额外的小部件正在渲染。

现在我的问题是如何正确创建一个需要使用条件元素进行转换的英雄组件。或者,如果 hero 小部件不适合此操作,我如何通过执行一些自定义动画来获得相同的结果?

使用 SingleChildScrollView 将您的列包裹在 TicCard 中

导入'package:flutter/material.dart';

import 'page2.dart';

class TicCard extends StatelessWidget {
  final List<Widget> children;
  final double elevation;
  final Color color;

  const TicCard({
    Key key,
    this.children,
    this.elevation,
    this.color,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => Navigator.of(context).push(
        MaterialPageRoute(
          builder: (_) => Page2(),
        ),
      ),
      child: Card(
        elevation: elevation,
        color: color,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: children,
            ),
          ),
        ),
      ),
    );
  }
}

利用flightShuttleBuilder。在这个构建器中创建一个新的 TicCard,它采用 hero 动画。您现在可以使用此 animationflight(屏幕转换)期间为所有视图设置动画。

我不满意的一件事是 _animationWidget。它的作用:它将所有 Widget 包装在 FadeTransitionSizeTransition 中,如果没有 animationisSmall 为真,则 returns 为空 Container.

小部件:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:ticketapp_pakket/components/tic-card.dart';
import 'package:ticketapp_pakket/models/package.dart';

class TicPackage extends StatelessWidget {
  TicPackage(
      {this.heroTag,
      this.package,
      this.onTap,
      this.isSmall = false,
      this.animation});

  final String heroTag;
  final Animation<double> animation;
  final Package package;
  final bool isSmall;
  final Function() onTap;

  final NumberFormat currencyFormatter =
      NumberFormat.currency(locale: "nl", decimalDigits: 2, symbol: "€");

  Widget _animationWidget({Widget child}) {
    return animation != null
        ? FadeTransition(
            opacity: animation,
            child: SizeTransition(
                axisAlignment: 1.0, sizeFactor: animation, child: child))
        : !isSmall ? child : Container();
  }

  @override
  Widget build(BuildContext context) {
    Widget eventCountText = _animationWidget(
        child: Padding(
            padding: EdgeInsets.only(bottom: 10),
            child: Text("${package.eventCount} evenementen",
                style: TextStyle(color: Color.fromRGBO(255, 255, 255, 0.5)))));

    Widget goodsText = _animationWidget(
      child: Padding(
          padding: EdgeInsets.only(top: 12),
          child: Text(package.goods,
              style:
                  TextStyle(color: Colors.white, fontStyle: FontStyle.italic))),
    );

    Widget discountText = _animationWidget(
        child: Padding(
            padding: EdgeInsets.only(top: 10),
            child: Container(
              child: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 10, vertical: 3),
                  child: Text(
                    "${currencyFormatter.format(package.discount)} korting",
                    style: TextStyle(color: Colors.white),
                  )),
              decoration: BoxDecoration(
                  border: Border.all(color: Color.fromRGBO(255, 255, 255, 0.5)),
                  borderRadius: BorderRadius.circular(100)),
            )));

    Widget titleText = Text(
      package.name,
      style: TextStyle(
          color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold),
    );

    Widget card = TicCard(
        color: package.color,
        borderRadius: BorderRadius.circular(10),
        margin: EdgeInsets.only(left: 20, right: 20, bottom: 10, top: 5),
        onTap: onTap,
        child: Container(
          padding: EdgeInsets.all(15),
          child: Stack(
            children: <Widget>[
              Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  eventCountText,
                  titleText,
                  Padding(
                      padding: EdgeInsets.only(top: 2),
                      child: Text(package.description,
                          style: TextStyle(color: Colors.white))),
                  goodsText,
                  discountText,
                ],
              ),
              Positioned(
                  child: Text(
                    "${currencyFormatter.format(package.price)}",
                    style: TextStyle(
                        color: Colors.white,
                        fontSize: 22,
                        fontWeight: FontWeight.bold),
                  ),
                  top: 0,
                  right: 0)
            ],
          ),
        ));

    if (heroTag == null) {
      return card;
    }

    return Hero(
        tag: heroTag,
        flightShuttleBuilder: (
          BuildContext flightContext,
          Animation<double> animation,
          HeroFlightDirection flightDirection,
          BuildContext fromHeroContext,
          BuildContext toHeroContext,
        ) {
          return TicPackage(
            package: package,
            animation: ReverseAnimation(animation),
          );
        },
        child: card);
  }
}

如何使用插件:

在两个屏幕上使用 TicPackage 小部件并使用相同的 heroTag

TicPackage(
  heroTag: "package_1",
  package: package,
  onTap: () {
    Navigator.pushNamed(context, '/package-detail',
      arguments: PackageDetailPageArguments(package: package));
  })

结果:

慢动作结果: