enjoyingfoss

joined 2 years ago

It turns out, the culprit was (surpisingly!) Impeller.

Turning it off on Android completely solved the problem.

Surprised to see Canonical of all companies prioritizing Windows over Linux and macOS here.

That said, it's a good development nonetheless, and I expect Linux and macOS support to come shortly.

 

I'm trying to implement the OpenContainer transition for go_router in Flutter. It seems like no matter what I do, the end result is choppy. :/

I'm using the native Transition widgets as well as the relatively new SnapshotWidget, but to no avail. Should I maybe use a custom painter? Would that improve performance?

Please take a look at the code below and let me know if you see any ways that I could improve performance.


Here's my attempt at an OpenContainer transition for go_router:

import 'package:flutter/material.dart';
import 'package:your_project_name_xxx/ui_models/container_transition_extra.dart';

class ContainerTransition extends StatefulWidget {
  final ContainerTransitionExtra extra;
  final Animation<double> animation;
  final Widget? sourceWidget;
  final Widget targetWidget;

  const ContainerTransition({
    super.key,
    required this.extra,
    required this.animation,
    required this.sourceWidget,
    required this.targetWidget,
  });

  @override
  State<ContainerTransition> createState() => _ContainerTransitionState();
}

class _ContainerTransitionState extends State<ContainerTransition> {
  static final _toTween = Tween<double>(begin: 0, end: 1);
  static final _fromTween = Tween<double>(begin: 1, end: 0);

  late SnapshotController _snapshotController;
  late CurvedAnimation _curvedAnimation;
  late CurvedAnimation _sourceAnimation;
  late CurvedAnimation _targetAnimation;

  late RelativeRect? _sourcePosition;
  late Animation<double> _scrimOpacityAnimation;
  late Animation<double> _sourceOpacityAnimation;
  late Animation<double> _targetOpacityAnimation;
  late Animation<BorderRadius?> _containerRadiusAnimation;
  late Animation<RelativeRect> _containerPositionAnimation;

  @override
  void initState() {
    super.initState();
    _sourcePosition = widget.extra.tween.begin;
    _snapshotController = SnapshotController(allowSnapshotting: true);
    _curvedAnimation = CurvedAnimation(
      parent: widget.animation,
      curve: Curves.easeInOut,
    );
    _curvedAnimation.addStatusListener((status) {
      if (status.isAnimating) {
        _snapshotController.allowSnapshotting = true;
      } else if (status.isCompleted || status.isDismissed) {
        _snapshotController.allowSnapshotting = false;
      }
    });
    _sourceAnimation = CurvedAnimation(
      parent: _curvedAnimation,
      curve: Interval(0, 1 / 3),
    );
    _targetAnimation = CurvedAnimation(
      parent: _curvedAnimation,
      curve: Interval(1 / 3, 1),
    );
    _scrimOpacityAnimation = _toTween.animate(_sourceAnimation);
    _sourceOpacityAnimation = _fromTween.animate(_sourceAnimation);
    _targetOpacityAnimation = _toTween.animate(_targetAnimation);
    _containerRadiusAnimation = BorderRadiusTween(
            begin: BorderRadius.circular(widget.extra.containerRadius),
            end: BorderRadius.zero)
        .animate(_curvedAnimation);
    _containerPositionAnimation = _curvedAnimation.drive(widget.extra.tween);
  }

  @override
  void dispose() {
    _snapshotController.dispose();
    _sourceAnimation.dispose();
    _targetAnimation.dispose();
    _curvedAnimation.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned.fill(
          child: FadeTransition(
            opacity: _scrimOpacityAnimation,
            child: ColoredBox(color: widget.extra.scrimColor),
          ),
        ),
        PositionedTransition(
          rect: _containerPositionAnimation,
          child: DecoratedBox(
            decoration: BoxDecoration(
              borderRadius: _containerRadiusAnimation.value,
              color: widget.extra.containerColor,
            ),
            child: FadeTransition(
              opacity: _targetOpacityAnimation,
              child: widget.targetWidget,
            ),
          ),
        ),
        if (_sourcePosition != null)
          Positioned.fromRelativeRect(
            rect: _sourcePosition!,
            child: FadeTransition(
              opacity: _sourceOpacityAnimation,
              child: widget.sourceWidget,
            ),
          ),
      ],
    );
  }
}

Here's how I'm building the route:

Page<T> buildContainerRoute<T>(
    BuildContext context,
    GoRouterState state,
    Ref ref,
    Widget child,
  ) {
    final containerExtra = state.extra;

    if (containerExtra is ContainerTransitionExtra) {
      final registryTag = containerExtra.sourceBuilderTag;
      final registryBuilder =
          registryTag != null ? SourceBuilderRegistry().get(registryTag) : null;
      if (registryBuilder != null) {
        if (registryBuilder.wasUsedAlready) {
          SourceBuilderRegistry().unregister(registryTag!);
        } else {
          SourceBuilderRegistry().markAsUsed(registryTag!);
        }
      }
      return CustomTransitionPage<T>(
        key: state.pageKey,
        child: child,
        transitionDuration: Durations.medium2,
        reverseTransitionDuration: Durations.medium1,
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          return ContainerTransition(
            animation: animation,
            extra: containerExtra,
            sourceWidget:
                Center(child: registryBuilder?.registryItem.call(context)),
            targetWidget: ClipRect(
              child: OverflowBox(
                alignment: Alignment.topCenter,
                maxWidth: containerExtra.cachedMaxWidth,
                maxHeight: containerExtra.cachedMaxHeight,
                child: child,
              ),
            ),
          );
        },
      );
    } else {
      if (!kIsWeb && Platform.isAndroid) {
        return MaterialPage(key: state.pageKey, child: child);
      } else {
        return CupertinoPage(key: state.pageKey, child: child);
      }
    }
  }