1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:vector_math/vector_math_64.dart';
8
9import 'debug.dart';
10import 'object.dart';
11import 'sliver.dart';
12
13/// Insets a [RenderSliver] by applying [resolvedPadding] on each side.
14///
15/// A [RenderSliverEdgeInsetsPadding] subclass wraps the [SliverGeometry.layoutExtent]
16/// of its child. Any incoming [SliverConstraints.overlap] is ignored and not
17/// passed on to the child.
18///
19/// {@template flutter.rendering.RenderSliverEdgeInsetsPadding}
20/// Applying padding in the main extent of the viewport to slivers that have scroll effects is likely to have
21/// undesired effects. For example, wrapping a [SliverPersistentHeader] with
22/// `pinned:true` will cause only the appbar to stay pinned while the padding will scroll away.
23/// {@endtemplate}
24abstract class RenderSliverEdgeInsetsPadding extends RenderSliver with RenderObjectWithChildMixin<RenderSliver> {
25 /// The amount to pad the child in each dimension.
26 ///
27 /// The offsets are specified in terms of visual edges, left, top, right, and
28 /// bottom. These values are not affected by the [TextDirection].
29 ///
30 /// Must not be null or contain negative values when [performLayout] is called.
31 EdgeInsets? get resolvedPadding;
32
33 /// The padding in the scroll direction on the side nearest the 0.0 scroll direction.
34 ///
35 /// Only valid after layout has started, since before layout the render object
36 /// doesn't know what direction it will be laid out in.
37 double get beforePadding {
38 assert(resolvedPadding != null);
39 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
40 case AxisDirection.up:
41 return resolvedPadding!.bottom;
42 case AxisDirection.right:
43 return resolvedPadding!.left;
44 case AxisDirection.down:
45 return resolvedPadding!.top;
46 case AxisDirection.left:
47 return resolvedPadding!.right;
48 }
49 }
50
51 /// The padding in the scroll direction on the side furthest from the 0.0 scroll offset.
52 ///
53 /// Only valid after layout has started, since before layout the render object
54 /// doesn't know what direction it will be laid out in.
55 double get afterPadding {
56 assert(resolvedPadding != null);
57 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
58 case AxisDirection.up:
59 return resolvedPadding!.top;
60 case AxisDirection.right:
61 return resolvedPadding!.right;
62 case AxisDirection.down:
63 return resolvedPadding!.bottom;
64 case AxisDirection.left:
65 return resolvedPadding!.left;
66 }
67 }
68
69 /// The total padding in the [SliverConstraints.axisDirection]. (In other
70 /// words, for a vertical downwards-growing list, the sum of the padding on
71 /// the top and bottom.)
72 ///
73 /// Only valid after layout has started, since before layout the render object
74 /// doesn't know what direction it will be laid out in.
75 double get mainAxisPadding {
76 assert(resolvedPadding != null);
77 return resolvedPadding!.along(constraints.axis);
78 }
79
80 /// The total padding in the cross-axis direction. (In other words, for a
81 /// vertical downwards-growing list, the sum of the padding on the left and
82 /// right.)
83 ///
84 /// Only valid after layout has started, since before layout the render object
85 /// doesn't know what direction it will be laid out in.
86 double get crossAxisPadding {
87 assert(resolvedPadding != null);
88 switch (constraints.axis) {
89 case Axis.horizontal:
90 return resolvedPadding!.vertical;
91 case Axis.vertical:
92 return resolvedPadding!.horizontal;
93 }
94 }
95
96 @override
97 void setupParentData(RenderObject child) {
98 if (child.parentData is! SliverPhysicalParentData) {
99 child.parentData = SliverPhysicalParentData();
100 }
101 }
102
103 @override
104 void performLayout() {
105 final SliverConstraints constraints = this.constraints;
106 assert(resolvedPadding != null);
107 final double beforePadding = this.beforePadding;
108 final double afterPadding = this.afterPadding;
109 final double mainAxisPadding = this.mainAxisPadding;
110 final double crossAxisPadding = this.crossAxisPadding;
111 if (child == null) {
112 final double paintExtent = calculatePaintOffset(
113 constraints,
114 from: 0.0,
115 to: mainAxisPadding,
116 );
117 final double cacheExtent = calculateCacheOffset(
118 constraints,
119 from: 0.0,
120 to: mainAxisPadding,
121 );
122 geometry = SliverGeometry(
123 scrollExtent: mainAxisPadding,
124 paintExtent: math.min(paintExtent, constraints.remainingPaintExtent),
125 maxPaintExtent: mainAxisPadding,
126 cacheExtent: cacheExtent,
127 );
128 return;
129 }
130 final double beforePaddingPaintExtent = calculatePaintOffset(
131 constraints,
132 from: 0.0,
133 to: beforePadding,
134 );
135 double overlap = constraints.overlap;
136 if (overlap > 0) {
137 overlap = math.max(0.0, constraints.overlap - beforePaddingPaintExtent);
138 }
139 child!.layout(
140 constraints.copyWith(
141 scrollOffset: math.max(0.0, constraints.scrollOffset - beforePadding),
142 cacheOrigin: math.min(0.0, constraints.cacheOrigin + beforePadding),
143 overlap: overlap,
144 remainingPaintExtent: constraints.remainingPaintExtent - calculatePaintOffset(constraints, from: 0.0, to: beforePadding),
145 remainingCacheExtent: constraints.remainingCacheExtent - calculateCacheOffset(constraints, from: 0.0, to: beforePadding),
146 crossAxisExtent: math.max(0.0, constraints.crossAxisExtent - crossAxisPadding),
147 precedingScrollExtent: beforePadding + constraints.precedingScrollExtent,
148 ),
149 parentUsesSize: true,
150 );
151 final SliverGeometry childLayoutGeometry = child!.geometry!;
152 if (childLayoutGeometry.scrollOffsetCorrection != null) {
153 geometry = SliverGeometry(
154 scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection,
155 );
156 return;
157 }
158 final double afterPaddingPaintExtent = calculatePaintOffset(
159 constraints,
160 from: beforePadding + childLayoutGeometry.scrollExtent,
161 to: mainAxisPadding + childLayoutGeometry.scrollExtent,
162 );
163 final double mainAxisPaddingPaintExtent = beforePaddingPaintExtent + afterPaddingPaintExtent;
164 final double beforePaddingCacheExtent = calculateCacheOffset(
165 constraints,
166 from: 0.0,
167 to: beforePadding,
168 );
169 final double afterPaddingCacheExtent = calculateCacheOffset(
170 constraints,
171 from: beforePadding + childLayoutGeometry.scrollExtent,
172 to: mainAxisPadding + childLayoutGeometry.scrollExtent,
173 );
174 final double mainAxisPaddingCacheExtent = afterPaddingCacheExtent + beforePaddingCacheExtent;
175 final double paintExtent = math.min(
176 beforePaddingPaintExtent + math.max(childLayoutGeometry.paintExtent, childLayoutGeometry.layoutExtent + afterPaddingPaintExtent),
177 constraints.remainingPaintExtent,
178 );
179 geometry = SliverGeometry(
180 paintOrigin: childLayoutGeometry.paintOrigin,
181 scrollExtent: mainAxisPadding + childLayoutGeometry.scrollExtent,
182 paintExtent: paintExtent,
183 layoutExtent: math.min(mainAxisPaddingPaintExtent + childLayoutGeometry.layoutExtent, paintExtent),
184 cacheExtent: math.min(mainAxisPaddingCacheExtent + childLayoutGeometry.cacheExtent, constraints.remainingCacheExtent),
185 maxPaintExtent: mainAxisPadding + childLayoutGeometry.maxPaintExtent,
186 hitTestExtent: math.max(
187 mainAxisPaddingPaintExtent + childLayoutGeometry.paintExtent,
188 beforePaddingPaintExtent + childLayoutGeometry.hitTestExtent,
189 ),
190 hasVisualOverflow: childLayoutGeometry.hasVisualOverflow,
191 );
192
193 final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
194 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
195 case AxisDirection.up:
196 childParentData.paintOffset = Offset(resolvedPadding!.left, calculatePaintOffset(constraints, from: resolvedPadding!.bottom + childLayoutGeometry.scrollExtent, to: resolvedPadding!.bottom + childLayoutGeometry.scrollExtent + resolvedPadding!.top));
197 case AxisDirection.right:
198 childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: 0.0, to: resolvedPadding!.left), resolvedPadding!.top);
199 case AxisDirection.down:
200 childParentData.paintOffset = Offset(resolvedPadding!.left, calculatePaintOffset(constraints, from: 0.0, to: resolvedPadding!.top));
201 case AxisDirection.left:
202 childParentData.paintOffset = Offset(calculatePaintOffset(constraints, from: resolvedPadding!.right + childLayoutGeometry.scrollExtent, to: resolvedPadding!.right + childLayoutGeometry.scrollExtent + resolvedPadding!.left), resolvedPadding!.top);
203 }
204 assert(beforePadding == this.beforePadding);
205 assert(afterPadding == this.afterPadding);
206 assert(mainAxisPadding == this.mainAxisPadding);
207 assert(crossAxisPadding == this.crossAxisPadding);
208 }
209
210 @override
211 bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
212 if (child != null && child!.geometry!.hitTestExtent > 0.0) {
213 final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
214 result.addWithAxisOffset(
215 mainAxisPosition: mainAxisPosition,
216 crossAxisPosition: crossAxisPosition,
217 mainAxisOffset: childMainAxisPosition(child!),
218 crossAxisOffset: childCrossAxisPosition(child!),
219 paintOffset: childParentData.paintOffset,
220 hitTest: child!.hitTest,
221 );
222 }
223 return false;
224 }
225
226 @override
227 double childMainAxisPosition(RenderSliver child) {
228 assert(child == this.child);
229 return calculatePaintOffset(constraints, from: 0.0, to: beforePadding);
230 }
231
232 @override
233 double childCrossAxisPosition(RenderSliver child) {
234 assert(child == this.child);
235 assert(resolvedPadding != null);
236 switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
237 case AxisDirection.up:
238 case AxisDirection.down:
239 return resolvedPadding!.left;
240 case AxisDirection.left:
241 case AxisDirection.right:
242 return resolvedPadding!.top;
243 }
244 }
245
246 @override
247 double? childScrollOffset(RenderObject child) {
248 assert(child.parent == this);
249 return beforePadding;
250 }
251
252 @override
253 void applyPaintTransform(RenderObject child, Matrix4 transform) {
254 assert(child == this.child);
255 final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
256 childParentData.applyPaintTransform(transform);
257 }
258
259 @override
260 void paint(PaintingContext context, Offset offset) {
261 if (child != null && child!.geometry!.visible) {
262 final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
263 context.paintChild(child!, offset + childParentData.paintOffset);
264 }
265 }
266
267 @override
268 void debugPaint(PaintingContext context, Offset offset) {
269 super.debugPaint(context, offset);
270 assert(() {
271 if (debugPaintSizeEnabled) {
272 final Size parentSize = getAbsoluteSize();
273 final Rect outerRect = offset & parentSize;
274 Rect? innerRect;
275 if (child != null) {
276 final Size childSize = child!.getAbsoluteSize();
277 final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData;
278 innerRect = (offset + childParentData.paintOffset) & childSize;
279 assert(innerRect.top >= outerRect.top);
280 assert(innerRect.left >= outerRect.left);
281 assert(innerRect.right <= outerRect.right);
282 assert(innerRect.bottom <= outerRect.bottom);
283 }
284 debugPaintPadding(context.canvas, outerRect, innerRect);
285 }
286 return true;
287 }());
288 }
289}
290
291/// Insets a [RenderSliver], applying padding on each side.
292///
293/// A [RenderSliverPadding] object wraps the [SliverGeometry.layoutExtent] of
294/// its child. Any incoming [SliverConstraints.overlap] is ignored and not
295/// passed on to the child.
296///
297/// {@macro flutter.rendering.RenderSliverEdgeInsetsPadding}
298class RenderSliverPadding extends RenderSliverEdgeInsetsPadding {
299 /// Creates a render object that insets its child in a viewport.
300 ///
301 /// The [padding] argument must have non-negative insets.
302 RenderSliverPadding({
303 required EdgeInsetsGeometry padding,
304 TextDirection? textDirection,
305 RenderSliver? child,
306 }) : assert(padding.isNonNegative),
307 _padding = padding,
308 _textDirection = textDirection {
309 this.child = child;
310 }
311
312 @override
313 EdgeInsets? get resolvedPadding => _resolvedPadding;
314 EdgeInsets? _resolvedPadding;
315
316 void _resolve() {
317 if (resolvedPadding != null) {
318 return;
319 }
320 _resolvedPadding = padding.resolve(textDirection);
321 assert(resolvedPadding!.isNonNegative);
322 }
323
324 void _markNeedsResolution() {
325 _resolvedPadding = null;
326 markNeedsLayout();
327 }
328
329 /// The amount to pad the child in each dimension.
330 ///
331 /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection]
332 /// must not be null.
333 EdgeInsetsGeometry get padding => _padding;
334 EdgeInsetsGeometry _padding;
335 set padding(EdgeInsetsGeometry value) {
336 assert(padding.isNonNegative);
337 if (_padding == value) {
338 return;
339 }
340 _padding = value;
341 _markNeedsResolution();
342 }
343
344 /// The text direction with which to resolve [padding].
345 ///
346 /// This may be changed to null, but only after the [padding] has been changed
347 /// to a value that does not depend on the direction.
348 TextDirection? get textDirection => _textDirection;
349 TextDirection? _textDirection;
350 set textDirection(TextDirection? value) {
351 if (_textDirection == value) {
352 return;
353 }
354 _textDirection = value;
355 _markNeedsResolution();
356 }
357
358 @override
359 void performLayout() {
360 _resolve();
361 super.performLayout();
362 }
363
364 @override
365 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
366 super.debugFillProperties(properties);
367 properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
368 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
369 }
370}
371