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