Home > Mobile >  Hero widget transition conflict with other animations
Hero widget transition conflict with other animations

Time:11-30

I'm trying to achieve the Hero & shake upright animation result attached gif here. This is the result I got so far.

Seems like the Hero widget conflicts with the animation I've applied. It seems to be working for the 200 & 300 card. But when 100 is tapped, it seems to be working differently. Attached below are the code for the demo above.

Tried using WidgetsBinding.instance.addPostFrameCallback and SchedulerBinding.instance.addPersistentFrameCallback.

Is there any way to get the expected result instead of using the code I've used?

dummy_data.dart

class _DummyData {
  final IconData icons;
  final Color colors;
  final String backText;

  const _DummyData(this.icons, this.colors, this.backText);
}

const List<_DummyData> _datas = [
  _DummyData(Icons.abc, Colors.blue, '100'),
  _DummyData(Icons.alarm, Colors.red, '200'),
  _DummyData(Icons.shop, Colors.green, '300'),
];

playground_list.dart


class PlayGroundList extends StatelessWidget {
  const PlayGroundList({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: CustomScrollView(
        slivers: [
          const SliverToBoxAdapter(child: SizedBox(height: 50)),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              childCount: _datas.length,
              (context, index) => PlayGroundCardWidget(dummy: _datas[index]),
            ),
          ),
        ],
      ),
    );
  }
}

playground_card_widget.dart

class PlayGroundCardWidget extends StatelessWidget {
  final _DummyData dummy;

  const PlayGroundCardWidget({super.key, required this.dummy});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.5.h),
      height: 350,
      child: GestureDetector(
        onTap: () => Navigator.push(
          context,
          PageRouteBuilder(
            transitionsBuilder: (context, animation, _, child) =>
                FadeTransition(
              opacity: Tween(
                begin: 0.0,
                end: 1.0,
              ).chain(CurveTween(curve: Curves.ease)).animate(animation),
              child: child,
            ),
            pageBuilder: (context, _, __) => PlayGroundDetail(dummy: dummy),
          ),
        ),
        child: Stack(
          alignment: Alignment.center,
          children: [
            Positioned.fill(
              child: Hero(
                tag: dummy.colors.value,
                child: Material(color: dummy.colors),
              ),
            ),
            Align(
              alignment: Alignment.topCenter,
              child: Hero(
                tag: dummy.backText,
                child: Material(
                  color: Colors.transparent,
                  child: Text(
                    dummy.backText,
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.black,
                      fontSize: 140.0,
                    ),
                  ),
                ),
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                SizedBox(height: 3.h),
                Expanded(
                  flex: 12,
                  child: Hero(
                    tag: dummy.icons,
                    child: Icon(dummy.icons, size: 60.0),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

playground_detail.dart

const _shakeDuration = Duration(milliseconds: 900);

class PlayGroundDetail extends StatefulWidget {
  final _DummyData dummy;

  const PlayGroundDetail({super.key, required this.dummy});

  @override
  State<PlayGroundDetail> createState() => _PlayGroundDetailState();
}

class _PlayGroundDetailState extends State<PlayGroundDetail>
    with TickerProviderStateMixin {
  late final PageController _pageController;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          CustomScrollView(
            slivers: [
              const SliverToBoxAdapter(child: SizedBox(height: 50)),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 350,
                  child: Stack(
                    children: [
                      Positioned.fill(
                        child: Hero(
                          tag: widget.dummy.colors.value,
                          child: Material(color: widget.dummy.colors),
                        ),
                      ),
                      Align(
                        alignment: Alignment.topCenter,
                        child: Hero(
                          tag: widget.dummy.backText,
                          child: Material(
                            color: Colors.transparent,
                            child: ShakeTransitionWidget(
                              axis: Axis.vertical,
                              duration: _shakeDuration,
                              offset: 30,
                              child: Text(
                                widget.dummy.backText,
                                textAlign: TextAlign.center,
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                  color: Colors.black,
                                  fontSize: 140.0,
                                ),
                              ),
                            ),
                          ),
                        ),
                      ),
                      Center(
                        child: Hero(
                          tag: widget.dummy.icons,
                          child: ShakeTransitionWidget(
                            axis: Axis.vertical,
                            offset: 5,
                            duration: _shakeDuration,
                            child: Icon(widget.dummy.icons, size: 60.0),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

shake_transition_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class ShakeTransitionWidget extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double offset;
  final Axis axis;

  const ShakeTransitionWidget({
    super.key,
    required this.child,
    required this.duration,
    required this.offset,
    required this.axis,
  });

  @override
  State<ShakeTransitionWidget> createState() => _ShakeTransitionWidgetState();
}

class _ShakeTransitionWidgetState extends State<ShakeTransitionWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );
    _animation = Tween(
      begin: 1.0,
      end: 0.0,
    ).chain(CurveTween(curve: Curves.elasticOut)).animate(_controller);
    // Tried this.
    // WidgetsBinding.instance.addPostFrameCallback((_) => _controller.forward());
    if (SchedulerBinding.instance.schedulerPhase ==
        SchedulerPhase.persistentCallbacks) {
      _controller.forward();
    }
    SchedulerBinding.instance.addPersistentFrameCallback((timeStamp) {});
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (ctx, _) => Transform.translate(
        offset: widget.axis == Axis.horizontal
            ? Offset(_animation.value * widget.offset, 0.0)
            : Offset(0.0, _animation.value * widget.offset),
        child: widget.child,
      ),
    );
  }
}

CodePudding user response:

Wrap your Hero widget with ShakeTransitionWidget instead of the other way around:

// ... Hero is the child, not the parent
ShakeTransitionWidget(
  axis: Axis.vertical,
  duration: _shakeDuration,
  offset: 30,
  child: Hero(
  tag: widget.dummy.backText,
// ...

Now inside the initState() of _ShakeTransitionWidgetState simply call _controller.forward() without any SchedulerBinding or WidgetsBinding.

As a rule of thumb, usually the Hero and its children should not change from page to page. Instead add modifications to the parent widgets.

See gif.

  • Related