flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
162.27k stars 26.66k forks source link

[Bug] AppBar Material 3 does not animate correctly #131042

Open md-weber opened 10 months ago

md-weber commented 10 months ago

Is there an existing issue for this?

Steps to reproduce

  1. Create a new Flutter app
  2. Add a ListView with a lot of Items (enable Scrolling)
  3. Scroll the List

Expected results

Material 3 AppBar should animate to a different color

Actual results

Material 3 AppBar changes color without animation

Code sample

Code sample ```dart // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { final String title; const MyHomePage({ Key? key, required this.title, }) : super(key: key); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: ListView.separated( itemBuilder: ((context, index) { return const ListTile( title: Text("Test"), subtitle: Text(""), ); }), separatorBuilder: ((context, index) => const Divider()), itemCount: 250, ), ); } } ```

Screenshots or Video

Screenshots / Video demonstration [[Material 3 Official Documentation]https://m3.material.io/components/top-app-bar/guidelines#4eab4f50-4a3e-4189-bce2-a46514cde1da](https://m3.material.io/components/top-app-bar/guidelines#4eab4f50-4a3e-4189-bce2-a46514cde1da) Scrolling without animation on an iOS Simulator https://github.com/flutter/flutter/assets/8026644/a6f8683f-7c28-45a4-bbe0-d3250de9b171

Logs

No Logs

Flutter Doctor output

Doctor output ```console Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.10.6, on macOS 13.4.1 22F770820d darwin-arm64, locale en-DE) [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2) [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2022.2) [✓] Android Studio (version 2022.2) [✓] IntelliJ IDEA Ultimate Edition (version 2023.1.4) [✓] IntelliJ IDEA Ultimate Edition (version 2023.1.3) [✓] IntelliJ IDEA Ultimate Edition (version 2023.1.4) [✓] VS Code (version 1.80.1) [✓] Connected device (3 available) [✓] Network resources ```
dam-ease commented 10 months ago

Hi @md-weber. Thanks for filing this issue. I am able to reproduce this on the latest master and stable channels. The appBar color changes/ appears & disappears without animation as shown in the documentation.

Code Sample

```dart import 'package:flutter/material.dart'; /// Flutter code sample for [AppBar]. final List _items = List.generate(51, (int index) => index); void main() => runApp(const AppBarApp()); class AppBarApp extends StatelessWidget { const AppBarApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true, ), home: const AppBarExample(), ); } } class AppBarExample extends StatefulWidget { const AppBarExample({super.key}); @override State createState() => _AppBarExampleState(); } class _AppBarExampleState extends State { bool shadowColor = false; double? scrolledUnderElevation; @override Widget build(BuildContext context) { final ColorScheme colorScheme = Theme.of(context).colorScheme; final Color oddItemColor = colorScheme.primary.withOpacity(0.05); final Color evenItemColor = colorScheme.primary.withOpacity(0.15); return Scaffold( appBar: AppBar( title: const Text('AppBar Demo'), scrolledUnderElevation: scrolledUnderElevation, shadowColor: shadowColor ? Theme.of(context).colorScheme.shadow : null, ), body: GridView.builder( itemCount: _items.length, padding: const EdgeInsets.all(8.0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 2.0, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, ), itemBuilder: (BuildContext context, int index) { if (index == 0) { return Center( child: Text( 'Scroll to see the Appbar in effect.', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.center, ), ); } return Container( alignment: Alignment.center, // tileColor: _items[index].isOdd ? oddItemColor : evenItemColor, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.0), color: _items[index].isOdd ? oddItemColor : evenItemColor, ), child: Text('Item $index'), ); }, ), bottomNavigationBar: BottomAppBar( child: Padding( padding: const EdgeInsets.all(8), child: OverflowBar( overflowAlignment: OverflowBarAlignment.center, alignment: MainAxisAlignment.center, overflowSpacing: 5.0, children: [ ElevatedButton.icon( onPressed: () { setState(() { shadowColor = !shadowColor; }); }, icon: Icon( shadowColor ? Icons.visibility_off : Icons.visibility, ), label: const Text('shadow color'), ), const SizedBox(width: 5), ElevatedButton( onPressed: () { if (scrolledUnderElevation == null) { setState(() { // Default elevation is 3.0, increment by 1.0. scrolledUnderElevation = 4.0; }); } else { setState(() { scrolledUnderElevation = scrolledUnderElevation! + 1.0; }); } }, child: Text( 'scrolledUnderElevation: ${scrolledUnderElevation ?? 'default'}', ), ), ], ), ), ), ); } } ```

Video

https://github.com/flutter/flutter/assets/53122008/87710ccb-9be2-487d-b25f-264499195e0e

flutter doctor -v

``` [✓] Flutter (Channel stable, 3.10.6, on macOS 13.0 22A380 darwin-arm64, locale en-NG) • Flutter version 3.10.6 on channel stable at /Users/damilolaalimi/sdks/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision f468f3366c (9 days ago), 2023-07-12 15:19:05 -0700 • Engine revision cdbeda788a • Dart version 3.0.6 • DevTools version 2.23.1 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/damilolaalimi/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14E300c • CocoaPods version 1.12.1 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2022.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) [✓] VS Code (version 1.80.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.50.0 [✓] Connected device (4 available) • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 14 (API 34) (emulator) • iPhone 14 (mobile) • 1A122DE2-0CAB-4C3E-A395-691BF27D626F • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-4 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 13.0 22A380 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 114.0.5735.198 [✓] Network resources • All expected network resources are available. • No issues found! ``` ``` [✓] Flutter (Channel master, 3.13.0-7.0.pre.59, on macOS 13.0 22A380 darwin-arm64, locale en-NG) • Flutter version 3.13.0-7.0.pre.59 on channel master at /Users/damilolaalimi/fvm/versions/master • Upstream repository https://github.com/flutter/flutter.git • Framework revision f629809938 (4 hours ago), 2023-07-21 02:09:23 -0400 • Engine revision 264685f0ae • Dart version 3.1.0 (build 3.1.0-333.0.dev) • DevTools version 2.25.0 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/damilolaalimi/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 14.3.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 14E300c • CocoaPods version 1.12.1 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2022.2) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694) [✓] VS Code (version 1.80.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.50.0 [✓] Connected device (4 available) • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 14 (API 34) (emulator) • iPhone 14 (mobile) • 1A122DE2-0CAB-4C3E-A395-691BF27D626F • ios • com.apple.CoreSimulator.SimRuntime.iOS-16-4 (simulator) • macOS (desktop) • macos • darwin-arm64 • macOS 13.0 22A380 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 114.0.5735.198 [✓] Network resources • All expected network resources are available. • No issues found! ```

hkarmoush commented 10 months ago

I was able to reproduce and solve by passing a scrollController to the GridView and having a listener to update the value of scrolledUnderElevation

The issue was scrolledUnderElevation is either being used as a minimum of zero and a maximum of 3 by the AppBar widget, listening to the amount of scroll seems to do the job right.

import 'package:flutter/material.dart';

/// Flutter code sample for [AppBar].

final List<int> _items = List<int>.generate(51, (int index) => index);

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorSchemeSeed: const Color(0xff6750a4),
        useMaterial3: true,
      ),
      home: const AppBarExample(),
    );
  }
}

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

  @override
  State<AppBarExample> createState() => _AppBarExampleState();
}

class _AppBarExampleState extends State<AppBarExample> {
  ScrollController scrollController = ScrollController();
  bool shadowColor = false;
  double? scrolledUnderElevation;

  @override
  void initState() {
    super.initState();
    scrollController.addListener(_updateScrolledUnderElevation);
  }

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

  void _updateScrolledUnderElevation() {
    setState(() {
      // Calculate the amount scrolled under the AppBar based on the scroll offset.
      scrolledUnderElevation = scrollController.offset;
    });
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
    return Scaffold(
      appBar: AppBar(
        title: const Text('AppBar Demo'),
        scrolledUnderElevation: scrolledUnderElevation,
        shadowColor: shadowColor ? Theme.of(context).colorScheme.shadow : null,
      ),
      body: GridView.builder(
        controller: scrollController,
        itemCount: _items.length,
        padding: const EdgeInsets.all(8.0),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          childAspectRatio: 2.0,
          mainAxisSpacing: 10.0,
          crossAxisSpacing: 10.0,
        ),
        itemBuilder: (BuildContext context, int index) {
          if (index == 0) {
            return Center(
              child: Text(
                'Scroll to see the Appbar in effect.',
                style: Theme.of(context).textTheme.labelLarge,
                textAlign: TextAlign.center,
              ),
            );
          }
          return Container(
            alignment: Alignment.center,
            // tileColor: _items[index].isOdd ? oddItemColor : evenItemColor,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(20.0),
              color: _items[index].isOdd ? oddItemColor : evenItemColor,
            ),
            child: Text('Item $index'),
          );
        },
      ),
      bottomNavigationBar: BottomAppBar(
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: OverflowBar(
            overflowAlignment: OverflowBarAlignment.center,
            alignment: MainAxisAlignment.center,
            overflowSpacing: 5.0,
            children: <Widget>[
              ElevatedButton.icon(
                onPressed: () {
                  setState(() {
                    shadowColor = !shadowColor;
                  });
                },
                icon: Icon(
                  shadowColor ? Icons.visibility_off : Icons.visibility,
                ),
                label: const Text('shadow color'),
              ),
              const SizedBox(width: 5),
              SafeArea(
                bottom: true,
                child: ElevatedButton(
                  onPressed: () {
                    print('pressed');
                    if (scrolledUnderElevation == null) {
                      setState(() {
                        // Default elevation is 3.0, increment by 1.0.
                        scrolledUnderElevation = 4.0;
                      });
                    } else {
                      setState(() {
                        scrolledUnderElevation = scrolledUnderElevation! + 1.0;
                      });
                    }
                  },
                  child: Text(
                    'scrolledUnderElevation: ${scrolledUnderElevation ?? 'default'}',
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

https://github.com/flutter/flutter/assets/24737691/d5e26a88-61d6-4673-8d41-267443abe33e

MaxYablochkin commented 9 months ago

Similar issue #115568

rydmike commented 8 months ago

Glad to see Max @md-weber filed this issue. I have had it on my list of things I've been too lazy file for a few months. Well mostly I did not file it, because I have "secret" fix-workaround for it, gotta keep some goodies, haha 😄 Anyway, I'm about to reveal it now.

And @MaxYablochkin your early report of this issue from Nov 2022 is the same issue and first to mention it. You should get the creds for finding it. It was treated as a new feature request back then, but it is in fact a Material-3 design that it should animate. So a spec deviation bug, and indeed, the abrupt change does not look very good at all.

If anybody is interesting, the tint elevation in Material does contain, as far as I can see, a reasonably correct animation between the M3 elevation tint color values, when Materials elevation changes, it is all there in the Material code.

So when Material is used by AppBar and we have different non-scrolled-under elevation and scrolled-under elevation values, that it already uses and changes, the color change due to elevation change should already animate, right? Yes, but... as we can all see, it does not do so 🤔

Why is that? I missed it a first too and looked long and hard at the nifty animation code, which of course was the wrong place.

It does not animate because Material optimizes out this animation if Material does not use shape (is null) or has borderRadius that is null, since then there should be nothing to animate, or so this optimization thinks. Before animation of Material was only used if it had a shape or radius value. This optimization wisely removes a chunk of a bit expensive code on Material when it is typically not needed, this was fine in M2.

This optimization is here:

https://github.com/flutter/flutter/blob/382ceb570703c81f5e8ced5d96fec7d2595cef0a/packages/flutter/lib/src/material/material.dart#L500

When AppBar is built, and uses Material, it does not use the borderRadius property, but it does use shape here:

https://github.com/flutter/flutter/blob/382ceb570703c81f5e8ced5d96fec7d2595cef0a/packages/flutter/lib/src/material/app_bar.dart#L1131

This means there is a simple a workaround you can do to make the AppBar animate correctly.

The flex_color_scheme package uses it as a "hidden" bonus feature and you can see it in action in the Themes Playground on its AppBar, it can also be seen in a X/Tweet here

It is a simple one-liner workaround, just give the AppBar or AppBarTheme a RoundedRectangleBorder() and Material will not optimize out the animation anymore and M3 AppBar now animates the color change when its elevation changes, thus also when you "scroll-under" 🥳

From FlexColorScheme's AppBarTheme:

AppBarTheme(
      // --- 8< --- Snip --- 8< 
      // TODO(rydmike): This is a workaround to make tint elevation animate.
      // TODO(rydmike): Create issue for none animated AppBar elevation tint.
      shape: const RoundedRectangleBorder(),
    );

The defaults for shape in M3 AppBar is null, so to get a shape we have to give it one. The const RoundedRectangleBorder() is still a straight corner zero-radius shape so everything will still look the same, and if we add it to our ThemeData.appBarTheme.shape we get the tint elevation animation workaround fix on all AppBars in our app.

Well, I guess I can remove my own TODO above to make an issue, since it already exists 😄

By the way, the above example with scroll offset is cool and gives a similar result, but it has a slightly different result. There the animation is driven by how fast you scroll/move up or down, which of course is a cool effect. However, based on looking at the Compose demo of an M3 AppBar, it does not behave like that, it animates independently at fixed duration as soon as it is scrolled under even a tiny bit.


@HansMuller & @TahaTesser, this info might help anybody that wants to fix this issue.

Skyost commented 3 weeks ago

@rydmike Your workaround works great, but it really is something that should be done by default.

rydmike commented 3 weeks ago

@Skyost it is indeed a bug that should be fixed.

I have not checked what the status is on the latest beta 3.22 yet, that will land as the new stable soon (probably May 14). There are a lot of changes to the elevation tint for a lot of widgets.

Most new elevated widgets just use one of the new surface colors as fixed color for elevated state, and elevation tint color is transparent so they don't get anymore tint from elevation.

I did not yet look at how it all affects this scroll under elevation tint in AppBar, might not affect this issue, since the animation is discarded in a lower used Material widget as a part of its optimization. The optimization in question worked well on Material2 but not in Material3, which is why we now have this issue.

Skyost commented 3 weeks ago

@rydmike Apologizes, I'm on stable and therefore, was not aware of these changes. Looking forward this new release 😁

rydmike commented 3 weeks ago

@Skyost as mentioned, I have not checked either how the ColorScheme and elevation changes on master and beta have impacted AppBar, but I will soonish.

I will drop an update on the status of this issue when I do. I doubt it impacts the animation of the scroll under color, it probably only slightly changes the color of the resulting scroll under color, but change to it is probably still instant and not animated as it should be.