Skip to content

Conversation

@veloce
Copy link
Contributor

@veloce veloce commented May 26, 2024

This PR is making the CupertinoNavigationBar and CupertinoSliverNavigationBar appear transparent as long as the content is not scrolled under them, so they look like standard iOS apps nav bars.

cupertino_demo.mov
Enregistrement.de.l.ecran.2024-05-31.a.14.49.18.mov
Demo app code
import 'package:flutter/cupertino.dart';

/// Flutter code sample for [CupertinoTabScaffold].

void main() => runApp(const TabScaffoldApp());

class TabScaffoldApp extends StatefulWidget {
const TabScaffoldApp({super.key});

@override
State<TabScaffoldApp> createState() => _TabScaffoldAppState();
}

class _TabScaffoldAppState extends State<TabScaffoldApp> {
Brightness _brightness = Brightness.light;

@override
Widget build(BuildContext context) {
  return CupertinoApp(
    theme: CupertinoThemeData(brightness: _brightness),
    home: TabScaffoldExample(
        brightness: _brightness, onBrightnessToggle: _toggleBrightness),
  );
}

void _toggleBrightness() {
  setState(() {
    _brightness =
        _brightness == Brightness.light ? Brightness.dark : Brightness.light;
  });
}
}

class TabScaffoldExample extends StatefulWidget {
const TabScaffoldExample(
    {required this.brightness, required this.onBrightnessToggle, super.key});

final VoidCallback onBrightnessToggle;
final Brightness brightness;

@override
State<TabScaffoldExample> createState() => _TabScaffoldExampleState();
}

class _TabScaffoldExampleState extends State<TabScaffoldExample> {
@override
Widget build(BuildContext context) {
  return CupertinoTabScaffold(
    tabBar: CupertinoTabBar(
      items: const <BottomNavigationBarItem>[
        BottomNavigationBarItem(
          icon: Icon(CupertinoIcons.home),
          label: 'Home',
        ),
        BottomNavigationBarItem(
          icon: Icon(CupertinoIcons.search_circle_fill),
          label: 'Explore',
        ),
        BottomNavigationBarItem(
          icon: Icon(CupertinoIcons.person_fill),
          label: 'Profile',
        ),
        BottomNavigationBarItem(
          icon: Icon(CupertinoIcons.settings_solid),
          label: 'Settings',
        ),
      ],
    ),
    tabBuilder: (BuildContext context, int index) {
      return CupertinoTabView(
        builder: (BuildContext context) {
          return CupertinoPageScaffold(
            backgroundColor: index == 3
                ? CupertinoColors.secondarySystemBackground
                    .resolveFrom(context)
                : null,
            child: CustomScrollView(
              slivers: [
                CupertinoSliverNavigationBar(
                  largeTitle: Text('Tab $index'),
                  initiallyTransparent: index != 2,
                  trailing: CupertinoButton(
                    padding: EdgeInsets.zero,
                    onPressed: widget.onBrightnessToggle,
                    child: Icon(
                      widget.brightness == Brightness.light
                          ? CupertinoIcons.moon_stars
                          : CupertinoIcons.sun_max,
                    ),
                  ),
                ),
                SliverSafeArea(
                  top: false,
                  sliver: SliverList.list(
                    children: [
                      CupertinoButton(
                        child: const Text('Next page'),
                        onPressed: () {
                          Navigator.of(context).push(
                            CupertinoPageRoute<void>(
                              builder: (BuildContext context) {
                                return CupertinoPageScaffold(
                                  navigationBar: CupertinoNavigationBar(
                                    middle: Text('Inner page of tab $index'),
                                  ),
                                  child: ListView(
                                    children: [
                                      Center(
                                        child: CupertinoButton(
                                          child: const Text('Back'),
                                          onPressed: () {
                                            Navigator.of(context).pop();
                                          },
                                        ),
                                      ),
                                      if (index == 0) const _LongList(),
                                      const SizedBox(height: 20),
                                    ],
                                  ),
                                );
                              },
                            ),
                          );
                        },
                      ),
                      if (index == 1) const _LongList(),
                      const SizedBox(height: 20),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      );
    },
  );
}
}

class _LongList extends StatelessWidget {
const _LongList();

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      for (int i = 0; i < 50; i++) ...[
        CupertinoListTile(
          leading: const Icon(CupertinoIcons.book),
          title: Text('Bookstore item $i'),
        ),
      ],
    ],
  );
}
}

This is the continuation of #142439.

I tried to keep the simplest API possible, so it's only introducing a new automaticBackgroundVisibility boolean parameter.

In the implementation I had to use the CupertinoPageScaffold background color to make it look transparent instead of a 0 opacity, because of issues with route transitions.

I used an InheritedWidget so the nav bar is always getting the right background color from the parent scaffold, whether it is overridden or not. It would probably be better to make the inherited widget private but we'd need to move all the nav bar code to the same file as the scaffold, so for now I've just hidden it from the export. Let me know if it is okay to do that.

This PR is not dealing with the bottom tab bar, because the same straightforward approach doesn't work here. The problem is that the scroll notification is sent only when the scroll view is created or updated, so it doesn't work if one pushes a screen and navigates back.

Issues:

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: cupertino flutter/packages/flutter/cupertino repository d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels May 26, 2024
@veloce veloce marked this pull request as ready for review May 27, 2024 09:00
@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from fe59aa7 to 67a388b Compare May 27, 2024 09:00
@Piinks Piinks requested a review from MitchellGoodwin May 29, 2024 18:24
@Piinks
Copy link
Contributor

Piinks commented May 29, 2024

Hey @veloce thanks for contributing! It looks like there are some failing checks here, can you take a look?

@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from 67a388b to 5f739dc Compare May 30, 2024 08:36
Copy link
Contributor Author

@veloce veloce May 30, 2024

Choose a reason for hiding this comment

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

I made this change because there were errors in the flutter_gallery smoke tests:

Null check operator used on a null value

When the exception was thrown, this was the stack:
#0      Element.widget (package:flutter/src/widgets/framework.dart:3478:31)
#1      CupertinoDynamicColor.toString (package:flutter/src/cupertino/colors.dart:1105:135)
#2      DiagnosticsProperty.valueToString (package:flutter/src/foundation/diagnostics.dart:2616:60)
#3      DiagnosticsProperty.toDescription (package:flutter/src/foundation/diagnostics.dart:2633:21)
...
#111    RenderObject.toStringDeep (package:flutter/src/rendering/object.dart:3948:12)
#112    smokeDemo (dev/integration_tests/flutter_gallery/test/smoke_test.dart:82:102)

I could not yet track it down to the exact cause. When adding debug in the CupertinoDynamicColor.toString I could see a bunch of some defunct StatelessElement, so I believe this is because of the background color transition.
If I remove the Color.lerp in the nav_bar code, or if I pass the same color to both a and b of Color.lerp there are no more errors.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a problematic breaking change. It could make existing tests break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can understand why it could be an unwanted change: if it would hide potential errors that are not supposed to happen.

But making existing tests break, I don't think it would do that. If a test call CupertinoDynamicColor.toString on a CupertinoDynamicColor that has defunct Element as _debugResolveContext, it would fail anyway. Like in the smoke tests here.

Now the question is: are we hiding problematic behaviour by making this change?

Using only this PR as context, I'm not so sure it is a problem. I have isolated precisely the widget responsible for the error, as I explained in my previous comment. It is the persistent nav bar widget (which creates a color widget where the error happens). A simple

    print('${DateTime.now()} _persistentNavBar ${'${objectRuntimeType(context, '<optimized out>')}#${shortHash(context)}'}');

in its build method show that when we scroll, the framework will remove the existing Element associated to the widget from the tree and create another one, about at the moment the background color transition of the nav bar starts.

That the framework is doing such things in the background seems normal to me and should be transparent to the developer, so in theory I shouldn't try to change the code to make the framework behave differently.

So my conclusion is that with this patch, we've hit an edge case that has not occurred yet, involving the flutter_gallery smoke test toStringDeep and the CupertinoDynamicColor.toString which tries to get the context on a defunct element. So this change seems harmless after all.

What do you think @MitchellGoodwin ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still nervous that this test breakage is showing some weird behaviors. Is the setState on the observer causing too much of a rebuild on every scroll? That would be a difference from the other PR.

I'd like to track down exactly why this is failing now and wasn't before. I'm worried about introducing a weird hiccup to app scrolling because that has a large affect in how the app feels. @Piinks are you able to take a look?

Copy link
Contributor Author

@veloce veloce Jun 4, 2024

Choose a reason for hiding this comment

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

What I found is that if you replace in my PR:

    final Color effectiveBackgroundColor = initiallyTransparent
        ? Color.lerp(parentPageScaffoldBackgroundColor ?? backgroundColor, backgroundColor, shrinkAnimationValue) ?? backgroundColor
        : backgroundColor;

by

    final Color effectiveBackgroundColor = initiallyTransparent
        ? Color.lerp(backgroundColor.withOpacity(0), backgroundColor, shrinkAnimationValue) ?? backgroundColor
        : backgroundColor;

in _LargeTitleNavigationBarSliverDelegate.build(), then the framework is not replacing the element. I don't know why but it seems it can manage to keep the element when both colors are the same and only the opacity changes during a transition, but not when the color changes.

This is the reason why the tests were passing in the previous PR.

And I do not observe a difference in the number of rebuilds compared to current master.

cc @MitchellGoodwin and @Piinks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also found that if you replace

Color.lerp(backgroundColor.withOpacity(0), backgroundColor, shrinkAnimationValue)

with

backgroundColor.withOpacity(shrinkAnimationValue)

the element is also replaced. I have no idea why 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. I wonder if it's because parentPageScaffoldBackgroundColor is being set to a final instead of a const, and it's relying on a value from outside the widget's scope, so it's triggering replacing the widget? Similarly, withOpacity is not a static function, but .lerp is, and the withOpacity in that .lerp uses a constant value.

So is setting the parentPageScaffoldBackgroundColor to a const possible, and does that work? If not, would going the route with

Color.lerp(backgroundColor.withOpacity(0), backgroundColor, shrinkAnimationValue)

work? Does content show up in the navbar if partially scrolled? Would a safe area need to be added?

Copy link
Contributor Author

@veloce veloce Jun 5, 2024

Choose a reason for hiding this comment

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

I finally found what causes the widget to be replaced: the BackdropFilter. Since the _wrapWithBackground creates a backdrop filter only if the background is fully opaque, when we transition from the scaffold color to the bar color, a widget is returned with or without the backdrop filter, which is triggering the element to be replaced.

If I change the code to always return the backdrop filter:

  return ClipRect(
    child: BackdropFilter(
      filter: backgroundColor.alpha == 0xFF ? ImageFilter.blur() : ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
      child: childWithBackground,
    ),
  );

Now we don't have the widget replaced.

Note that going with the withOpacity(0) route, in order to avoid the above change, is not possible, because there is a blur artifact visible during the initial state, making it mandatory to update the check to if (backgroundColor.alpha == 0xFF || backgroundColor.alpha == 0).

What should I do now? My guess is that it is preferable to always create the BackdropFilter, even though there is a 0 blur (is there a better way?) and remove the problematic change in CupertinoDynamicColor.toString; but I'm waiting for your input.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great catch!

So that blur has been around for as long as the navbar has been in Flutter, but it's not gospel. Doing some digging, it's to allow for the navbar to be made transparent and have content scroll under it. It could theoretically be used to do this logic now, but that's a lot of work (as these PRs show) and ideally we would be able to do this by default. I think we should be looking at whether we want that BackdropFilter at all anymore, or at least not use it when the opacity can change dynamically.

Taking a step back, and looking at how SwiftUI handles native navbar, this logic is controlled by .toolbarBackground(_:for:). It can be applied to either the NavBar or the TabBar, and controls whether the background is hidden or not with an enum with three values.

It works like this, as compared to Flutter:

Background Visibility SwiftUI Flutter
Visible Always shows the background Color Currently default in Flutter
Hidden Always has the background Color as transparent Flutter allows this, and applies a blur to underlying content. Currently if the opacity changes from 1.0 it replaces the widget. We want to continue to allow doing any opacity value to customize the transparency for statically transparent NavBars.
Automatic Default in SwiftUI. Makes the background transparent until content scrolls under it. Flutter does not support this by default, and what this PR is trying to add.

I think we want to support all of this behavior. We could add an enum like Swift does, but my opinion is that it's better to add a bool property that turns off automatically changing the transparency of the navbar (it should change transparency by default). And if that is turned off, then we should do the blur like before. That would also make it easier to not swap in and out the BackdropFilter as opacity changes if the bool is true.

Copy link
Contributor Author

@veloce veloce Jun 6, 2024

Choose a reason for hiding this comment

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

Thanks for the detailed explanation!

If I understand correctly, flutter supports the "always transparent" behavior by allowing to explicitly pass a fully transparent background color. I think one of the known app that uses such an always hidden background is Signal.

I changed the boolean parameter name to make it clear that the widget supports all of this behavior. So it is now named automaticBackgroundVisibility.

It still feels like a workaround but I had to keep the parent scaffold background color to keep the route transitions looking as expected (don't have the time to figure out if it can be done differently).

I had to remove the tests related to the backdrop filter blur checks, because it always creates the BackdropFilter now. In theory we could avoid creating it if the automaticBackgroundVisibility is false AND the background fully opaque. I felt it was more confusing than useful to do that.

I will post new demos showcasing the 3 versions in this table.

@flutter-dashboard
Copy link

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

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.

Changes reported for pull request #149102 at sha 6056f5cb657987e300670d57dfff726d338f15a8

@flutter-dashboard flutter-dashboard bot added the will affect goldens Changes to golden files label May 30, 2024
Copy link
Contributor

@MitchellGoodwin MitchellGoodwin left a comment

Choose a reason for hiding this comment

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

Thank you for putting this PR together, this seems tricky. I'd like it if we could figure out that toString error. That may be a sign of something being pretty wrong.

Also, would it be possible to post a screen recording of what it looks like when you go backwards from a transparent background, back to an opaque one?

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't tend to go out of our way to hide classes that aren't explicitly private. We never know how people use things like this in their own custom implementations, and it could be useful for adaptive work. Was there a specific reason to hide this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No other reason that I wasn't sure about introducing this widget to the public API.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can let it be exposed then. It does no harm, and that way we avoid "why can't I use this Class" comments later when somebody comes up with an odd use case. 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, exporting everything is discussed a bit in our style guide. If we don't want to expose something, a private class would be more appropriate - which also helps to decide whether or not it should be exposed. If it cannot be private, then folks will likely need or want to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My bad then, I'll expose it :)

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: unneeded new line

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a problematic breaking change. It could make existing tests break.

Copy link
Contributor

Choose a reason for hiding this comment

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

This ! seems suspicious and would be my guess on what's causing the debug error. Does it work if you set a ?? fallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I tried with the fallback and it didn't work. But this is tricky I may have missed something. Will try again tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to look at the differences between #142439 (because the smoke test passed there) and this PR. And I couldn't find any explanation for why it fails here.

@veloce
Copy link
Contributor Author

veloce commented May 31, 2024

Also, would it be possible to post a screen recording of what it looks like when you go backwards from a transparent background, back to an opaque one?

Enregistrement.de.l.ecran.2024-05-31.a.14.49.18.mov

@veloce
Copy link
Contributor Author

veloce commented May 31, 2024

I still cannot explain exactly (or fix) the problem found in flutter_gallery smoke tests.

But using some debug print I think I could find which color is the culprit. This is a CupertinoDynamicColor defined in CupertinoNavigationBarBackButton:
https://github.com/veloce/flutter/blob/12c6446a9a0bfeee22f786cbc1c99c097dbbe25a/packages/flutter/lib/src/cupertino/nav_bar.dart#L1584

This is a color of a TextStyle, so it looks consistent with the stack trace of the test:

Null check operator used on a null value

When the exception was thrown, this was the stack:
#0      Element.widget (package:flutter/src/widgets/framework.dart:3478:31)
#1      CupertinoDynamicColor.toString (package:flutter/src/cupertino/colors.dart:1109:135)
#2      DiagnosticsProperty.valueToString (package:flutter/src/foundation/diagnostics.dart:2616:60)
#3      DiagnosticsProperty.toDescription (package:flutter/src/foundation/diagnostics.dart:2633:21)
#4      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1195:31)
#5      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1125:14)
#6      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1311:39)
#7      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1125:14)
#8      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1369:33)
#9      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1125:14)
#10     TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1369:33)
#11     TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1125:14)
#12     TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1369:33)
...

Now in my logs we can see the first CupertinoDynamicColor.toString which fails is due to the _PersistentNavigationBar widget:

00:22 +0: Flutter Gallery app smoke test (variant: TargetPlatform.android)
2024-05-31 16:40:50.917689 persistent nav bar element StatelessElement#0682a
back button element: StatelessElement#1ce2d
2024-05-31 16:40:51.167297 persistent nav bar element StatelessElement#14175
back button element: StatelessElement#2f4b2
2024-05-31 16:40:51.179605 persistent nav bar element StatelessElement#14175
back button element: StatelessElement#2f4b2
2024-05-31 16:40:51.397133 persistent nav bar element StatelessElement#033cf
back button element: StatelessElement#b9407
2024-05-31 16:40:51.631287 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.688865 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.715107 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.724946 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.734721 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.740004 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.745808 persistent nav bar element StatelessElement#300ad
2024-05-31 16:40:51.754271 persistent nav bar element StatelessElement#4553b
2024-05-31 16:40:51.763677 persistent nav bar element StatelessElement#4553b
2024-05-31 16:40:51.769524 persistent nav bar element StatelessElement#4553b
2024-05-31 16:40:51.774510 persistent nav bar element StatelessElement#4553b
2024-05-31 16:40:51.779014 persistent nav bar element StatelessElement#4553b
2024-05-31 16:40:51.985613: CupertinoDynamicColor.toString context StatelessElement#300ad(DEFUNCT)(no widget)
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following _TypeError was thrown running a test:
Null check operator used on a null value

Considering the timestamps, it looks like this happens during a scroll. Suddenly the framework creates another element for the same widget, and then it fails.

It don't have more time to debug this, will resume next week. But I'm really not sure how to fix this actually.

@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

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.

Changes reported for pull request #149102 at sha 12c6446a9a0bfeee22f786cbc1c99c097dbbe25a

@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from 12c6446 to 5631959 Compare June 4, 2024 08:37
@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

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.

Changes reported for pull request #149102 at sha 5631959d180bf06a5997c9fa059680117ce49174

@MitchellGoodwin
Copy link
Contributor

Going to take the time to thank you @veloce again for your work on this so far. This has been much more of a rabbit hole than expected, and I appreciate your patience.

@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from 5631959 to 6153dfc Compare June 6, 2024 08:14
@veloce
Copy link
Contributor Author

veloce commented Jun 6, 2024

Going to take the time to thank you @veloce again for your work on this so far. This has been much more of a rabbit hole than expected, and I appreciate your patience.

No problem. I also haven't expected it to be that complicated, but I understand that things have to be done properly.

@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

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.

Changes reported for pull request #149102 at sha 5f16463

@veloce
Copy link
Contributor Author

veloce commented Jun 6, 2024

Here are new demos, using a slightly modified version of flutter gallery:

Automatic background visibility

automatic_visibility_nav_bar.mov

Alway hidden background

For this one I had to increase the sigma values, otherwise the buttons in the nav may not be visible and it is less pretty. The problem is that some flickering is noticeable around the edges, due to the blur.

alway_hidden_background_nav_bar.mov

Always visible background

alway_visible_background_nav_bar.mov

child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
filter: backgroundColor.alpha == 0xFF || !isContentScrolledUnder
? ImageFilter.blur()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wrote that to show it works but we'd need an ImageFilter.noOp() or similar instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like in the examples when it's supposed to be opaque there's still a blur. Is this why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By examples you mean the new screen recordings I posted?

In that case it's normal to see blur everywhere since it did not make an example with a fully opaque background.

I can make one if needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you wouldn't mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I posted another one in the main comment section. About the ImageFilter.blur(), it works as expected as it has no visual effect. So having an ImageFilter.noOp() is to make it explicit we want a no op here.

Regarding the new demos that I pushed, is the visual result matching what you were expected as behavior?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it does, thank you. It looks great.

I think this is more than sufficient for this PR. I'd wonder if a lerp on the Blur would be the smoothest way to do it in the future. Just thinking out loud.

Copy link
Contributor

Choose a reason for hiding this comment

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

you should add an enabled flag to BackdropFilter widget like we have for ImageFilter:

https://api.flutter.dev/flutter/widgets/ImageFiltered/ImageFiltered.html

There is no such thing as a no-op filter, this will still compute a blur which can be expensive.

@veloce
Copy link
Contributor Author

veloce commented Jun 7, 2024

Here is another screen recording with an opaque background this time. The CupertinoSliverNavigationBar is configured with the default automatic visibility, and the inner page is configured with an always visible background.

cupertino_opaque_bar.mov

Copy link
Contributor

@MitchellGoodwin MitchellGoodwin left a comment

Choose a reason for hiding this comment

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

Code LGTM. I think we want to make sure the original cases are checked in the tests, and that our documentation is up to date. The sample code for CupertinoNavigationBar also probably needs to be updated with the flag turned off, and maybe a new sample with the flag turned on should be added to show the automatic transparency? Or that original sample could be setup with a commented out line to turn it on and off and see the difference.

expect(find.byType(CupertinoNavigationBar), paints..rect(color: background.darkColor));
});

testWidgets('Non-opaque background adds blur effects', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we want to make sure that non-opaque backgrounds, with the automatic transparency turned off still apply a blur. It looks like your tests cover the other cases. Since we always have a BackdropFilter, can we check BackdropFilter.filter? And see if it's not a Blur with no sigma. At worst with toString?

@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from 5f16463 to b8fe05f Compare June 12, 2024 15:27
@veloce
Copy link
Contributor Author

veloce commented Jun 12, 2024

I updated the doc comment and pushed new tests for the blur effects.

The sample was already updated (I changed the column for a ListView and increased the height of the coloured strips to be able to scroll up).

We can't keep the previous sample as is, (even with automaticBackgroundVisibility to false) because now the _wrapWithBackground function is changed to only add the blur if isContentScrolledUnder is true so it needs a ScrollView.

@MitchellGoodwin
Copy link
Contributor

The sample was already updated (I changed the column for a ListView and increased the height of the coloured strips to be able to scroll up).

I saw that. I meant the sample should be able to show both the previous behavior and this new one. Maybe just adding a line setting the flag to true and a comment saying to change it to false to either show a static opaque or blurred navbar.

We can't keep the previous sample as is, (even with automaticBackgroundVisibility to false) because now the _wrapWithBackground function is changed to only add the blur if isContentScrolledUnder is true so it needs a ScrollView.

So does that mean the original behavior wouldn't be possible unless content scrolls under? If somebody had a background image for their page and wanted the NavBar to always be blurred and partially show the image no matter if there's a scroll or not, would that be possible?

@veloce
Copy link
Contributor Author

veloce commented Jun 18, 2024

So does that mean the original behavior wouldn't be possible unless content scrolls under?

In the current state, yes.

If somebody had a background image for their page and wanted the NavBar to always be blurred and partially show the image no matter if there's a scroll or not, would that be possible?

I think that's possible if I revert the blur logic to the original behaviour, which is to blur as long as the background is not fully opaque. The only drawback that I see is that it could lead to unwanted visual effects if the scaffold background color itself is semi-transparent (I suppose it is a very edge case).

@veloce veloce force-pushed the cupertino_transparent_nav_bar branch from 08fa6bb to b906f5a Compare July 3, 2024 19:07
@flutter-dashboard
Copy link

Golden file changes are available for triage from new commit, Click here to view.

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.

Changes reported for pull request #149102 at sha b906f5a

@MitchellGoodwin MitchellGoodwin added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 3, 2024
@auto-submit auto-submit bot merged commit 6e246ac into flutter:master Jul 3, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 4, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 5, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jul 5, 2024
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Jul 7, 2024
Roll Flutter from af913a7 to fafd67d (41 revisions)

flutter/flutter@af913a7...fafd67d

2024-07-05 [email protected] Roll Flutter Engine from 74d40c160e48 to 4ee09d3b7f3b (1 revision) (flutter/flutter#151346)
2024-07-05 [email protected] Roll Flutter Engine from ba9c7b6336ef to 74d40c160e48 (1 revision) (flutter/flutter#151340)
2024-07-05 [email protected] Roll Flutter Engine from 1f0f950ea02a to ba9c7b6336ef (1 revision) (flutter/flutter#151331)
2024-07-05 [email protected] Roll Flutter Engine from 3c6a373bda3e to 1f0f950ea02a (1 revision) (flutter/flutter#151326)
2024-07-04 [email protected] Roll Flutter Engine from 79a91e38c587 to 3c6a373bda3e (2 revisions) (flutter/flutter#151318)
2024-07-04 [email protected] Roll Packages from d2705fb to 754de19 (3 revisions) (flutter/flutter#151315)
2024-07-04 [email protected] Roll Flutter Engine from 2b6bb516e7e6 to 79a91e38c587 (2 revisions) (flutter/flutter#151314)
2024-07-04 [email protected] Roll Flutter Engine from 8e2d05fa95d7 to 2b6bb516e7e6 (2 revisions) (flutter/flutter#151299)
2024-07-04 [email protected] Roll Flutter Engine from 4190543cb093 to 8e2d05fa95d7 (13 revisions) (flutter/flutter#151293)
2024-07-03 [email protected] Roll pub packages (flutter/flutter#151203)
2024-07-03 [email protected] Fix invalid URL suggestion for gradle incompatability (flutter/flutter#150999)
2024-07-03 [email protected] Cupertino transparent navigation bars (flutter/flutter#149102)
2024-07-03 [email protected] Prepares semantics_update_test for upcoming link URL change (flutter/flutter#151261)
2024-07-03 [email protected] Add a message about spam/brigading (flutter/flutter#150583)
2024-07-03 [email protected] PinnedHeaderSliver example based on the iOS Settings AppBar (flutter/flutter#151205)
2024-07-03 [email protected] Update deprecation policy (flutter/flutter#151257)
2024-07-03 [email protected] SliverFloatingHeader (flutter/flutter#151145)
2024-07-03 [email protected] Remove warning when KGP version not detected (flutter/flutter#151254)
2024-07-03 [email protected] Feat: Add withOpacity to gradient (flutter/flutter#150670)
2024-07-03 [email protected] Fix references in examples (flutter/flutter#151204)
2024-07-03 [email protected] Fix link in tree hygene doc (flutter/flutter#151235)
2024-07-03 [email protected] content dimensions are not established get controller value error (flutter/flutter#148938)
2024-07-03 [email protected] chore: fix typos and link broken (flutter/flutter#150402)
2024-07-03 [email protected] Add example of goldenFileComparator usage in widget tests (flutter/flutter#150422)
2024-07-03 [email protected] Fix project name fallback (flutter/flutter#150614)
2024-07-03 [email protected] Roll Flutter Engine from a3e61c0fd1c2 to 4190543cb093 (1 revision) (flutter/flutter#151241)
2024-07-03 [email protected] Roll Flutter Engine from 8274f54f11be to a3e61c0fd1c2 (2 revisions) (flutter/flutter#151237)
2024-07-03 [email protected] Force regeneration of platform-specific manifests before running performance tests (flutter/flutter#151003)
2024-07-03 [email protected] Handle a SocketException thrown when sending the browser close command to Chrome (flutter/flutter#151197)
2024-07-03 [email protected] Roll Flutter Engine from a02e3f673da3 to 8274f54f11be (4 revisions) (flutter/flutter#151226)
2024-07-03 [email protected] Roll Flutter Engine from c5c0c54d6d1d to a02e3f673da3 (1 revision) (flutter/flutter#151212)
2024-07-03 [email protected] Roll Flutter Engine from 44278941443e to c5c0c54d6d1d (9 revisions) (flutter/flutter#151208)
2024-07-02 [email protected] Fix scheduler event loop being stuck due to task with Priority.idle (flutter/flutter#151168)
2024-07-02 [email protected] Fix result propagation in RenderSliverEdgeInsetsPadding.hitTestChildren (flutter/flutter#149825)
2024-07-02 [email protected] docImports for flutter_test (flutter/flutter#151189)
2024-07-02 [email protected] Interactable ScrollView content when settling a scroll activity (flutter/flutter#145848)
2024-07-02 [email protected] [flutter_tools] Update the mapping for the Dart SDK internal URI (flutter/flutter#151170)
2024-07-02 [email protected] Roll pub packages (flutter/flutter#151129)
2024-07-02 [email protected] Fix typo (flutter/flutter#151192)
2024-07-02 [email protected] [tool] Fix `stdin.flush` calls on processes started by `FakeProcessManager` (flutter/flutter#151183)
2024-07-02 [email protected] Roll Flutter Engine from 433d360eee11 to 44278941443e (4 revisions) (flutter/flutter#151186)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected],[email protected] on the revert to ensure that a human
...
victorsanni pushed a commit to victorsanni/flutter that referenced this pull request Jul 8, 2024
This PR is making the `CupertinoNavigationBar` and `CupertinoSliverNavigationBar` appear transparent as long as the content is not scrolled under them, so they look like standard iOS apps nav bars.

https://github.com/flutter/flutter/assets/423393/eee2700b-2a91-4577-922c-6163d47cb357

https://github.com/flutter/flutter/assets/423393/3847f2b5-0dac-4d5e-aa6f-03c1d2893e30

<details>
  <summary>Demo app code</summary>
  
  ```dart
import 'package:flutter/cupertino.dart';

/// Flutter code sample for [CupertinoTabScaffold].

void main() => runApp(const TabScaffoldApp());

class TabScaffoldApp extends StatefulWidget {
  const TabScaffoldApp({super.key});

  @OverRide
  State<TabScaffoldApp> createState() => _TabScaffoldAppState();
}

class _TabScaffoldAppState extends State<TabScaffoldApp> {
  Brightness _brightness = Brightness.light;

  @OverRide
  Widget build(BuildContext context) {
    return CupertinoApp(
      theme: CupertinoThemeData(brightness: _brightness),
      home: TabScaffoldExample(
          brightness: _brightness, onBrightnessToggle: _toggleBrightness),
    );
  }

  void _toggleBrightness() {
    setState(() {
      _brightness =
          _brightness == Brightness.light ? Brightness.dark : Brightness.light;
    });
  }
}

class TabScaffoldExample extends StatefulWidget {
  const TabScaffoldExample(
      {required this.brightness, required this.onBrightnessToggle, super.key});

  final VoidCallback onBrightnessToggle;
  final Brightness brightness;

  @OverRide
  State<TabScaffoldExample> createState() => _TabScaffoldExampleState();
}

class _TabScaffoldExampleState extends State<TabScaffoldExample> {
  @OverRide
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search_circle_fill),
            label: 'Explore',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.person_fill),
            label: 'Profile',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.settings_solid),
            label: 'Settings',
          ),
        ],
      ),
      tabBuilder: (BuildContext context, int index) {
        return CupertinoTabView(
          builder: (BuildContext context) {
            return CupertinoPageScaffold(
              backgroundColor: index == 3
                  ? CupertinoColors.secondarySystemBackground
                      .resolveFrom(context)
                  : null,
              child: CustomScrollView(
                slivers: [
                  CupertinoSliverNavigationBar(
                    largeTitle: Text('Tab $index'),
                    initiallyTransparent: index != 2,
                    trailing: CupertinoButton(
                      padding: EdgeInsets.zero,
                      onPressed: widget.onBrightnessToggle,
                      child: Icon(
                        widget.brightness == Brightness.light
                            ? CupertinoIcons.moon_stars
                            : CupertinoIcons.sun_max,
                      ),
                    ),
                  ),
                  SliverSafeArea(
                    top: false,
                    sliver: SliverList.list(
                      children: [
                        CupertinoButton(
                          child: const Text('Next page'),
                          onPressed: () {
                            Navigator.of(context).push(
                              CupertinoPageRoute<void>(
                                builder: (BuildContext context) {
                                  return CupertinoPageScaffold(
                                    navigationBar: CupertinoNavigationBar(
                                      middle: Text('Inner page of tab $index'),
                                    ),
                                    child: ListView(
                                      children: [
                                        Center(
                                          child: CupertinoButton(
                                            child: const Text('Back'),
                                            onPressed: () {
                                              Navigator.of(context).pop();
                                            },
                                          ),
                                        ),
                                        if (index == 0) const _LongList(),
                                        const SizedBox(height: 20),
                                      ],
                                    ),
                                  );
                                },
                              ),
                            );
                          },
                        ),
                        if (index == 1) const _LongList(),
                        const SizedBox(height: 20),
                      ],
                    ),
                  ),
                ],
              ),
            );
          },
        );
      },
    );
  }
}

class _LongList extends StatelessWidget {
  const _LongList();

  @OverRide
  Widget build(BuildContext context) {
    return Column(
      children: [
        for (int i = 0; i < 50; i++) ...[
          CupertinoListTile(
            leading: const Icon(CupertinoIcons.book),
            title: Text('Bookstore item $i'),
          ),
        ],
      ],
    );
  }
}
  ```
</details>

This is the continuation of flutter#142439.

I tried to keep the simplest API possible, so it's only introducing a new `automaticBackgroundVisibility` boolean parameter. 

In the implementation I had to use the `CupertinoPageScaffold` background color to make it look transparent instead of a 0 opacity, because of issues with route transitions.

I used an `InheritedWidget` so the nav bar is always getting the right background color from the parent scaffold, whether it is overridden or not. It would probably be better to make the inherited widget private but we'd need to move all the nav bar code to the same file as the scaffold, so for now I've just hidden it from the export. Let me know if it is okay to do that.

This PR is not dealing with the bottom tab bar, because the same [straightforward approach](veloce@dde8ec6) doesn't work here. The problem is that the scroll notification is sent only when the scroll view is created or updated, so it doesn't work if one pushes a screen and navigates back.

Issues:
- flutter#78607 
- flutter#60411
@victorsanni
Copy link
Contributor

Hi @veloce, issue #78607 is linked in this PR. Does this PR fix that issue?

@veloce
Copy link
Contributor Author

veloce commented Oct 15, 2024

It addresses it partially I think. It doesn't allow to have different background colors for collapsed and exanded states, as the linked issue title suggests. But the main use case (transparent expanded background) is covered. @victorsanni

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

Labels

autosubmit Merge PR when tree becomes green via auto submit App d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: cupertino flutter/packages/flutter/cupertino repository framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants