Skip to content

Conversation

@nate-thegrate
Copy link
Contributor

@nate-thegrate nate-thegrate commented Aug 29, 2024

This pull request has a few goals:

  • Improve performance by updating animations each frame without rebuilding the widget tree
  • Improve code style by factoring out type casts and dynamic annotations
  • Improve the developer experience with an "implicit animation" API that requires 1 class declaration instead of 2

Before

class AnimatedPadding extends ImplicitlyAnimatedWidget {
  const AnimatedPadding({
    super.key,
    required this.padding,
    super.curve,
    required super.duration,
    super.onEnd,
    this.child,
  });

  final EdgeInsetsGeometry padding;
  final Widget? child;

  @override
  AnimatedWidgetBaseState<AnimatedPadding> createState() => _AnimatedPaddingState();
}

class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
  EdgeInsetsGeometryTween? _padding;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _padding = visitor(
      _padding,
      widget.padding,
      (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry),
    ) as EdgeInsetsGeometryTween?;
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: _padding!
        .evaluate(animation)
        .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity),
      child: widget.child,
    );
  }
}

After

class AnimatedPadding extends AnimatedValue<EdgeInsetsGeometry> {
  const AnimatedPadding({
    super.key,
    required EdgeInsetsGeometry padding,
    super.curve,
    super.duration,
    super.onEnd,
    super.child,
  }) : super(value: padding, lerp: EdgeInsetsGeometry.lerp);
  
  @override
  Widget build(BuildContext context, Animation<EdgeInsetsGeometry> animation) {
    return PaddingTransition(padding: animation, child: child);
  }
}




ValueAnimation is essentially "ValueNotifier but with smooth transitions".

Before

class _MyWidgetState extends State<MyWidget> {
  var _alignment = Alignment.bottomCenter;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) {
        setState(() {
          _alignment = Alignment.topCenter;
        });
      },
      onExit: (_) {
        setState(() {
          _alignment = Alignment.bottomCenter;
        });
      },
      child: AnimatedAlign(
        alignment: _alignment,
        duration: Durations.medium2,
        curve: Curves.ease,
        // ...
      ),
    );
  }
}

After

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  // Duration and Curve are pulled from an ancestor DefaultAnimationStyle,
  // and `Alignment.lerp()` will be used to transition between values.
  late final _alignment = ValueAnimation(Alignment.bottomCenter, vsync: this);

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

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => _alignment.value = Alignment.topCenter,
      onExit: (_) => _alignment.value = Alignment.bottomCenter,
      child: AlignTransition(
        alignment: _alignment,
        // ...
      ),
    );
  }
}

This API comes without an AnimationController's boilerplate. The MouseRegion no longer needs to rebuild at all, and when combined with #159757, the "build" stage of the pipeline can be skipped entirely after each animation tick.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. a: animation Animation APIs d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Aug 29, 2024
@nate-thegrate nate-thegrate marked this pull request as ready for review August 30, 2024 04:49
@PurplePolyhedron
Copy link
Contributor

PurplePolyhedron commented Aug 30, 2024

I personally think the AnimatedValue.builder is the easiest and suitable for most cases (and TweenAnimationBuilder is more difficult to use).

It could be a bit confusing to have an abstract class named AnimatedValue while all the other AnimatedWidgets are concrete. How do you feel about using a format similar to StreamBuilderBase and StreamBuilder?

@nate-thegrate
Copy link
Contributor Author

Thanks @PurplePolyhedron! $\textcolor{green}{\textsf{+826 }}\textcolor{#d1242f}{\textsf{−153}}$ is a lot of diffs to go through; I appreciate you taking time to give feedback :)

I personally think the AnimatedValue.builder is the easiest and suitable for most cases

100% agree. I've been using this branch to work on my own Flutter project and was surprised at how often I used some variation of that constructor.


a format similar to StreamBuilderBase and StreamBuilder

I understand that this likely won't happen, but my preference would be to change those stream builder classes instead.

The style guide has a rule to encourage grouping constants under a class name rather than having them in the global namespace, and I'm a huge fan, since it makes autofill suggestions much less cluttered.

The disadvantage of turning a generative constructor into a factory is that you're no longer able to extend the class. But for "builder" classes, you'd never want to do it anyway:

class MyWidget extends Builder {
  MyWidget({super.key})
      : super(builder: (context) {
          return Column(children: [
            // ...
          ]);
        });
}

Extending StatelessWidget directly allows having a const constructor, not to mention how much nicer it looks when the build method isn't passed as a super-constructor argument.

I personally feel that organizing builder classes as constructors under the same namespace (like how PageView.builder() and SliverGrid.builder() are set up) is intuitive and also great for de-cluttering autofill suggestions. Perhaps at some point we could switch Builder to Widget.builder(), but it's probably not a huge deal.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnimatedValue<T> looks like a nice simplification.

nate-thegrate

This comment was marked as resolved.

@nate-thegrate nate-thegrate force-pushed the animated-value branch 2 times, most recently from 80adf1d to 4302178 Compare August 31, 2024 23:29
@nate-thegrate

This comment was marked as resolved.

@nate-thegrate nate-thegrate marked this pull request as draft September 5, 2024 14:49
@nate-thegrate
Copy link
Contributor Author

Okay, here is a naming scheme that I'm decently happy with:

AnimatedValue value initialValue
.builder() initial
AnimatedRotation turns initialTurns
AnimatedScale scale initialScale

I've changed the base AnimatedValue() constructor so that there aren't any positional arguments, and all subclasses follow a foo / initialFoo pattern.

AnimatedValue.builder() is meant to be flexible, so it doesn't have an explicit value name.

@nate-thegrate nate-thegrate marked this pull request as ready for review September 5, 2024 19:53
Copy link
Contributor Author

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

} else {
from = value;
target = newTarget;
value = widget.lerp(from, target, 0)!;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing lerp() methods are set up with nullable parameters and a nullable return type, for example:

class Color {
  static Color? lerp(Color? a, Color? b, double t) {
    // ...
  }
}

The idea is that passing a non-null Color to a and b always gives a non-null output, but there's also an option to lerp to/from null, which could cause the output to be null.

Since this widget's value is non-nullable, we're guaranteed that the lerp function won't ever be passed a null argument, so the only way this could fail is with a user-defined callback that can return null based on non-null inputs.


handle the null case gracefully

That's a great idea—rather than a confusing "null-check operator used on a null value", we could have an assert with a useful message. I'll go ahead and add that, thanks!

@nate-thegrate nate-thegrate force-pushed the animated-value branch 3 times, most recently from 97e8d23 to ace99c1 Compare September 11, 2024 14:27
@nate-thegrate
Copy link
Contributor Author

Marking as a draft, since I've made progress on flutter.dev/go/zero-rebuilds that has some overlap with this PR. Will hopefully get it figured out over the weekend!

@goderbauer
Copy link
Member

@nate-thegrate says he is still looking at this.

@github-actions github-actions bot removed d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Dec 10, 2024
@nate-thegrate nate-thegrate marked this pull request as ready for review December 10, 2024 01:06
@nate-thegrate
Copy link
Contributor Author

An update, finally! 🥳

The plan is to break this addition into multiple PRs, starting with #160005. But feel free to leave any feedback regarding the overall direction here!

@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@nate-thegrate nate-thegrate marked this pull request as draft December 19, 2024 01:11
@flutter-dashboard
Copy link

This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@github-actions github-actions bot added f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Dec 19, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jan 9, 2025
preparing for #154378

It'd be nice to clean up the implicitly animated widget logic,
especially since it could make the upcoming PR easier to review.
@nate-thegrate
Copy link
Contributor Author

This will likely be implemented as a separate package, unless a maintainer from the Flutter team recommends otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: animation Animation APIs d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants