[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)),
+ ],
+ ),
+ );
+ }
+}