Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter hero transaction container with conditional widgets

I'm trying to implement a hero transaction which is going smoothly, but the container that I'm transitioning has two variants (small/big).

Big:

big container

Small:

small container

As you can see is the small version the same as the big one, but just with some elements missing. The version that needs to be rendered is set with a property isSmall.

The component looks as followed:

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)),
              ))
      ],
    );
  }
}

Screen A:

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

Screen B:

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

Now the transition looks as followed:

hero transition

As you can see it's working quite well, but it's a little bit snappy since I'm using conditional rendering here. Also the back transition gives an error:

A RenderFlex overflowed by 96 pixels on the bottom.

I guess this is because on the way back the space suddenly overflows because those extra widgets are getting rendered.

Now my question is how to properly create a hero component that needs to transition with conditional elements. Or if a hero widget isn't suited for this how can I achieve the same result with doing some custom animations?

like image 886
toonvanstrijp Avatar asked Jan 18 '26 05:01

toonvanstrijp


2 Answers

Wrap your Column inside TicCard with 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,
            ),
          ),
        ),
      ),
    );
  }
}
like image 116
Kherel Avatar answered Jan 21 '26 08:01

Kherel


Make use of the flightShuttleBuilder. Within this builder create a new TicCard that takes the hero animation. You can use this animation now to animate all views during flight (screen transition).

One thing that I'm not comfortable with is the _animationWidget. What it does: it wraps all the Widgets inside an FadeTransition and SizeTransition, if there is no animation and isSmall is true it returns an empty Container.

The widget:

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);
  }
}

How to use the widget:

Use the TicPackage widget on both screens and use the same heroTag.

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

Result:

result

Result in slow motion:

result in slow motion

like image 21
toonvanstrijp Avatar answered Jan 21 '26 08:01

toonvanstrijp