Unverified Commit 541fdd60 authored by Tae Hyung Kim's avatar Tae Hyung Kim Committed by GitHub

DecoratedSliver (#127823)

This is a second attempt to merge #107269. Currently I've fixed two of the issues:
1. Fixed horizontal scrollview by using a switch statement to consider vertical/horizontal case.
2. Fixed issue of `paintExtent` not being the right extent for painting. Rather using a `scrollExtent` for the main axis length of the decoration box and painting it offsetted by the `scrollOffset`.
3. If the sliver child has inifinite scrollExtent, then we only draw the decoration down to the bottom of the `cacheExtent`. The developer is expected to ensure that the border does not creep up above the cache area.

This PR includes a test that checks that the correct rectangle is drawn at a certain scrollOffset for both the horizontal and vertical case which should be sufficient for checking that `SliverDecoration` works properly now.

Fixes https://github.com/flutter/flutter/issues/107498.
parent 08561958
// Copyright 2014 The Flutter Authors. 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(const SliverDecorationExampleApp());
class SliverDecorationExampleApp extends StatelessWidget {
const SliverDecorationExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('SliverDecoration Sample')),
body: const SliverDecorationExample(),
),
);
}
}
class SliverDecorationExample extends StatelessWidget {
const SliverDecorationExample({super.key});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(-0.5, -0.6),
radius: 0.15,
colors: <Color>[
Color(0xFFEEEEEE),
Color(0xFF111133),
],
stops: <double>[0.9, 1.0],
),
),
sliver: SliverList(
delegate: SliverChildListDelegate(<Widget>[
const Text('Goodnight Moon'),
]),
),
),
const DecoratedSliver(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.all(Radius.circular(50))
),
sliver: SliverToBoxAdapter(child: SizedBox(height: 300)),
),
],
);
}
}
......@@ -37,6 +37,7 @@ export 'src/rendering/custom_layout.dart';
export 'src/rendering/custom_paint.dart';
export 'src/rendering/debug.dart';
export 'src/rendering/debug_overflow_indicator.dart';
export 'src/rendering/decorated_sliver.dart';
export 'src/rendering/editable.dart';
export 'src/rendering/error.dart';
export 'src/rendering/flex.dart';
......
......@@ -68,6 +68,8 @@ import 'image_provider.dart';
///
/// * [DecoratedBox] and [Container], widgets that can be configured with
/// [BoxDecoration] objects.
/// * [DecoratedSliver], a widget that can be configured with a [BoxDecoration]
/// that is converted to render with slivers.
/// * [CustomPaint], a widget that lets you draw arbitrary graphics.
/// * [Decoration], the base class which lets you define other decorations.
class BoxDecoration extends Decoration {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'object.dart';
import 'proxy_box.dart';
import 'proxy_sliver.dart';
import 'sliver.dart';
/// Paints a [Decoration] either before or after its child paints.
///
/// If the child has infinite scroll extent, then the [Decoration] paints itself up to the
/// bottom cache extent.
class RenderDecoratedSliver extends RenderProxySliver {
/// Creates a decorated sliver.
///
/// The [decoration], [position], and [configuration] arguments must not be
/// null. By default the decoration paints behind the child.
///
/// The [ImageConfiguration] will be passed to the decoration (with the size
/// filled in) to let it resolve images.
RenderDecoratedSliver({
required Decoration decoration,
DecorationPosition position = DecorationPosition.background,
ImageConfiguration configuration = ImageConfiguration.empty,
}) : _decoration = decoration,
_position = position,
_configuration = configuration;
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
Decoration get decoration => _decoration;
Decoration _decoration;
set decoration(Decoration value) {
if (value == decoration) {
return;
}
_decoration = value;
_painter?.dispose();
_painter = decoration.createBoxPainter(markNeedsPaint);
markNeedsPaint();
}
/// Whether to paint the box decoration behind or in front of the child.
DecorationPosition get position => _position;
DecorationPosition _position;
set position(DecorationPosition value) {
if (value == position) {
return;
}
_position = value;
markNeedsPaint();
}
/// The settings to pass to the decoration when painting, so that it can
/// resolve images appropriately. See [ImageProvider.resolve] and
/// [BoxPainter.paint].
///
/// The [ImageConfiguration.textDirection] field is also used by
/// direction-sensitive [Decoration]s for painting and hit-testing.
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
if (value == configuration) {
return;
}
_configuration = value;
markNeedsPaint();
}
BoxPainter? _painter;
@override
void attach(covariant PipelineOwner owner) {
_painter = decoration.createBoxPainter(markNeedsPaint);
super.attach(owner);
}
@override
void detach() {
_painter?.dispose();
_painter = null;
super.detach();
}
@override
void dispose() {
_painter?.dispose();
_painter = null;
super.dispose();
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && child!.geometry!.visible) {
final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
final Size childSize;
final Offset scrollOffset;
// In the case where the child sliver has infinite scroll extent, the decoration
// should only extend down to the bottom cache extent.
final double cappedMainAxisExtent = child!.geometry!.scrollExtent.isInfinite
? constraints.scrollOffset + child!.geometry!.cacheExtent + constraints.cacheOrigin
: child!.geometry!.scrollExtent;
switch (constraints.axis) {
case Axis.vertical:
childSize = Size(constraints.crossAxisExtent, cappedMainAxisExtent);
scrollOffset = Offset(0.0, -constraints.scrollOffset);
case Axis.horizontal:
childSize = Size(cappedMainAxisExtent, constraints.crossAxisExtent);
scrollOffset = Offset(-constraints.scrollOffset, 0.0);
}
final Offset childOffset = offset + childParentData.paintOffset;
if (position == DecorationPosition.background) {
_painter!.paint(context.canvas, childOffset + scrollOffset, configuration.copyWith(size: childSize));
}
context.paintChild(child!, childOffset);
if (position == DecorationPosition.foreground) {
_painter!.paint(context.canvas, childOffset + scrollOffset, configuration.copyWith(size: childSize));
}
}
}
}
......@@ -52,6 +52,7 @@ import 'image.dart';
/// * [Decoration], which you can extend to provide other effects with
/// [DecoratedBox].
/// * [CustomPaint], another way to draw custom effects from the widget layer.
/// * [DecoratedSliver], which applies a [Decoration] to a sliver.
class DecoratedBox extends SingleChildRenderObjectWidget {
/// Creates a widget that paints a [Decoration].
///
......
// Copyright 2014 The Flutter Authors. 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/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'image.dart';
/// A sliver widget that paints a [Decoration] either before or after its child
/// paints.
///
/// Unlike [DecoratedBox], this widget expects its child to be a sliver, and
/// must be placed in a widget that expects a sliver.
///
/// If the child sliver has infinite [SliverGeometry.scrollExtent], then we only
/// draw the decoration down to the bottom [SliverGeometry.cacheExtent], and
/// it is necessary to ensure that the bottom border does not creep
/// above the top of the bottom cache. This can happen if the bottom has a
/// border radius larger than the extent of the cache area.
///
/// Commonly used with [BoxDecoration].
///
/// The [child] is not clipped. To clip a child to the shape of a particular
/// [ShapeDecoration], consider using a [ClipPath] widget.
///
/// {@tool dartpad}
/// This sample shows a radial gradient that draws a moon on a night sky:
///
/// ** See code in examples/api/lib/widgets/sliver/decorated_sliver.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [DecoratedBox], the version of this class that works with RenderBox widgets.
/// * [Decoration], which you can extend to provide other effects with
/// [DecoratedSliver].
/// * [CustomPaint], another way to draw custom effects from the widget layer.
class DecoratedSliver extends SingleChildRenderObjectWidget {
/// Creates a widget that paints a [Decoration].
///
/// The [decoration] and [position] arguments must not be null. By default the
/// decoration paints behind the child.
const DecoratedSliver({
super.key,
required this.decoration,
this.position = DecorationPosition.background,
Widget? sliver,
}) : super(child: sliver);
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
final Decoration decoration;
/// Whether to paint the box decoration behind or in front of the child.
final DecorationPosition position;
@override
RenderDecoratedSliver createRenderObject(BuildContext context) {
return RenderDecoratedSliver(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderDecoratedSliver renderObject) {
renderObject
..decoration = decoration
..position = position
..configuration = createLocalImageConfiguration(context);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final String label;
switch (position) {
case DecorationPosition.background:
label = 'bg';
case DecorationPosition.foreground:
label = 'fg';
}
properties.add(EnumProperty<DecorationPosition>('position', position, level: DiagnosticLevel.hidden));
properties.add(DiagnosticsProperty<Decoration>(label, decoration));
}
}
......@@ -38,6 +38,7 @@ export 'src/widgets/container.dart';
export 'src/widgets/context_menu_button_item.dart';
export 'src/widgets/context_menu_controller.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/decorated_sliver.dart';
export 'src/widgets/default_selection_style.dart';
export 'src/widgets/default_text_editing_shortcuts.dart';
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
testWidgets('DecoratedSliver creates, paints, and disposes BoxPainter', (WidgetTester tester) async {
final TestDecoration decoration = TestDecoration();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: decoration,
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 100, height: 100),
),
)
],
)
)
));
expect(decoration.painters, hasLength(1));
expect(decoration.painters.last.lastConfiguration!.size, const Size(800, 100));
expect(decoration.painters.last.lastOffset, Offset.zero);
expect(decoration.painters.last.disposed, false);
await tester.pumpWidget(const SizedBox());
expect(decoration.painters, hasLength(1));
expect(decoration.painters.last.disposed, true);
});
testWidgets('DecoratedSliver can update box painter', (WidgetTester tester) async {
final TestDecoration decorationA = TestDecoration();
final TestDecoration decorationB = TestDecoration();
Decoration activateDecoration = decorationA;
late StateSetter localSetState;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
localSetState = setState;
return CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: activateDecoration,
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 100, height: 100),
),
)
],
);
},
)
)
));
expect(decorationA.painters, hasLength(1));
expect(decorationA.painters.last.paintCount, 1);
expect(decorationB.painters, hasLength(0));
localSetState(() {
activateDecoration = decorationB;
});
await tester.pump();
expect(decorationA.painters, hasLength(1));
expect(decorationB.painters, hasLength(1));
expect(decorationB.painters.last.paintCount, 1);
});
testWidgets('DecoratedSliver can update DecorationPosition', (WidgetTester tester) async {
final TestDecoration decoration = TestDecoration();
DecorationPosition activePosition = DecorationPosition.foreground;
late StateSetter localSetState;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
localSetState = setState;
return CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: decoration,
position: activePosition,
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 100, height: 100),
),
)
],
);
},
)
)
));
expect(decoration.painters, hasLength(1));
expect(decoration.painters.last.paintCount, 1);
localSetState(() {
activePosition = DecorationPosition.background;
});
await tester.pump();
expect(decoration.painters, hasLength(1));
expect(decoration.painters.last.paintCount, 2);
});
testWidgets('DecoratedSliver golden test', (WidgetTester tester) async {
const BoxDecoration decoration = BoxDecoration(
color: Colors.blue,
);
final Key backgroundKey = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: RepaintBoundary(
key: backgroundKey,
child: CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: decoration,
sliver: SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(<Widget>[
Container(
height: 100,
color: Colors.red,
),
Container(
height: 100,
color: Colors.yellow,
),
Container(
height: 100,
color: Colors.red,
),
]),
),
),
),
],
),
),
)
));
await expectLater(find.byKey(backgroundKey), matchesGoldenFile('decorated_sliver.moon.background.png'));
final Key foregroundKey = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: RepaintBoundary(
key: foregroundKey,
child: CustomScrollView(
slivers: <Widget>[
DecoratedSliver(
decoration: decoration,
position: DecorationPosition.foreground,
sliver: SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(<Widget>[
Container(
height: 100,
color: Colors.red,
),
Container(
height: 100,
color: Colors.yellow,
),
Container(
height: 100,
color: Colors.red,
),
]),
),
),
),
],
),
),
)
));
await expectLater(find.byKey(foregroundKey), matchesGoldenFile('decorated_sliver.moon.foreground.png'));
});
testWidgets('DecoratedSliver paints its border correctly vertically', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 300,
width: 100,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 100, height: 500),
),
),
],
),
),
),
));
controller.jumpTo(200);
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, -199.5) & const Size(99, 499),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver paints its border correctly vertically reverse', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 300,
width: 100,
child: CustomScrollView(
controller: controller,
reverse: true,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 100, height: 500),
),
),
],
),
),
),
));
controller.jumpTo(200);
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, -199.5) & const Size(99, 499),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver paints its border correctly horizontally', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 100,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 500, height: 100),
),
),
],
),
),
),
));
controller.jumpTo(200);
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(-199.5, 0.5) & const Size(499, 99),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver paints its border correctly horizontally reverse', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 100,
width: 300,
child: CustomScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverToBoxAdapter(
child: SizedBox(width: 500, height: 100),
),
),
],
),
),
),
));
controller.jumpTo(200);
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(-199.5, 0.5) & const Size(499, 99),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver works with SliverMainAxisGroup', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 100,
width: 300,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverMainAxisGroup(
slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(height: 100)),
SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
),
],
),
),
),
));
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, 0.5) & const Size(299, 199),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver works with SliverCrossAxisGroup', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 100,
width: 300,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: const SliverCrossAxisGroup(
slivers: <Widget>[
SliverToBoxAdapter(child: SizedBox(height: 100)),
SliverToBoxAdapter(child: SizedBox(height: 100)),
],
),
),
],
),
),
),
));
await tester.pumpAndSettle();
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, 0.5) & const Size(299, 99),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
testWidgets('DecoratedSliver draws only up to the bottom cache when sliver has infinite scroll extent', (WidgetTester tester) async {
const Key key = Key('DecoratedSliver with border');
const Color black = Color(0xFF000000);
final ScrollController controller = ScrollController();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
height: 100,
width: 300,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
DecoratedSliver(
key: key,
decoration: BoxDecoration(border: Border.all()),
sliver: SliverList.builder(
itemBuilder: (BuildContext context, int index) => const SizedBox(height: 100),
),
),
],
),
),
),
));
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, 0.5) & const Size(299, 349),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
controller.jumpTo(200);
await tester.pumpAndSettle();
// Note that the bottom edge is of the rect is the same as above.
expect(find.byKey(key), paints..rect(
rect: const Offset(0.5, -199.5) & const Size(299, 549),
color: black,
style: PaintingStyle.stroke,
strokeWidth: 1.0,
));
});
}
class TestDecoration extends Decoration {
final List<TestBoxPainter> painters = <TestBoxPainter>[];
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
final TestBoxPainter painter = TestBoxPainter();
painters.add(painter);
return painter;
}
}
class TestBoxPainter extends BoxPainter {
Offset? lastOffset;
ImageConfiguration? lastConfiguration;
bool disposed = false;
int paintCount = 0;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
lastOffset = offset;
lastConfiguration = configuration;
paintCount += 1;
}
@override
void dispose() {
assert(!disposed);
disposed = true;
super.dispose();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment