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 | |
5 | import 'dart:math' as math; |
6 | |
7 | import 'package:vector_math/vector_math_64.dart' ; |
8 | |
9 | import 'debug.dart'; |
10 | import 'object.dart'; |
11 | import '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} |
24 | abstract 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} |
298 | class 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 | |