[CP] FlexibleSpaceBar background fix (#128288)

Cherry pick for Issue https://github.com/flutter/flutter/issues/127836 / PR https://github.com/flutter/flutter/pull/128273

CP Issue: https://github.com/flutter/flutter/issues/128257
diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart
index 09f98a0..bd10ea6 100644
--- a/packages/flutter/lib/src/material/flexible_space_bar.dart
+++ b/packages/flutter/lib/src/material/flexible_space_bar.dart
@@ -6,6 +6,7 @@
 import 'dart:ui' as ui;
 
 import 'package:flutter/foundation.dart' show clampDouble;
+import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
 
 import 'colors.dart';
@@ -245,12 +246,13 @@
             constraints.maxHeight > height) {
             height = constraints.maxHeight;
           }
+          final double topPadding = _getCollapsePadding(t, settings);
           children.add(Positioned(
-            top: _getCollapsePadding(t, settings),
+            top: topPadding,
             left: 0.0,
             right: 0.0,
             height: height,
-            child: Opacity(
+            child: _FlexibleSpaceHeaderOpacity(
               // IOS is relying on this semantics node to correctly traverse
               // through the app bar when it is collapsed.
               alwaysIncludeSemantics: true,
@@ -420,3 +422,33 @@
         || isScrolledUnder != oldWidget.isScrolledUnder;
   }
 }
+
+// We need the child widget to repaint, however both the opacity
+// and potentially `widget.background` can be constant which won't
+// lead to repainting.
+// see: https://github.com/flutter/flutter/issues/127836
+class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget {
+  const _FlexibleSpaceHeaderOpacity({required this.opacity, required super.child, required this.alwaysIncludeSemantics});
+
+  final double opacity;
+  final bool alwaysIncludeSemantics;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderFlexibleSpaceHeaderOpacity(opacity: opacity, alwaysIncludeSemantics: alwaysIncludeSemantics);
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, covariant _RenderFlexibleSpaceHeaderOpacity renderObject) {
+    renderObject
+      ..alwaysIncludeSemantics = alwaysIncludeSemantics
+      ..opacity = opacity;
+  }
+}
+
+class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity {
+  _RenderFlexibleSpaceHeaderOpacity({super.opacity, super.alwaysIncludeSemantics});
+
+  @override
+  bool get isRepaintBoundary => false;
+}
diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart
index f869f23..0826d97 100644
--- a/packages/flutter/test/material/flexible_space_bar_test.dart
+++ b/packages/flutter/test/material/flexible_space_bar_test.dart
@@ -8,6 +8,7 @@
 library;
 
 import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import '../widgets/semantics_tester.dart';
@@ -159,7 +160,11 @@
       ),
     );
 
-    final Opacity backgroundOpacity = tester.firstWidget(find.byType(Opacity));
+    final dynamic backgroundOpacity = tester.firstWidget(
+      find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_FlexibleSpaceHeaderOpacity')
+    );
+    // accessing private type member.
+    // ignore: avoid_dynamic_calls
     expect(backgroundOpacity.opacity, 1.0);
   });
 
@@ -790,6 +795,22 @@
     await tester.pumpWidget(buildFrame(TargetPlatform.linux, true));
     expect(getTitleBottomLeft(), const Offset(390.0, 0.0));
   });
+
+  testWidgets('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async {
+    await tester.pumpWidget(const MaterialApp(
+      home: SubCategoryScreenView(),
+    ));
+
+    expect(RenderRebuildTracker.count, 1);
+
+    // We drag up to fully collapse the space bar.
+    for (int i = 0; i < 20; i++) {
+      await tester.drag(find.byKey(SubCategoryScreenView.scrollKey), const Offset(0, -50.0));
+      await tester.pumpAndSettle();
+    }
+
+    expect(RenderRebuildTracker.count, greaterThan(1));
+  });
 }
 
 class TestDelegate extends SliverPersistentHeaderDelegate {
@@ -814,3 +835,83 @@
   @override
   bool shouldRebuild(TestDelegate oldDelegate) => false;
 }
+
+class RebuildTracker extends SingleChildRenderObjectWidget {
+  const RebuildTracker({super.key});
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return RenderRebuildTracker();
+  }
+}
+
+class RenderRebuildTracker extends RenderProxyBox {
+  static int count = 0;
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    count++;
+    super.paint(context, offset);
+  }
+}
+
+class SubCategoryScreenView extends StatefulWidget {
+  const SubCategoryScreenView({
+    super.key,
+  });
+
+  static const Key scrollKey = Key('orange box');
+
+  @override
+  State<SubCategoryScreenView> createState() => _SubCategoryScreenViewState();
+}
+
+class _SubCategoryScreenViewState extends State<SubCategoryScreenView>
+    with TickerProviderStateMixin {
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        centerTitle: true,
+        title: const Text('Test'),
+      ),
+      body: CustomScrollView(
+        key: SubCategoryScreenView.scrollKey,
+        slivers: <Widget>[
+          SliverAppBar(
+            leading: const SizedBox(),
+            expandedHeight: MediaQuery.of(context).size.width / 1.7,
+            collapsedHeight: 0,
+            toolbarHeight: 0,
+            titleSpacing: 0,
+            leadingWidth: 0,
+            flexibleSpace: const FlexibleSpaceBar(
+              background: AspectRatio(
+                aspectRatio: 1.7,
+                child: RebuildTracker(),
+              ),
+            ),
+          ),
+          const SliverToBoxAdapter(child: SizedBox(height: 12)),
+          SliverToBoxAdapter(
+            child: GridView.builder(
+              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+                crossAxisCount: 3,
+              ),
+              shrinkWrap: true,
+              physics: const NeverScrollableScrollPhysics(),
+              itemCount: 300,
+              itemBuilder: (BuildContext context, int index) {
+                return Card(
+                  color: Colors.amber,
+                  child: Center(child: Text('$index')),
+                );
+              },
+            ),
+          ),
+          const SliverToBoxAdapter(child: SizedBox(height: 12)),
+        ],
+      ),
+    );
+  }
+}