Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented May 16, 2019

Description

When a route push transition happens, Hero widgets involved in the animation are all replaced by placeholder widgets (SizedBoxes). This removes their elements (as well as state) from the tree which might not be desirable for the from Heroes if they are stateful.

Related Issues

Fixes #31503 and #32356, somewhat mitigates #31473

Tests

I added the following tests:

  • Hero works with images that don't have both width and height specified
  • From hero's state should be preserved, heroes work well with child widgets that has global keys

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

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

This looks good. Just some trivial formatting feedback.

setState(() {
_placeholderSize = box.size;
});
setState(() => _placeholderSize = box.size);
Copy link
Contributor

Choose a reason for hiding this comment

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

We usually only use => for one-liners that return a value. That's why the original version of this statement was formatted with { }

Same comment below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. But technically the expression evaluates to box.size, and setState does use the returned value (check it's runtime type). Not sure where to draw the line.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's true that setState() checks the return value, but only to warn the user if they've mistakenly returned a Future. I think one-liners that implement a void method should get curly braced.

_proxyAnimation.parent = manifest.animation;

manifest.fromHero.startFlight();
manifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: manifest.type == HeroFlightDirection.push);
Copy link
Member

Choose a reason for hiding this comment

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

We need to be slightly careful here. If the user uses a shuttleBuilder, it could be reasonable for the user to expect that he/she can just use some globalkey that used to be in the from Hero inside the custom shuttleBuilder. That might break if we kept the original tree still under the Hero

Copy link
Contributor Author

Choose a reason for hiding this comment

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

going to add a Limitations section in flightShuttleBuilder's documentation. Is this considered a breaking change?

Copy link
Member

Choose a reason for hiding this comment

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

Then you'd have to explain that that's only true for the Hero of the from route in the event of a push navigation. Perhaps add another argument to Hero for preservePlaceholderStateDuringFlight that defaults to true and gets overridden when a custom placeholderBuilder is given?

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong May 21, 2019

Choose a reason for hiding this comment

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

I think I did:

/// ## Limitations
///
/// Currently if a widget built by [flightShuttleBuilder] takes part in a
/// [Navigator] push transition, that widget or its descendants must not have
/// any [GlobalKey] that is used in the source Hero's descendant widgets.

Isn't flightShuttleBuilder == null always equal to preservePlaceholderStateDuringFlight?
In that case maybe we should add a Size parameter to placeholderBuilder?

? Visibility(
visible: false,
maintainState: true,
maintainAnimation: true,
Copy link
Member

Choose a reason for hiding this comment

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

ummm this is a bit unfortunate. @HansMuller it's not super clear to me why this is needed looking at the cascading logic in Visibility. Do you know?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what you mean by "cascading logic". I agree that keeping the from Hero's animation running while it is hidden seems unnecessary.

Copy link
Member

Choose a reason for hiding this comment

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

I just meant this series of logic

assert(maintainState == true || maintainAnimation == false, 'Cannot maintain animations if the state is not also maintained.'),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replacing the visibility widget with SizedBox(Offstage(TickerMode)) seems to work

@Piinks Piinks added framework flutter/packages/flutter repository. See also f: labels. f: scrolling Viewports, list views, slivers, etc. a: animation Animation APIs labels May 18, 2019
Copy link
Member

@xster xster left a comment

Choose a reason for hiding this comment

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

LGTM

? Visibility(
visible: false,
maintainState: true,
maintainAnimation: true,
Copy link
Member

Choose a reason for hiding this comment

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

I just meant this series of logic

assert(maintainState == true || maintainAnimation == false, 'Cannot maintain animations if the state is not also maintained.'),

_proxyAnimation.parent = manifest.animation;

manifest.fromHero.startFlight();
manifest.fromHero.startFlight(shouldIncludedChildInPlaceholder: manifest.type == HeroFlightDirection.push);
Copy link
Member

Choose a reason for hiding this comment

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

Then you'd have to explain that that's only true for the Hero of the from route in the event of a push navigation. Perhaps add another argument to Hero for preservePlaceholderStateDuringFlight that defaults to true and gets overridden when a custom placeholderBuilder is given?

@LongCatIsLooong LongCatIsLooong added the c: API break Backwards-incompatible API changes label May 23, 2019
@LongCatIsLooong LongCatIsLooong requested a review from xster May 23, 2019 21:44
@Piinks
Copy link
Contributor

Piinks commented May 28, 2019

Is there a link to the breaking change announcement?

@LongCatIsLooong
Copy link
Contributor Author

@Piinks there isn't one yet. I'll post the announcement once the changes are finalized.

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

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

LGTM - just some suggestions about the docs

/// [MaterialRectArcTween].
typedef CreateRectTween = Tween<Rect> Function(Rect begin, Rect end);

/// A builder that builds a placeholder widget given a child and a [Size]
Copy link
Contributor

Choose a reason for hiding this comment

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

A function that builds a [Hero] placeholder widget ...

/// A builder that builds a placeholder widget given a child and a [Size]
///
/// The child can optionally be part of the returned widget tree. The returned
/// widget should typically be sized to [heroSize], if it doesn't do so
Copy link
Contributor

Choose a reason for hiding this comment

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

sized => constrained

///
/// If a widget built by [flightShuttleBuilder] takes part in a [Navigator]
/// push transition, that widget or its descendants must not have any
/// [GlobalKey] that is used in the source Hero's descendant widgets, as both
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to break the "as both" point into a new sentence:

...widgets. Both subtrees will be included ... animation, and global keys must be unique across the entire widget tree.

/// By default, an empty SizedBox keeping the Hero child's original size is
/// left in place once the Hero shuttle has taken flight.
final TransitionBuilder placeholderBuilder;
/// By default the placeholder widget left in place is an empty [SizedBox]
Copy link
Contributor

Choose a reason for hiding this comment

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

Please leave out "left in place"

...placeholder widget is an empty...

final TransitionBuilder placeholderBuilder;
/// By default the placeholder widget left in place is an empty [SizedBox]
/// keeping the Hero child's original size, unless this Hero is a source Hero
/// of a [Navigator] push transition, in which case the placeholder will be a
Copy link
Contributor

Choose a reason for hiding this comment

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

This is run-on. Maybe:
...push transition. If this hero the source of a push transition, the [child] will be a descendant of the placeholder. The child will be kept [Offstage] during the hero's flight.

@Piinks
Copy link
Contributor

Piinks commented Jun 5, 2019

Hey @LongCatIsLooong , this is great. It looks like your branch needs to be updated to fix those codegen errors that are causing test failures.

/// [GlobalKey] that is used in the source Hero's descendant widgets. That is
/// because both subtrees will be included in the widget tree during the Hero
/// flight animation, and [GlobalKey]s must be unique across the entire widget
/// tree.
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't understand this. Are you saying the child will be included twice in the tree? Because that won't work if the widget is stateful... the whole point of the Hero logic is that it only be included once.

@Hixie
Copy link
Contributor

Hixie commented Jun 6, 2019

cc @goderbauer

@LongCatIsLooong LongCatIsLooong merged commit 731e981 into flutter:master Jun 14, 2019
@LongCatIsLooong LongCatIsLooong deleted the maintain-hero-state branch June 14, 2019 03:41
tango5614 added a commit to tango5614/flutter that referenced this pull request Jun 14, 2019
* master: (24 commits)
  [flutter_tool,fuchsia] Update the install flow for packaging migration. (flutter#34447)
  SliverFillRemaining flag for different use cases (flutter#33627)
  SizedBox documentation (flutter#34424)
  Change API doc link to api.dart.dev (flutter#34388)
  2589785 Roll src/third_party/skia 87e885038893..c3252a04b377 (3 commits) (flutter/engine#9327) (flutter#34484)
  ace5d59 Fix rawTypes errors in Android embedding classes (flutter/engine#9326) (flutter#34481)
  bf0def6 Roll src/third_party/skia 4c4945a25248..87e885038893 (1 commits) (flutter/engine#9325) (flutter#34471)
  Roll engine f1d821d..6f5347c (13 commits) (flutter#34466)
  Allow "from" hero state to survive hero animation in a push transition (flutter#32842)
  Roll pub dependencies (flutter#33677)
  Skip flaky test on Windows (flutter#34464)
  Allow flaky tests to pass or fail and mark web tests as flaky (flutter#34456)
  Dont depend on web SDK unless running tests on chrome (flutter#34457)
  Fix semantics_tester (flutter#34368)
  Include raw value in Diagnostics json for basic types (flutter#34417)
  Refactor Gradle plugin (flutter#34353)
  Allow web tests to fail in cirrus config (flutter#34436)
  skip bottom_sheet (flutter#34430)
  Remove unused flag `--target-platform` from `flutter run` (flutter#34369)
  Extract DiagnosticsNode serializer from WidgetInspector (flutter#34012)
  ...
}

return SizedBox(
width: _placeholderSize?.width,
Copy link
Contributor

Choose a reason for hiding this comment

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

This breaks getting sematnics for a FloatingActionButton, e.g.

  await tester.pumpWidget(MaterialApp(
        home: FloatingActionButton(
      onPressed: () {},
    )));

    expect(
        tester.getSemantics(find.byType(FloatingActionButton)),
        matchesSemantics(
          hasTapAction: true,
          hasEnabledState: true,
          isButton: true,
          isEnabled: true,
        ));
 

Now returns the semantics node for the parent of the widget rather than the semantics node for the widget.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fix for this is in #34512

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

a: animation Animation APIs c: API break Backwards-incompatible API changes 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.

Hero animation changes scroll offset of ListView after navigator.pop

7 participants