flutter icon indicating copy to clipboard operation
flutter copied to clipboard

FadeForwardsPageTransitionsBuilder flickers for AppBars with dark background

Open olischne opened this issue 1 month ago • 3 comments

Steps to reproduce

Using FadeForwardsPageTransitionsBuilder (e. g. in Android or by setting it by hand like in the example).

  1. Create two screens each containing a Scaffold with an AppBar with a dark background.
  2. Navigate from the first screen to the second and back.

Expected results

The AppBar should keep its dark color while navigating.

Actual results

The AppBar flickers to a bright color.

Code sample

Code sample
import 'package:flutter/material.dart';

/// Based on https://api.flutter.dev/flutter/material/FadeForwardsPageTransitionsBuilder-class.html
/// changed AppBar backgroundColor to black and removed unnecessary parts

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        pageTransitionsTheme: PageTransitionsTheme(
          builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
            TargetPlatform.values,
            value: (_) => const FadeForwardsPageTransitionsBuilder(),
          ),
        ),
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        foregroundColor: Colors.white,
        backgroundColor: Colors.black,
      ),
      body: Center(
        child: FilledButton(
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()));
          },
          child: Text("Navigate"),
        ),
      ),
    );
  }
}


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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        foregroundColor: Colors.white,
        backgroundColor: Colors.black,
      ),
      body: Center(child: Text("Second Screen")),
    );
  }
}

Screenshots or Video

Video demonstration

https://github.com/user-attachments/assets/f72b8fa9-d9cc-4612-b135-dc926ba894f7

Logs

No response

Flutter Doctor output

Also reproducible on DartPad

Doctor output
[✓] Flutter (Channel stable, 3.38.4, on macOS 26.1 25B78 darwin-arm64, locale en-DE) [463ms]
    • Flutter version 3.38.4 on channel stable at /Users/olischne/fvm/versions/stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 66dd93f9a2 (5 days ago), 2025-12-03 14:56:10 -0800
    • Engine revision a5cb96369e
    • Dart version 3.10.3
    • DevTools version 2.51.1
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file,
      enable-lldb-debugging

[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [1,663ms]
    • Android SDK at /Users/olischne/Library/Android/sdk
    • Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
    • Platform android-36, build-tools 36.1.0
    • Java binary at: /Users/olischne/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.0.1) [1,391ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17A400
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [6ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Connected device (4 available) [9.3s]
    • sdk gphone64 arm64 (mobile) • emulator-5554                        • android-arm64  • Android 16 (API 36) (emulator)
    • iPhone 17 Pro (mobile)      • 87B49BC9-D1F0-4E16-8744-53847B35AA7D • ios            • com.apple.CoreSimulator.SimRuntime.iOS-26-0 (simulator)
    • macOS (desktop)             • macos                                • darwin-arm64   • macOS 26.1 25B78 darwin-arm64
    • Chrome (web)                • chrome                               • web-javascript • Google Chrome 143.0.7499.40

[✓] Network resources [1,751ms]
    • All expected network resources are available.

• No issues found!

olischne avatar Dec 08 '25 17:12 olischne

cc @MitchellGoodwin @QuncCccccc

victorsanni avatar Dec 08 '25 17:12 victorsanni

Thanks for the report. Seeing the same behavior on latest stable and master versions.

stable: 3.38.4
master: 3.40.0-1.0.pre-55

darshankawar avatar Dec 09 '25 08:12 darshankawar

This happens because a colored box is put behind the pages as they are sliding in. The color matches the surface color of the theme by default, but you can set it manually. In this case because the header is practically the opposite color from the background color of the content, it triggers this flash. You can get somewhat of a workaround by setting the background color of the transition to transparent, which keeps the flash from happening, but it causes the black from behind the app itself to show through a little during the transition, which is not preferable.

builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
  TargetPlatform.values,
    value: (_) => const FadeForwardsPageTransitionsBuilder(backgroundColor: Colors.transparent),
  ),

https://github.com/user-attachments/assets/7f757cd5-ae78-486d-9b4d-8a9fcb5a1344

MitchellGoodwin avatar Dec 09 '25 19:12 MitchellGoodwin

Same thing with ZoomPageTransitionsBuilder:

  // A transition builder that takes into account the snapshotting properties of
  // ZoomPageTransitionsBuilder.
  static Widget _snapshotAwareDelegatedTransition(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget? child,
    bool allowSnapshotting,
    bool allowEnterRouteSnapshotting,
    Color? backgroundColor,
  ) {
    final Color enterTransitionBackgroundColor =
        backgroundColor ?? Theme.of(context).colorScheme.surface;    /// << HERE
    return DualTransitionBuilder(
      animation: ReverseAnimation(secondaryAnimation),
      forwardBuilder: (BuildContext context, Animation<double> animation, Widget? child) {
        return _ZoomEnterTransition(
          animation: animation,
          allowSnapshotting: allowSnapshotting && allowEnterRouteSnapshotting,
          reverse: true,
          backgroundColor: enterTransitionBackgroundColor,
          child: child,
        );
      },
      reverseBuilder: (BuildContext context, Animation<double> animation, Widget? child) {
        return _ZoomExitTransition(
          animation: animation,
          allowSnapshotting: allowSnapshotting,
          child: child,
        );
      },
      child: child,
    );
  }

Fortunately, setting backgroundColor: Colors.transparent fixes the problem. But when using PredictiveBackFullscreenPageTransitionsBuilder, it can fall back to the const ZoomPageTransitionsBuilder(), where we can't set backgroundColor. The same thing happens when PredictiveBackPageTransitionsBuilder falls back to const FadeForwardsPageTransitionsBuilder().

darkstarx avatar Dec 11 '25 20:12 darkstarx