Контейнер транзакций Flutter Hero с условными виджетами - PullRequest
1 голос
/ 29 марта 2020

Я пытаюсь реализовать транзакцию героя, которая проходит гладко, но у контейнера, который я перемещаю, есть два варианта (маленький / большой).

Большой:

big container

Малый:

small container

Как видите, маленькая версия такая же, как и большая , но только некоторые элементы отсутствуют. Версия, которая должна быть отображена, задается с помощью свойства 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
)))

Теперь переход выглядит следующим образом:

hero transition

Как вы можете видеть, он работает довольно хорошо, но это немного быстро, так как я использую условный рендеринг здесь. Также обратный переход выдает ошибку:

A RenderFlex overflowed by 96 pixels on the bottom.

Я полагаю, это потому, что на обратном пути пространство внезапно переполняется, потому что эти дополнительные виджеты визуализируются.

Теперь мой вопрос, как правильно создать компонент героя, который должен переходить с условными элементами. Или, если виджет hero не подходит для этого, как я могу достичь того же результата с помощью некоторых пользовательских анимаций?

Ответы [ 2 ]

1 голос
/ 29 марта 2020

Воспользуйтесь flightShuttleBuilder. В этом конструкторе создайте новый TicCard, который принимает анимацию hero. Вы можете использовать это animation сейчас, чтобы анимировать все виды во время flight (переход экрана).

Одна вещь, с которой мне не комфортно, это _animationWidget. Что он делает: он оборачивает все виджеты внутри FadeTransition и SizeTransition, если нет animation и isSmall true, он возвращает пустой 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));
  })

Результат:

result

Результат в замедленном режиме:

result in slow motion

1 голос
/ 29 марта 2020

Оберните вашу колонку внутри TicCard с SingleChildScrollView

enter image description here

import '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,
            ),
          ),
        ),
      ),
    );
  }
}
...