| 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 = effectivePrimary |
| 258 | ? PrimaryScrollController.maybeOf(context) |
| 259 | : controller; |
| 260 | |
| 261 | Widget scrollable = Scrollable( |
| 262 | dragStartBehavior: dragStartBehavior, |
| 263 | axisDirection: axisDirection, |
| 264 | controller: scrollController, |
| 265 | physics: physics, |
| 266 | restorationId: restorationId, |
| 267 | clipBehavior: clipBehavior, |
| 268 | hitTestBehavior: hitTestBehavior, |
| 269 | viewportBuilder: (BuildContext context, ViewportOffset offset) { |
| 270 | return _SingleChildViewport( |
| 271 | axisDirection: axisDirection, |
| 272 | offset: offset, |
| 273 | clipBehavior: clipBehavior, |
| 274 | child: contents, |
| 275 | ); |
| 276 | }, |
| 277 | ); |
| 278 | |
| 279 | final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = |
| 280 | keyboardDismissBehavior ?? |
| 281 | ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); |
| 282 | |
| 283 | if (effectiveKeyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { |
| 284 | scrollable = NotificationListener<ScrollUpdateNotification>( |
| 285 | child: scrollable, |
| 286 | onNotification: (ScrollUpdateNotification notification) { |
| 287 | final FocusScopeNode currentScope = FocusScope.of(context); |
| 288 | if (notification.dragDetails != null && |
| 289 | !currentScope.hasPrimaryFocus && |
| 290 | currentScope.hasFocus) { |
| 291 | FocusManager.instance.primaryFocus?.unfocus(); |
| 292 | } |
| 293 | return false; |
| 294 | }, |
| 295 | ); |
| 296 | } |
| 297 | |
| 298 | return effectivePrimary && scrollController != null |
| 299 | // Further descendant ScrollViews will not inherit the same |
| 300 | // PrimaryScrollController |
| 301 | ? PrimaryScrollController.none(child: scrollable) |
| 302 | : scrollable; |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | class _SingleChildViewport extends SingleChildRenderObjectWidget { |
| 307 | const _SingleChildViewport({ |
| 308 | this.axisDirection = AxisDirection.down, |
| 309 | required this.offset, |
| 310 | super.child, |
| 311 | required this.clipBehavior, |
| 312 | }); |
| 313 | |
| 314 | final AxisDirection axisDirection; |
| 315 | final ViewportOffset offset; |
| 316 | final Clip clipBehavior; |
| 317 | |
| 318 | @override |
| 319 | _RenderSingleChildViewport createRenderObject(BuildContext context) { |
| 320 | return _RenderSingleChildViewport( |
| 321 | axisDirection: axisDirection, |
| 322 | offset: offset, |
| 323 | clipBehavior: clipBehavior, |
| 324 | ); |
| 325 | } |
| 326 | |
| 327 | @override |
| 328 | void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) { |
| 329 | // Order dependency: The offset setter reads the axis direction. |
| 330 | renderObject |
| 331 | ..axisDirection = axisDirection |
| 332 | ..offset = offset |
| 333 | ..clipBehavior = clipBehavior; |
| 334 | } |
| 335 | |
| 336 | @override |
| 337 | SingleChildRenderObjectElement createElement() { |
| 338 | return _SingleChildViewportElement(this); |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | class _SingleChildViewportElement extends SingleChildRenderObjectElement |
| 343 | with NotifiableElementMixin, ViewportElementMixin { |
| 344 | _SingleChildViewportElement(_SingleChildViewport super.widget); |
| 345 | } |
| 346 | |
| 347 | class _RenderSingleChildViewport extends RenderBox |
| 348 | with RenderObjectWithChildMixin<RenderBox> |
| 349 | implements RenderAbstractViewport { |
| 350 | _RenderSingleChildViewport({ |
| 351 | AxisDirection axisDirection = AxisDirection.down, |
| 352 | required ViewportOffset offset, |
| 353 | RenderBox? child, |
| 354 | required Clip clipBehavior, |
| 355 | }) : _axisDirection = axisDirection, |
| 356 | _offset = offset, |
| 357 | _clipBehavior = clipBehavior { |
| 358 | this.child = child; |
| 359 | } |
| 360 | |
| 361 | AxisDirection get axisDirection => _axisDirection; |
| 362 | AxisDirection _axisDirection; |
| 363 | set axisDirection(AxisDirection value) { |
| 364 | if (value == _axisDirection) { |
| 365 | return; |
| 366 | } |
| 367 | _axisDirection = value; |
| 368 | markNeedsLayout(); |
| 369 | } |
| 370 | |
| 371 | Axis get axis => axisDirectionToAxis(axisDirection); |
| 372 | |
| 373 | ViewportOffset get offset => _offset; |
| 374 | ViewportOffset _offset; |
| 375 | set offset(ViewportOffset value) { |
| 376 | if (value == _offset) { |
| 377 | return; |
| 378 | } |
| 379 | if (attached) { |
| 380 | _offset.removeListener(_hasScrolled); |
| 381 | } |
| 382 | _offset = value; |
| 383 | if (attached) { |
| 384 | _offset.addListener(_hasScrolled); |
| 385 | } |
| 386 | markNeedsLayout(); |
| 387 | } |
| 388 | |
| 389 | /// {@macro flutter.material.Material.clipBehavior} |
| 390 | /// |
| 391 | /// Defaults to [Clip.none]. |
| 392 | Clip get clipBehavior => _clipBehavior; |
| 393 | Clip _clipBehavior = Clip.none; |
| 394 | set clipBehavior(Clip value) { |
| 395 | if (value != _clipBehavior) { |
| 396 | _clipBehavior = value; |
| 397 | markNeedsPaint(); |
| 398 | markNeedsSemanticsUpdate(); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | void _hasScrolled() { |
| 403 | markNeedsPaint(); |
| 404 | markNeedsSemanticsUpdate(); |
| 405 | } |
| 406 | |
| 407 | @override |
| 408 | void setupParentData(RenderObject child) { |
| 409 | // We don't actually use the offset argument in BoxParentData, so let's |
| 410 | // avoid allocating it at all. |
| 411 | if (child.parentData is! ParentData) { |
| 412 | child.parentData = ParentData(); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | @override |
| 417 | void attach(PipelineOwner owner) { |
| 418 | super.attach(owner); |
| 419 | _offset.addListener(_hasScrolled); |
| 420 | } |
| 421 | |
| 422 | @override |
| 423 | void detach() { |
| 424 | _offset.removeListener(_hasScrolled); |
| 425 | super.detach(); |
| 426 | } |
| 427 | |
| 428 | @override |
| 429 | bool get isRepaintBoundary => true; |
| 430 | |
| 431 | double get _viewportExtent { |
| 432 | assert(hasSize); |
| 433 | return switch (axis) { |
| 434 | Axis.horizontal => size.width, |
| 435 | Axis.vertical => size.height, |
| 436 | }; |
| 437 | } |
| 438 | |
| 439 | double get _minScrollExtent { |
| 440 | assert(hasSize); |
| 441 | return 0.0; |
| 442 | } |
| 443 | |
| 444 | double get _maxScrollExtent { |
| 445 | assert(hasSize); |
| 446 | if (child == null) { |
| 447 | return 0.0; |
| 448 | } |
| 449 | return math.max(0.0, switch (axis) { |
| 450 | Axis.horizontal => child!.size.width - size.width, |
| 451 | Axis.vertical => child!.size.height - size.height, |
| 452 | }); |
| 453 | } |
| 454 | |
| 455 | BoxConstraints _getInnerConstraints(BoxConstraints constraints) { |
| 456 | return switch (axis) { |
| 457 | Axis.horizontal => constraints.heightConstraints(), |
| 458 | Axis.vertical => constraints.widthConstraints(), |
| 459 | }; |
| 460 | } |
| 461 | |
| 462 | @override |
| 463 | double computeMinIntrinsicWidth(double height) { |
| 464 | return child?.getMinIntrinsicWidth(height) ?? 0.0; |
| 465 | } |
| 466 | |
| 467 | @override |
| 468 | double computeMaxIntrinsicWidth(double height) { |
| 469 | return child?.getMaxIntrinsicWidth(height) ?? 0.0; |
| 470 | } |
| 471 | |
| 472 | @override |
| 473 | double computeMinIntrinsicHeight(double width) { |
| 474 | return child?.getMinIntrinsicHeight(width) ?? 0.0; |
| 475 | } |
| 476 | |
| 477 | @override |
| 478 | double computeMaxIntrinsicHeight(double width) { |
| 479 | return child?.getMaxIntrinsicHeight(width) ?? 0.0; |
| 480 | } |
| 481 | |
| 482 | // We don't override computeDistanceToActualBaseline(), because we |
| 483 | // want the default behavior (returning null). Otherwise, as you |
| 484 | // scroll, it would shift in its parent if the parent was baseline-aligned, |
| 485 | // which makes no sense. |
| 486 | |
| 487 | @override |
| 488 | Size computeDryLayout(BoxConstraints constraints) { |
| 489 | if (child == null) { |
| 490 | return constraints.smallest; |
| 491 | } |
| 492 | final Size childSize = child!.getDryLayout(_getInnerConstraints(constraints)); |
| 493 | return constraints.constrain(childSize); |
| 494 | } |
| 495 | |
| 496 | @override |
| 497 | void performLayout() { |
| 498 | final BoxConstraints constraints = this.constraints; |
| 499 | if (child == null) { |
| 500 | size = constraints.smallest; |
| 501 | } else { |
| 502 | child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); |
| 503 | size = constraints.constrain(child!.size); |
| 504 | } |
| 505 | |
| 506 | if (offset.hasPixels) { |
| 507 | if (offset.pixels > _maxScrollExtent) { |
| 508 | offset.correctBy(_maxScrollExtent - offset.pixels); |
| 509 | } else if (offset.pixels < _minScrollExtent) { |
| 510 | offset.correctBy(_minScrollExtent - offset.pixels); |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | offset.applyViewportDimension(_viewportExtent); |
| 515 | offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
| 516 | } |
| 517 | |
| 518 | Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); |
| 519 | |
| 520 | Offset _paintOffsetForPosition(double position) { |
| 521 | return switch (axisDirection) { |
| 522 | AxisDirection.up => Offset(0.0, position - child!.size.height + size.height), |
| 523 | AxisDirection.left => Offset(position - child!.size.width + size.width, 0.0), |
| 524 | AxisDirection.right => Offset(-position, 0.0), |
| 525 | AxisDirection.down => Offset(0.0, -position), |
| 526 | }; |
| 527 | } |
| 528 | |
| 529 | bool _shouldClipAtPaintOffset(Offset paintOffset) { |
| 530 | assert(child != null); |
| 531 | switch (clipBehavior) { |
| 532 | case Clip.none: |
| 533 | return false; |
| 534 | case Clip.hardEdge: |
| 535 | case Clip.antiAlias: |
| 536 | case Clip.antiAliasWithSaveLayer: |
| 537 | return paintOffset.dx < 0 || |
| 538 | paintOffset.dy < 0 || |
| 539 | paintOffset.dx + child!.size.width > size.width || |
| 540 | paintOffset.dy + child!.size.height > size.height; |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | @override |
| 545 | void paint(PaintingContext context, Offset offset) { |
| 546 | if (child != null) { |
| 547 | final Offset paintOffset = _paintOffset; |
| 548 | |
| 549 | void paintContents(PaintingContext context, Offset offset) { |
| 550 | context.paintChild(child!, offset + paintOffset); |
| 551 | } |
| 552 | |
| 553 | if (_shouldClipAtPaintOffset(paintOffset)) { |
| 554 | _clipRectLayer.layer = context.pushClipRect( |
| 555 | needsCompositing, |
| 556 | offset, |
| 557 | Offset.zero & size, |
| 558 | paintContents, |
| 559 | clipBehavior: clipBehavior, |
| 560 | oldLayer: _clipRectLayer.layer, |
| 561 | ); |
| 562 | } else { |
| 563 | _clipRectLayer.layer = null; |
| 564 | paintContents(context, offset); |
| 565 | } |
| 566 | } |
| 567 | } |
| 568 | |
| 569 | final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); |
| 570 | |
| 571 | @override |
| 572 | void dispose() { |
| 573 | _clipRectLayer.layer = null; |
| 574 | super.dispose(); |
| 575 | } |
| 576 | |
| 577 | @override |
| 578 | void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| 579 | final Offset paintOffset = _paintOffset; |
| 580 | transform.translateByDouble(paintOffset.dx, paintOffset.dy, 0, 1); |
| 581 | } |
| 582 | |
| 583 | @override |
| 584 | Rect? describeApproximatePaintClip(RenderObject? child) { |
| 585 | if (child != null && _shouldClipAtPaintOffset(_paintOffset)) { |
| 586 | return Offset.zero & size; |
| 587 | } |
| 588 | return null; |
| 589 | } |
| 590 | |
| 591 | @override |
| 592 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
| 593 | if (child != null) { |
| 594 | return result.addWithPaintOffset( |
| 595 | offset: _paintOffset, |
| 596 | position: position, |
| 597 | hitTest: (BoxHitTestResult result, Offset transformed) { |
| 598 | assert(transformed == position + -_paintOffset); |
| 599 | return child!.hitTest(result, position: transformed); |
| 600 | }, |
| 601 | ); |
| 602 | } |
| 603 | return false; |
| 604 | } |
| 605 | |
| 606 | @override |
| 607 | RevealedOffset getOffsetToReveal( |
| 608 | RenderObject target, |
| 609 | double alignment, { |
| 610 | Rect? rect, |
| 611 | Axis? axis, |
| 612 | }) { |
| 613 | // One dimensional viewport has only one axis, override if it was |
| 614 | // provided/may be mismatched. |
| 615 | axis = this.axis; |
| 616 | |
| 617 | rect ??= target.paintBounds; |
| 618 | if (target is! RenderBox) { |
| 619 | return RevealedOffset(offset: offset.pixels, rect: rect); |
| 620 | } |
| 621 | |
| 622 | final RenderBox targetBox = target; |
| 623 | final Matrix4 transform = targetBox.getTransformTo(child); |
| 624 | final Rect bounds = MatrixUtils.transformRect(transform, rect); |
| 625 | final Size contentSize = child!.size; |
| 626 | |
| 627 | final ( |
| 628 | double mainAxisExtent, |
| 629 | double leadingScrollOffset, |
| 630 | double targetMainAxisExtent, |
| 631 | ) = switch (axisDirection) { |
| 632 | AxisDirection.up => (size.height, contentSize.height - bounds.bottom, bounds.height), |
| 633 | AxisDirection.left => (size.width, contentSize.width - bounds.right, bounds.width), |
| 634 | AxisDirection.right => (size.width, bounds.left, bounds.width), |
| 635 | AxisDirection.down => (size.height, bounds.top, bounds.height), |
| 636 | }; |
| 637 | |
| 638 | final double targetOffset = |
| 639 | leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; |
| 640 | final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); |
| 641 | return RevealedOffset(offset: targetOffset, rect: targetRect); |
| 642 | } |
| 643 | |
| 644 | @override |
| 645 | void showOnScreen({ |
| 646 | RenderObject? descendant, |
| 647 | Rect? rect, |
| 648 | Duration duration = Duration.zero, |
| 649 | Curve curve = Curves.ease, |
| 650 | }) { |
| 651 | if (!offset.allowImplicitScrolling) { |
| 652 | return super.showOnScreen( |
| 653 | descendant: descendant, |
| 654 | rect: rect, |
| 655 | duration: duration, |
| 656 | curve: curve, |
| 657 | ); |
| 658 | } |
| 659 | |
| 660 | final Rect? newRect = RenderViewportBase.showInViewport( |
| 661 | descendant: descendant, |
| 662 | viewport: this, |
| 663 | offset: offset, |
| 664 | rect: rect, |
| 665 | duration: duration, |
| 666 | curve: curve, |
| 667 | ); |
| 668 | super.showOnScreen(rect: newRect, duration: duration, curve: curve); |
| 669 | } |
| 670 | |
| 671 | @override |
| 672 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 673 | super.debugFillProperties(properties); |
| 674 | properties.add(DiagnosticsProperty<Offset>('offset' , _paintOffset)); |
| 675 | } |
| 676 | |
| 677 | @override |
| 678 | Rect describeSemanticsClip(RenderObject child) { |
| 679 | final double remainingOffset = _maxScrollExtent - offset.pixels; |
| 680 | switch (axisDirection) { |
| 681 | case AxisDirection.up: |
| 682 | return Rect.fromLTRB( |
| 683 | semanticBounds.left, |
| 684 | semanticBounds.top - remainingOffset, |
| 685 | semanticBounds.right, |
| 686 | semanticBounds.bottom + offset.pixels, |
| 687 | ); |
| 688 | case AxisDirection.right: |
| 689 | return Rect.fromLTRB( |
| 690 | semanticBounds.left - offset.pixels, |
| 691 | semanticBounds.top, |
| 692 | semanticBounds.right + remainingOffset, |
| 693 | semanticBounds.bottom, |
| 694 | ); |
| 695 | case AxisDirection.down: |
| 696 | return Rect.fromLTRB( |
| 697 | semanticBounds.left, |
| 698 | semanticBounds.top - offset.pixels, |
| 699 | semanticBounds.right, |
| 700 | semanticBounds.bottom + remainingOffset, |
| 701 | ); |
| 702 | case AxisDirection.left: |
| 703 | return Rect.fromLTRB( |
| 704 | semanticBounds.left - remainingOffset, |
| 705 | semanticBounds.top, |
| 706 | semanticBounds.right + offset.pixels, |
| 707 | semanticBounds.bottom, |
| 708 | ); |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | |