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';
8library;
9
10import 'dart:math' as math;
11
12import 'package:flutter/gestures.dart' show DragStartBehavior;
13import 'package:flutter/rendering.dart';
14
15import 'basic.dart';
16import 'focus_manager.dart';
17import 'focus_scope.dart';
18import 'framework.dart';
19import 'notification_listener.dart';
20import 'primary_scroll_controller.dart';
21import 'scroll_configuration.dart';
22import 'scroll_controller.dart';
23import 'scroll_notification.dart';
24import 'scroll_physics.dart';
25import 'scroll_view.dart';
26import '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.
147class 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
306class _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
342class _SingleChildViewportElement extends SingleChildRenderObjectElement
343 with NotifiableElementMixin, ViewportElementMixin {
344 _SingleChildViewportElement(_SingleChildViewport super.widget);
345}
346
347class _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