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 | /// @docImport 'layout_builder.dart'; |
6 | /// @docImport 'page_storage.dart'; |
7 | /// @docImport 'page_view.dart'; |
8 | library; |
9 | |
10 | import 'dart:math' as math; |
11 | |
12 | import 'package:flutter/gestures.dart' show DragStartBehavior; |
13 | import 'package:flutter/rendering.dart'; |
14 | |
15 | import 'basic.dart'; |
16 | import 'focus_manager.dart'; |
17 | import 'focus_scope.dart'; |
18 | import 'framework.dart'; |
19 | import 'notification_listener.dart'; |
20 | import 'primary_scroll_controller.dart'; |
21 | import 'scroll_configuration.dart'; |
22 | import 'scroll_controller.dart'; |
23 | import 'scroll_notification.dart'; |
24 | import 'scroll_physics.dart'; |
25 | import 'scroll_view.dart'; |
26 | import 'scrollable.dart'; |
27 | |
28 | /// A box in which a single widget can be scrolled. |
29 | /// |
30 | /// This widget is useful when you have a single box that will normally be |
31 | /// entirely visible, for example a clock face in a time picker, but you need to |
32 | /// make sure it can be scrolled if the container gets too small in one axis |
33 | /// (the scroll direction). |
34 | /// |
35 | /// It is also useful if you need to shrink-wrap in both axes (the main |
36 | /// scrolling direction as well as the cross axis), as one might see in a dialog |
37 | /// or pop-up menu. In that case, you might pair the [SingleChildScrollView] |
38 | /// with a [ListBody] child. |
39 | /// |
40 | /// When you have a list of children and do not require cross-axis |
41 | /// shrink-wrapping behavior, for example a scrolling list that is always the |
42 | /// width of the screen, consider [ListView], which is vastly more efficient |
43 | /// than a [SingleChildScrollView] containing a [ListBody] or [Column] with |
44 | /// many children. |
45 | /// |
46 | /// ## Sample code: Using [SingleChildScrollView] with a [Column] |
47 | /// |
48 | /// Sometimes a layout is designed around the flexible properties of a |
49 | /// [Column], but there is the concern that in some cases, there might not |
50 | /// be enough room to see the entire contents. This could be because some |
51 | /// devices have unusually small screens, or because the application can |
52 | /// be used in landscape mode where the aspect ratio isn't what was |
53 | /// originally envisioned, or because the application is being shown in a |
54 | /// small window in split-screen mode. In any case, as a result, it might |
55 | /// make sense to wrap the layout in a [SingleChildScrollView]. |
56 | /// |
57 | /// Doing so, however, usually results in a conflict between the [Column], |
58 | /// which typically tries to grow as big as it can, and the [SingleChildScrollView], |
59 | /// which provides its children with an infinite amount of space. |
60 | /// |
61 | /// To resolve this apparent conflict, there are a couple of techniques, as |
62 | /// discussed below. These techniques should only be used when the content is |
63 | /// normally expected to fit on the screen, so that the lazy instantiation of a |
64 | /// sliver-based [ListView] or [CustomScrollView] is not expected to provide any |
65 | /// performance benefit. If the viewport is expected to usually contain content |
66 | /// beyond the dimensions of the screen, then [SingleChildScrollView] would be |
67 | /// very expensive (in which case [ListView] may be a better choice than |
68 | /// [Column]). |
69 | /// |
70 | /// ### Centering, spacing, or aligning fixed-height content |
71 | /// |
72 | /// If the content has fixed (or intrinsic) dimensions but needs to be spaced out, |
73 | /// centered, or otherwise positioned using the [Flex] layout model of a [Column], |
74 | /// the following technique can be used to provide the [Column] with a minimum |
75 | /// dimension while allowing it to shrink-wrap the contents when there isn't enough |
76 | /// room to apply these spacing or alignment needs. |
77 | /// |
78 | /// A [LayoutBuilder] is used to obtain the size of the viewport (implicitly via |
79 | /// the constraints that the [SingleChildScrollView] sees, since viewports |
80 | /// typically grow to fit their maximum height constraint). Then, inside the |
81 | /// scroll view, a [ConstrainedBox] is used to set the minimum height of the |
82 | /// [Column]. |
83 | /// |
84 | /// The [Column] has no [Expanded] children, so rather than take on the infinite |
85 | /// height from its [BoxConstraints.maxHeight], (the viewport provides no maximum height |
86 | /// constraint), it automatically tries to shrink to fit its children. It cannot |
87 | /// be smaller than its [BoxConstraints.minHeight], though, and It therefore |
88 | /// becomes the bigger of the minimum height provided by the |
89 | /// [ConstrainedBox] and the sum of the heights of the children. |
90 | /// |
91 | /// If the children aren't enough to fit that minimum size, the [Column] ends up |
92 | /// with some remaining space to allocate as specified by its |
93 | /// [Column.mainAxisAlignment] argument. |
94 | /// |
95 | /// {@tool dartpad} |
96 | /// In this example, the children are spaced out equally, unless there's no more |
97 | /// room, in which case they stack vertically and scroll. |
98 | /// |
99 | /// When using this technique, [Expanded] and [Flexible] are not useful, because |
100 | /// in both cases the "available space" is infinite (since this is in a viewport). |
101 | /// The next section describes a technique for providing a maximum height constraint. |
102 | /// |
103 | /// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.0.dart ** |
104 | /// {@end-tool} |
105 | /// |
106 | /// ### Expanding content to fit the viewport |
107 | /// |
108 | /// The following example builds on the previous one. In addition to providing a |
109 | /// minimum dimension for the child [Column], an [IntrinsicHeight] widget is used |
110 | /// to force the column to be exactly as big as its contents. This constraint |
111 | /// combines with the [ConstrainedBox] constraints discussed previously to ensure |
112 | /// that the column becomes either as big as viewport, or as big as the contents, |
113 | /// whichever is biggest. |
114 | /// |
115 | /// Both constraints must be used to get the desired effect. If only the |
116 | /// [IntrinsicHeight] was specified, then the column would not grow to fit the |
117 | /// entire viewport when its children were smaller than the whole screen. If only |
118 | /// the size of the viewport was used, then the [Column] would overflow if the |
119 | /// children were bigger than the viewport. |
120 | /// |
121 | /// The widget that is to grow to fit the remaining space so provided is wrapped |
122 | /// in an [Expanded] widget. |
123 | /// |
124 | /// This technique is quite expensive, as it more or less requires that the contents |
125 | /// of the viewport be laid out twice (once to find their intrinsic dimensions, and |
126 | /// once to actually lay them out). The number of widgets within the column should |
127 | /// therefore be kept small. Alternatively, subsets of the children that have known |
128 | /// dimensions can be wrapped in a [SizedBox] that has tight vertical constraints, |
129 | /// so that the intrinsic sizing algorithm can short-circuit the computation when it |
130 | /// reaches those parts of the subtree. |
131 | /// |
132 | /// {@tool dartpad} |
133 | /// In this example, the column becomes either as big as viewport, or as big as |
134 | /// the contents, whichever is biggest. |
135 | /// |
136 | /// ** See code in examples/api/lib/widgets/single_child_scroll_view/single_child_scroll_view.1.dart ** |
137 | /// {@end-tool} |
138 | /// |
139 | /// {@macro flutter.widgets.ScrollView.PageStorage} |
140 | /// |
141 | /// See also: |
142 | /// |
143 | /// * [ListView], which handles multiple children in a scrolling list. |
144 | /// * [GridView], which handles multiple children in a scrolling grid. |
145 | /// * [PageView], for a scrollable that works page by page. |
146 | /// * [Scrollable], which handles arbitrary scrolling effects. |
147 | class SingleChildScrollView extends StatelessWidget { |
148 | /// Creates a box in which a single widget can be scrolled. |
149 | const SingleChildScrollView({ |
150 | super.key, |
151 | this.scrollDirection = Axis.vertical, |
152 | this.reverse = false, |
153 | this.padding, |
154 | this.primary, |
155 | this.physics, |
156 | this.controller, |
157 | this.child, |
158 | this.dragStartBehavior = DragStartBehavior.start, |
159 | this.clipBehavior = Clip.hardEdge, |
160 | this.hitTestBehavior = HitTestBehavior.opaque, |
161 | this.restorationId, |
162 | this.keyboardDismissBehavior, |
163 | }) : assert( |
164 | !(controller != null && (primary ?? false)), |
165 | 'Primary ScrollViews obtain their ScrollController via inheritance ' |
166 | 'from a PrimaryScrollController widget. You cannot both set primary to ' |
167 | 'true and pass an explicit controller.', |
168 | ); |
169 | |
170 | /// {@macro flutter.widgets.scroll_view.scrollDirection} |
171 | final Axis scrollDirection; |
172 | |
173 | /// Whether the scroll view scrolls in the reading direction. |
174 | /// |
175 | /// For example, if the reading direction is left-to-right and |
176 | /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from |
177 | /// left to right when [reverse] is false and from right to left when |
178 | /// [reverse] is true. |
179 | /// |
180 | /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view |
181 | /// scrolls from top to bottom when [reverse] is false and from bottom to top |
182 | /// when [reverse] is true. |
183 | /// |
184 | /// Defaults to false. |
185 | final bool reverse; |
186 | |
187 | /// The amount of space by which to inset the child. |
188 | final EdgeInsetsGeometry? padding; |
189 | |
190 | /// An object that can be used to control the position to which this scroll |
191 | /// view is scrolled. |
192 | /// |
193 | /// Must be null if [primary] is true. |
194 | /// |
195 | /// A [ScrollController] serves several purposes. It can be used to control |
196 | /// the initial scroll position (see [ScrollController.initialScrollOffset]). |
197 | /// It can be used to control whether the scroll view should automatically |
198 | /// save and restore its scroll position in the [PageStorage] (see |
199 | /// [ScrollController.keepScrollOffset]). It can be used to read the current |
200 | /// scroll position (see [ScrollController.offset]), or change it (see |
201 | /// [ScrollController.animateTo]). |
202 | final ScrollController? controller; |
203 | |
204 | /// {@macro flutter.widgets.scroll_view.primary} |
205 | final bool? primary; |
206 | |
207 | /// How the scroll view should respond to user input. |
208 | /// |
209 | /// For example, determines how the scroll view continues to animate after the |
210 | /// user stops dragging the scroll view. |
211 | /// |
212 | /// Defaults to matching platform conventions. |
213 | final ScrollPhysics? physics; |
214 | |
215 | /// The widget that scrolls. |
216 | /// |
217 | /// {@macro flutter.widgets.ProxyWidget.child} |
218 | final Widget? child; |
219 | |
220 | /// {@macro flutter.widgets.scrollable.dragStartBehavior} |
221 | final DragStartBehavior dragStartBehavior; |
222 | |
223 | /// {@macro flutter.material.Material.clipBehavior} |
224 | /// |
225 | /// Defaults to [Clip.hardEdge]. |
226 | final Clip clipBehavior; |
227 | |
228 | /// {@macro flutter.widgets.scrollable.hitTestBehavior} |
229 | /// |
230 | /// Defaults to [HitTestBehavior.opaque]. |
231 | final HitTestBehavior hitTestBehavior; |
232 | |
233 | /// {@macro flutter.widgets.scrollable.restorationId} |
234 | final String? restorationId; |
235 | |
236 | /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} |
237 | /// |
238 | /// If [keyboardDismissBehavior] is null then it will fallback to the inherited |
239 | /// [ScrollBehavior.getKeyboardDismissBehavior]. |
240 | final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; |
241 | |
242 | AxisDirection _getDirection(BuildContext context) { |
243 | return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); |
244 | } |
245 | |
246 | @override |
247 | Widget build(BuildContext context) { |
248 | final AxisDirection axisDirection = _getDirection(context); |
249 | Widget? contents = child; |
250 | if (padding != null) { |
251 | contents = Padding(padding: padding!, child: contents); |
252 | } |
253 | final bool effectivePrimary = |
254 | primary ?? |
255 | controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection); |
256 | |
257 | final ScrollController? scrollController = |
258 | effectivePrimary ? PrimaryScrollController.maybeOf(context) : controller; |
259 | |
260 | Widget scrollable = Scrollable( |
261 | dragStartBehavior: dragStartBehavior, |
262 | axisDirection: axisDirection, |
263 | controller: scrollController, |
264 | physics: physics, |
265 | restorationId: restorationId, |
266 | clipBehavior: clipBehavior, |
267 | hitTestBehavior: hitTestBehavior, |
268 | viewportBuilder: (BuildContext context, ViewportOffset offset) { |
269 | return _SingleChildViewport( |
270 | axisDirection: axisDirection, |
271 | offset: offset, |
272 | clipBehavior: clipBehavior, |
273 | child: contents, |
274 | ); |
275 | }, |
276 | ); |
277 | |
278 | final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = |
279 | keyboardDismissBehavior ?? |
280 | ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); |
281 | |
282 | if (effectiveKeyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { |
283 | scrollable = NotificationListener<ScrollUpdateNotification>( |
284 | child: scrollable, |
285 | onNotification: (ScrollUpdateNotification notification) { |
286 | final FocusScopeNode currentScope = FocusScope.of(context); |
287 | if (notification.dragDetails != null && |
288 | !currentScope.hasPrimaryFocus && |
289 | currentScope.hasFocus) { |
290 | FocusManager.instance.primaryFocus?.unfocus(); |
291 | } |
292 | return false; |
293 | }, |
294 | ); |
295 | } |
296 | |
297 | return effectivePrimary && scrollController != null |
298 | // Further descendant ScrollViews will not inherit the same |
299 | // PrimaryScrollController |
300 | ? PrimaryScrollController.none(child: scrollable) |
301 | : scrollable; |
302 | } |
303 | } |
304 | |
305 | class _SingleChildViewport extends SingleChildRenderObjectWidget { |
306 | const _SingleChildViewport({ |
307 | this.axisDirection = AxisDirection.down, |
308 | required this.offset, |
309 | super.child, |
310 | required this.clipBehavior, |
311 | }); |
312 | |
313 | final AxisDirection axisDirection; |
314 | final ViewportOffset offset; |
315 | final Clip clipBehavior; |
316 | |
317 | @override |
318 | _RenderSingleChildViewport createRenderObject(BuildContext context) { |
319 | return _RenderSingleChildViewport( |
320 | axisDirection: axisDirection, |
321 | offset: offset, |
322 | clipBehavior: clipBehavior, |
323 | ); |
324 | } |
325 | |
326 | @override |
327 | void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) { |
328 | // Order dependency: The offset setter reads the axis direction. |
329 | renderObject |
330 | ..axisDirection = axisDirection |
331 | ..offset = offset |
332 | ..clipBehavior = clipBehavior; |
333 | } |
334 | |
335 | @override |
336 | SingleChildRenderObjectElement createElement() { |
337 | return _SingleChildViewportElement(this); |
338 | } |
339 | } |
340 | |
341 | class _SingleChildViewportElement extends SingleChildRenderObjectElement |
342 | with NotifiableElementMixin, ViewportElementMixin { |
343 | _SingleChildViewportElement(_SingleChildViewport super.widget); |
344 | } |
345 | |
346 | class _RenderSingleChildViewport extends RenderBox |
347 | with RenderObjectWithChildMixin<RenderBox> |
348 | implements RenderAbstractViewport { |
349 | _RenderSingleChildViewport({ |
350 | AxisDirection axisDirection = AxisDirection.down, |
351 | required ViewportOffset offset, |
352 | RenderBox? child, |
353 | required Clip clipBehavior, |
354 | }) : _axisDirection = axisDirection, |
355 | _offset = offset, |
356 | _clipBehavior = clipBehavior { |
357 | this.child = child; |
358 | } |
359 | |
360 | AxisDirection get axisDirection => _axisDirection; |
361 | AxisDirection _axisDirection; |
362 | set axisDirection(AxisDirection value) { |
363 | if (value == _axisDirection) { |
364 | return; |
365 | } |
366 | _axisDirection = value; |
367 | markNeedsLayout(); |
368 | } |
369 | |
370 | Axis get axis => axisDirectionToAxis(axisDirection); |
371 | |
372 | ViewportOffset get offset => _offset; |
373 | ViewportOffset _offset; |
374 | set offset(ViewportOffset value) { |
375 | if (value == _offset) { |
376 | return; |
377 | } |
378 | if (attached) { |
379 | _offset.removeListener(_hasScrolled); |
380 | } |
381 | _offset = value; |
382 | if (attached) { |
383 | _offset.addListener(_hasScrolled); |
384 | } |
385 | markNeedsLayout(); |
386 | } |
387 | |
388 | /// {@macro flutter.material.Material.clipBehavior} |
389 | /// |
390 | /// Defaults to [Clip.none]. |
391 | Clip get clipBehavior => _clipBehavior; |
392 | Clip _clipBehavior = Clip.none; |
393 | set clipBehavior(Clip value) { |
394 | if (value != _clipBehavior) { |
395 | _clipBehavior = value; |
396 | markNeedsPaint(); |
397 | markNeedsSemanticsUpdate(); |
398 | } |
399 | } |
400 | |
401 | void _hasScrolled() { |
402 | markNeedsPaint(); |
403 | markNeedsSemanticsUpdate(); |
404 | } |
405 | |
406 | @override |
407 | void setupParentData(RenderObject child) { |
408 | // We don't actually use the offset argument in BoxParentData, so let's |
409 | // avoid allocating it at all. |
410 | if (child.parentData is! ParentData) { |
411 | child.parentData = ParentData(); |
412 | } |
413 | } |
414 | |
415 | @override |
416 | void attach(PipelineOwner owner) { |
417 | super.attach(owner); |
418 | _offset.addListener(_hasScrolled); |
419 | } |
420 | |
421 | @override |
422 | void detach() { |
423 | _offset.removeListener(_hasScrolled); |
424 | super.detach(); |
425 | } |
426 | |
427 | @override |
428 | bool get isRepaintBoundary => true; |
429 | |
430 | double get _viewportExtent { |
431 | assert(hasSize); |
432 | return switch (axis) { |
433 | Axis.horizontal => size.width, |
434 | Axis.vertical => size.height, |
435 | }; |
436 | } |
437 | |
438 | double get _minScrollExtent { |
439 | assert(hasSize); |
440 | return 0.0; |
441 | } |
442 | |
443 | double get _maxScrollExtent { |
444 | assert(hasSize); |
445 | if (child == null) { |
446 | return 0.0; |
447 | } |
448 | return math.max(0.0, switch (axis) { |
449 | Axis.horizontal => child!.size.width - size.width, |
450 | Axis.vertical => child!.size.height - size.height, |
451 | }); |
452 | } |
453 | |
454 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
455 | return switch (axis) { |
456 | Axis.horizontal => constraints.heightConstraints(), |
457 | Axis.vertical => constraints.widthConstraints(), |
458 | }; |
459 | } |
460 | |
461 | @override |
462 | double computeMinIntrinsicWidth(double height) { |
463 | return child?.getMinIntrinsicWidth(height) ?? 0.0; |
464 | } |
465 | |
466 | @override |
467 | double computeMaxIntrinsicWidth(double height) { |
468 | return child?.getMaxIntrinsicWidth(height) ?? 0.0; |
469 | } |
470 | |
471 | @override |
472 | double computeMinIntrinsicHeight(double width) { |
473 | return child?.getMinIntrinsicHeight(width) ?? 0.0; |
474 | } |
475 | |
476 | @override |
477 | double computeMaxIntrinsicHeight(double width) { |
478 | return child?.getMaxIntrinsicHeight(width) ?? 0.0; |
479 | } |
480 | |
481 | // We don't override computeDistanceToActualBaseline(), because we |
482 | // want the default behavior (returning null). Otherwise, as you |
483 | // scroll, it would shift in its parent if the parent was baseline-aligned, |
484 | // which makes no sense. |
485 | |
486 | @override |
487 | Size computeDryLayout(BoxConstraints constraints) { |
488 | if (child == null) { |
489 | return constraints.smallest; |
490 | } |
491 | final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
492 | return constraints.constrain(childSize); |
493 | } |
494 | |
495 | @override |
496 | void performLayout() { |
497 | final BoxConstraints constraints = this.constraints; |
498 | if (child == null) { |
499 | size = constraints.smallest; |
500 | } else { |
501 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
502 | size = constraints.constrain(child!.size); |
503 | } |
504 | |
505 | if (offset.hasPixels) { |
506 | if (offset.pixels > _maxScrollExtent) { |
507 | offset.correctBy(_maxScrollExtent - offset.pixels); |
508 | } else if (offset.pixels < _minScrollExtent) { |
509 | offset.correctBy(_minScrollExtent - offset.pixels); |
510 | } |
511 | } |
512 | |
513 | offset.applyViewportDimension(_viewportExtent); |
514 | offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
515 | } |
516 | |
517 | Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); |
518 | |
519 | Offset _paintOffsetForPosition(double position) { |
520 | return switch (axisDirection) { |
521 | AxisDirection.up => Offset(0.0, position - child!.size.height + size.height), |
522 | AxisDirection.left => Offset(position - child!.size.width + size.width, 0.0), |
523 | AxisDirection.right => Offset(-position, 0.0), |
524 | AxisDirection.down => Offset(0.0, -position), |
525 | }; |
526 | } |
527 | |
528 | bool _shouldClipAtPaintOffset(Offset paintOffset) { |
529 | assert(child != null); |
530 | switch (clipBehavior) { |
531 | case Clip.none: |
532 | return false; |
533 | case Clip.hardEdge: |
534 | case Clip.antiAlias: |
535 | case Clip.antiAliasWithSaveLayer: |
536 | return paintOffset.dx < 0 || |
537 | paintOffset.dy < 0 || |
538 | paintOffset.dx + child!.size.width > size.width || |
539 | paintOffset.dy + child!.size.height > size.height; |
540 | } |
541 | } |
542 | |
543 | @override |
544 | void paint(PaintingContext context, Offset offset) { |
545 | if (child != null) { |
546 | final Offset paintOffset = _paintOffset; |
547 | |
548 | void paintContents(PaintingContext context, Offset offset) { |
549 | context.paintChild(child!, offset + paintOffset); |
550 | } |
551 | |
552 | if (_shouldClipAtPaintOffset(paintOffset)) { |
553 | _clipRectLayer.layer = context.pushClipRect( |
554 | needsCompositing, |
555 | offset, |
556 | Offset.zero & size, |
557 | paintContents, |
558 | clipBehavior: clipBehavior, |
559 | oldLayer: _clipRectLayer.layer, |
560 | ); |
561 | } else { |
562 | _clipRectLayer.layer = null; |
563 | paintContents(context, offset); |
564 | } |
565 | } |
566 | } |
567 | |
568 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
569 | |
570 | @override |
571 | void dispose() { |
572 | _clipRectLayer.layer = null; |
573 | super.dispose(); |
574 | } |
575 | |
576 | @override |
577 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
578 | final Offset paintOffset = _paintOffset; |
579 | transform.translate(paintOffset.dx, paintOffset.dy); |
580 | } |
581 | |
582 | @override |
583 | Rect? describeApproximatePaintClip(RenderObject? child) { |
584 | if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { |
585 | return Offset.zero & size; |
586 | } |
587 | return null; |
588 | } |
589 | |
590 | @override |
591 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
592 | if (child != null) { |
593 | return result.addWithPaintOffset( |
594 | offset: _paintOffset, |
595 | position: position, |
596 | hitTest: (BoxHitTestResult result, Offset transformed) { |
597 | assert(transformed == position + -_paintOffset); |
598 | return child!.hitTest(result, position: transformed); |
599 | }, |
600 | ); |
601 | } |
602 | return false; |
603 | } |
604 | |
605 | @override |
606 | RevealedOffset getOffsetToReveal( |
607 | RenderObject target, |
608 | double alignment, { |
609 | Rect? rect, |
610 | Axis? axis, |
611 | }) { |
612 | // One dimensional viewport has only one axis, override if it was |
613 | // provided/may be mismatched. |
614 | axis = this.axis; |
615 | |
616 | rect ??= target.paintBounds; |
617 | if (target is! RenderBox) { |
618 | return RevealedOffset(offset: offset.pixels, rect: rect); |
619 | } |
620 | |
621 | final RenderBox targetBox = target; |
622 | final Matrix4 transform = targetBox.getTransformTo(child); |
623 | final Rect bounds = MatrixUtils.transformRect(transform, rect); |
624 | final Size contentSize = child!.size; |
625 | |
626 | final ( |
627 | double mainAxisExtent, |
628 | double leadingScrollOffset, |
629 | double targetMainAxisExtent, |
630 | ) = switch (axisDirection) { |
631 | AxisDirection.up => (size.height, contentSize.height - bounds.bottom, bounds.height), |
632 | AxisDirection.left => (size.width, contentSize.width - bounds.right, bounds.width), |
633 | AxisDirection.right => (size.width, bounds.left, bounds.width), |
634 | AxisDirection.down => (size.height, bounds.top, bounds.height), |
635 | }; |
636 | |
637 | final double targetOffset = |
638 | leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; |
639 | final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); |
640 | return RevealedOffset(offset: targetOffset, rect: targetRect); |
641 | } |
642 | |
643 | @override |
644 | void showOnScreen({ |
645 | RenderObject? descendant, |
646 | Rect? rect, |
647 | Duration duration = Duration.zero, |
648 | Curve curve = Curves.ease, |
649 | }) { |
650 | if (!offset.allowImplicitScrolling) { |
651 | return super.showOnScreen( |
652 | descendant: descendant, |
653 | rect: rect, |
654 | duration: duration, |
655 | curve: curve, |
656 | ); |
657 | } |
658 | |
659 | final Rect? newRect = RenderViewportBase.showInViewport( |
660 | descendant: descendant, |
661 | viewport: this, |
662 | offset: offset, |
663 | rect: rect, |
664 | duration: duration, |
665 | curve: curve, |
666 | ); |
667 | super.showOnScreen(rect: newRect, duration: duration, curve: curve); |
668 | } |
669 | |
670 | @override |
671 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
672 | super.debugFillProperties(properties); |
673 | properties.add(DiagnosticsProperty<Offset>('offset', _paintOffset)); |
674 | } |
675 | |
676 | @override |
677 | Rect describeSemanticsClip(RenderObject child) { |
678 | final double remainingOffset = _maxScrollExtent - offset.pixels; |
679 | switch (axisDirection) { |
680 | case AxisDirection.up: |
681 | return Rect.fromLTRB( |
682 | semanticBounds.left, |
683 | semanticBounds.top - remainingOffset, |
684 | semanticBounds.right, |
685 | semanticBounds.bottom + offset.pixels, |
686 | ); |
687 | case AxisDirection.right: |
688 | return Rect.fromLTRB( |
689 | semanticBounds.left - offset.pixels, |
690 | semanticBounds.top, |
691 | semanticBounds.right + remainingOffset, |
692 | semanticBounds.bottom, |
693 | ); |
694 | case AxisDirection.down: |
695 | return Rect.fromLTRB( |
696 | semanticBounds.left, |
697 | semanticBounds.top - offset.pixels, |
698 | semanticBounds.right, |
699 | semanticBounds.bottom + remainingOffset, |
700 | ); |
701 | case AxisDirection.left: |
702 | return Rect.fromLTRB( |
703 | semanticBounds.left - remainingOffset, |
704 | semanticBounds.top, |
705 | semanticBounds.right + offset.pixels, |
706 | semanticBounds.bottom, |
707 | ); |
708 | } |
709 | } |
710 | } |
711 |
Definitions
- SingleChildScrollView
- SingleChildScrollView
- _getDirection
- build
- _SingleChildViewport
- _SingleChildViewport
- createRenderObject
- updateRenderObject
- createElement
- _SingleChildViewportElement
- _SingleChildViewportElement
- _RenderSingleChildViewport
- _RenderSingleChildViewport
- axisDirection
- axisDirection
- axis
- offset
- offset
- clipBehavior
- clipBehavior
- _hasScrolled
- setupParentData
- attach
- detach
- isRepaintBoundary
- _viewportExtent
- _minScrollExtent
- _maxScrollExtent
- _getInnerConstraints
- computeMinIntrinsicWidth
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMaxIntrinsicHeight
- computeDryLayout
- performLayout
- _paintOffset
- _paintOffsetForPosition
- _shouldClipAtPaintOffset
- paint
- paintContents
- dispose
- applyPaintTransform
- describeApproximatePaintClip
- hitTestChildren
- getOffsetToReveal
- showOnScreen
- debugFillProperties
Learn more about Flutter for embedded and desktop on industrialflutter.com