diff --git a/packages/flutter/lib/src/rendering/sliver_fill.dart b/packages/flutter/lib/src/rendering/sliver_fill.dart index b4eb76188f..b0b821d445 100644 --- a/packages/flutter/lib/src/rendering/sliver_fill.dart +++ b/packages/flutter/lib/src/rendering/sliver_fill.dart @@ -57,41 +57,6 @@ class RenderSliverFillViewport extends RenderSliverFixedExtentBoxAdaptor { _viewportFraction = value; markNeedsLayout(); } - - double get _padding => (1.0 - viewportFraction) * constraints.viewportMainAxisExtent * 0.5; - - @override - double indexToLayoutOffset(double itemExtent, int index) { - return _padding + super.indexToLayoutOffset(itemExtent, index); - } - - @override - int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - return super.getMinChildIndexForScrollOffset(math.max(scrollOffset - _padding, 0.0), itemExtent); - } - - @override - int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) { - return super.getMaxChildIndexForScrollOffset(math.max(scrollOffset - _padding, 0.0), itemExtent); - } - - @override - double estimateMaxScrollOffset( - SliverConstraints constraints, { - int firstIndex, - int lastIndex, - double leadingScrollOffset, - double trailingScrollOffset, - }) { - final double padding = _padding; - return childManager.estimateMaxScrollOffset( - constraints, - firstIndex: firstIndex, - lastIndex: lastIndex, - leadingScrollOffset: leadingScrollOffset - padding, - trailingScrollOffset: trailingScrollOffset - padding, - ) + padding + padding; - } } /// A sliver that contains a single box child that fills the remaining space in diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart index 5a607fbd51..7e6b2d1256 100644 --- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart @@ -195,7 +195,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda if (firstChild == null) { if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) { // There are either no children, or we are past the end of all our children. - // If it is the later, we will need to find the first available child. + // If it is the latter, we will need to find the first available child. double max; if (childManager.childCount != null) { max = computeMaxScrollOffset(constraints, itemExtent); diff --git a/packages/flutter/lib/src/rendering/sliver_padding.dart b/packages/flutter/lib/src/rendering/sliver_padding.dart index 01cf8a275f..56b53cbf17 100644 --- a/packages/flutter/lib/src/rendering/sliver_padding.dart +++ b/packages/flutter/lib/src/rendering/sliver_padding.dart @@ -12,73 +12,27 @@ import 'debug.dart'; import 'object.dart'; import 'sliver.dart'; -/// Inset a [RenderSliver], applying padding on each side. +/// Insets a [RenderSliver] by applying [resolvedPadding] on each side. /// -/// A [RenderSliverPadding] object wraps the [SliverGeometry.layoutExtent] of -/// its child. Any incoming [SliverConstraints.overlap] is ignored and not +/// A [RenderSliverEdgeInsetsPadding] subclass wraps the [SliverGeometry.layoutExtent] +/// of its child. Any incoming [SliverConstraints.overlap] is ignored and not /// passed on to the child. /// +/// {@template flutter.rendering.sliverPadding.limitation} /// Applying padding to anything but the most mundane sliver is likely to have -/// undesired effects. For example, wrapping a -/// [RenderSliverPinnedPersistentHeader] will cause the app bar to overlap -/// earlier slivers (contrary to the normal behavior of pinned app bars), and -/// while the app bar is pinned, the padding will scroll away. -class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin { - /// Creates a render object that insets its child in a viewport. - /// - /// The [padding] argument must not be null and must have non-negative insets. - RenderSliverPadding({ - @required EdgeInsetsGeometry padding, - TextDirection textDirection, - RenderSliver child, - }) : assert(padding != null), - assert(padding.isNonNegative), - _padding = padding, - _textDirection = textDirection { - this.child = child; - } - - EdgeInsets _resolvedPadding; - - void _resolve() { - if (_resolvedPadding != null) - return; - _resolvedPadding = padding.resolve(textDirection); - assert(_resolvedPadding.isNonNegative); - } - - void _markNeedResolution() { - _resolvedPadding = null; - markNeedsLayout(); - } - +/// undesired effects. For example, wrapping a [RenderSliverPinnedPersistentHeader] +/// will cause the app bar to overlap earlier slivers (contrary to the normal +/// behavior of pinned app bars), and while the app bar is pinned, the padding +/// will scroll away. +/// {@endtemplate} +abstract class RenderSliverEdgeInsetsPadding extends RenderSliver with RenderObjectWithChildMixin { /// The amount to pad the child in each dimension. /// - /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] - /// must not be null. - EdgeInsetsGeometry get padding => _padding; - EdgeInsetsGeometry _padding; - set padding(EdgeInsetsGeometry value) { - assert(value != null); - assert(padding.isNonNegative); - if (_padding == value) - return; - _padding = value; - _markNeedResolution(); - } - - /// The text direction with which to resolve [padding]. + /// The offsets are specified in terms of visual edges, left, top, right, and + /// bottom. These values are not affected by the [TextDirection]. /// - /// This may be changed to null, but only after the [padding] has been changed - /// to a value that does not depend on the direction. - TextDirection get textDirection => _textDirection; - TextDirection _textDirection; - set textDirection(TextDirection value) { - if (_textDirection == value) - return; - _textDirection = value; - _markNeedResolution(); - } + /// Must not be null or contain negative values when [performLayout] is called. + EdgeInsets get resolvedPadding; /// The padding in the scroll direction on the side nearest the 0.0 scroll direction. /// @@ -88,16 +42,16 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin _resolvedPadding; + EdgeInsets _resolvedPadding; + + void _resolve() { + if (resolvedPadding != null) + return; + _resolvedPadding = padding.resolve(textDirection); + assert(resolvedPadding.isNonNegative); + } + + void _markNeedsResolution() { + _resolvedPadding = null; + markNeedsLayout(); + } + + /// The amount to pad the child in each dimension. + /// + /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] + /// must not be null. + EdgeInsetsGeometry get padding => _padding; + EdgeInsetsGeometry _padding; + set padding(EdgeInsetsGeometry value) { + assert(value != null); + assert(padding.isNonNegative); + if (_padding == value) + return; + _padding = value; + _markNeedsResolution(); + } + + /// The text direction with which to resolve [padding]. + /// + /// This may be changed to null, but only after the [padding] has been changed + /// to a value that does not depend on the direction. + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (_textDirection == value) + return; + _textDirection = value; + _markNeedsResolution(); + } + + @override + void performLayout() { + _resolve(); + super.performLayout(); + } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 213c63d6fe..347a019d28 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -345,8 +345,16 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri forcePixels(getPixelsFromPage(oldPage)); } + // The amount of offset that will be added to [minScrollExtent] and subtracted + // from [maxScrollExtent], such that every page will properly snap to the center + // of the viewport when viewportFraction is greater than 1. + // + // The value is 0 if viewportFraction is less than or equal to 1, larger than 0 + // otherwise. + double get _initialPageOffset => math.max(0, viewportDimension * (viewportFraction - 1) / 2); + double getPageFromPixels(double pixels, double viewportDimension) { - final double actual = math.max(0.0, pixels) / math.max(1.0, viewportDimension * viewportFraction); + final double actual = math.max(0.0, pixels - _initialPageOffset) / math.max(1.0, viewportDimension * viewportFraction); final double round = actual.roundToDouble(); if ((actual - round).abs() < precisionErrorTolerance) { return round; @@ -355,7 +363,7 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri } double getPixelsFromPage(double page) { - return page * viewportDimension * viewportFraction; + return page * viewportDimension * viewportFraction + _initialPageOffset; } @override @@ -396,6 +404,15 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri return result; } + @override + bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { + final double newMinScrollExtent = minScrollExtent + _initialPageOffset; + return super.applyContentDimensions( + newMinScrollExtent, + math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset), + ); + } + @override PageMetrics copyWith({ double minScrollExtent, diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 48e314785b..43b3d0ebae 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -725,6 +725,7 @@ abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget { }) : assert(delegate != null), super(key: key); + /// {@template flutter.widgets.sliverMultiBoxAdaptor.delegate} /// The delegate that provides the children for this widget. /// /// The children are constructed lazily using this delegate to avoid creating @@ -735,6 +736,7 @@ abstract class SliverMultiBoxAdaptorWidget extends SliverWithKeepAliveWidget { /// * [SliverChildBuilderDelegate] and [SliverChildListDelegate], which are /// commonly used subclasses of [SliverChildDelegate] that use a builder /// callback and an explicit child list, respectively. + /// {@endtemplate} final SliverChildDelegate delegate; @override @@ -1023,7 +1025,7 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget { } } -/// A sliver that contains a multiple box children that each fill the viewport. +/// A sliver that contains multiple box children that each fills the viewport. /// /// [SliverFillViewport] places its children in a linear array along the main /// axis. Each child is sized to fill the viewport, both in the main and cross @@ -1038,9 +1040,40 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget { /// the main axis extent of each item. /// * [SliverList], which does not require its children to have the same /// extent in the main axis. -class SliverFillViewport extends SliverMultiBoxAdaptorWidget { +class SliverFillViewport extends StatelessWidget { /// Creates a sliver whose box children that each fill the viewport. const SliverFillViewport({ + Key key, + @required this.delegate, + this.viewportFraction = 1.0, + }) : assert(viewportFraction != null), + assert(viewportFraction > 0.0), + super(key: key); + + /// The fraction of the viewport that each child should fill in the main axis. + /// + /// If this fraction is less than 1.0, more than one child will be visible at + /// once. If this fraction is greater than 1.0, each child will be larger than + /// the viewport in the main axis. + final double viewportFraction; + + /// {@macro flutter.widgets.sliverMultiBoxAdaptor.delegate} + final SliverChildDelegate delegate; + + @override + Widget build(BuildContext context) { + return _SliverFractionalPadding( + viewportFraction: (1 - viewportFraction).clamp(0, 1) / 2, + sliver: _SliverFillViewportRenderObjectWidget( + viewportFraction: viewportFraction, + delegate: delegate, + ), + ); + } +} + +class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget { + const _SliverFillViewportRenderObjectWidget({ Key key, @required SliverChildDelegate delegate, this.viewportFraction = 1.0, @@ -1048,11 +1081,6 @@ class SliverFillViewport extends SliverMultiBoxAdaptorWidget { assert(viewportFraction > 0.0), super(key: key, delegate: delegate); - /// The fraction of the viewport that each child should fill in the main axis. - /// - /// If this fraction is less than 1.0, more than one child will be visible at - /// once. If this fraction is greater than 1.0, each child will be larger than - /// the viewport in the main axis. final double viewportFraction; @override @@ -1067,6 +1095,77 @@ class SliverFillViewport extends SliverMultiBoxAdaptorWidget { } } +class _SliverFractionalPadding extends SingleChildRenderObjectWidget { + const _SliverFractionalPadding({ + this.viewportFraction = 0, + Widget sliver, + }) : assert(viewportFraction != null), + assert(viewportFraction >= 0), + assert(viewportFraction <= 0.5), + super(child: sliver); + + final double viewportFraction; + + @override + RenderObject createRenderObject(BuildContext context) => _RenderSliverFractionalPadding(viewportFraction: viewportFraction); + + @override + void updateRenderObject(BuildContext context, _RenderSliverFractionalPadding renderObject) { + renderObject.viewportFraction = viewportFraction; + } +} + +class _RenderSliverFractionalPadding extends RenderSliverEdgeInsetsPadding { + _RenderSliverFractionalPadding({ + double viewportFraction = 0, + }) : assert(viewportFraction != null), + assert(viewportFraction <= 0.5), + assert(viewportFraction >= 0), + _viewportFraction = viewportFraction; + + double get viewportFraction => _viewportFraction; + double _viewportFraction; + set viewportFraction(double newValue) { + assert(newValue != null); + if (_viewportFraction == newValue) + return; + _viewportFraction = newValue; + _markNeedsResolution(); + } + + @override + EdgeInsets get resolvedPadding => _resolvedPadding; + EdgeInsets _resolvedPadding; + + void _markNeedsResolution() { + _resolvedPadding = null; + markNeedsLayout(); + } + + void _resolve() { + if (_resolvedPadding != null) + return; + assert(constraints.axis != null); + final double paddingValue = constraints.viewportMainAxisExtent * viewportFraction; + switch (constraints.axis) { + case Axis.horizontal: + _resolvedPadding = EdgeInsets.symmetric(horizontal: paddingValue); + break; + case Axis.vertical: + _resolvedPadding = EdgeInsets.symmetric(vertical: paddingValue); + break; + } + + return; + } + + @override + void performLayout() { + _resolve(); + super.performLayout(); + } +} + /// An element that lazily builds children for a [SliverMultiBoxAdaptorWidget]. /// /// Implements [RenderSliverBoxChildManager], which lets this element manage diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index efee8e6aaf..c976197e42 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -532,8 +532,7 @@ void main() { }); testWidgets('PageView large viewportFraction', (WidgetTester tester) async { - final PageController controller = - PageController(viewportFraction: 5/4); + final PageController controller = PageController(viewportFraction: 5/4); Widget build(PageController controller) { return Directionality( @@ -601,6 +600,89 @@ void main() { expect(tester.getTopLeft(find.text('Hawaii')), const Offset(-(4 - 1) * 800 / 2, 0)); }); + testWidgets( + 'PageView large viewportFraction can scroll to the last page and snap', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/45096. + final PageController controller = PageController(viewportFraction: 5/4); + + Widget build(PageController controller) { + return Directionality( + textDirection: TextDirection.ltr, + child: PageView.builder( + controller: controller, + itemCount: 3, + itemBuilder: (BuildContext context, int index) { + return Container( + height: 200.0, + color: index % 2 == 0 + ? const Color(0xFF0000FF) + : const Color(0xFF00FF00), + child: Text(index.toString()), + ); + }, + ), + ); + } + + await tester.pumpWidget(build(controller)); + + expect(tester.getCenter(find.text('0')), const Offset(400, 300)); + + controller.jumpToPage(2); + await tester.pump(); + await tester.pumpAndSettle(); + + expect(tester.getCenter(find.text('2')), const Offset(400, 300)); + }); + + testWidgets( + 'All visible pages are able to receive touch events', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/23873. + final PageController controller = PageController(viewportFraction: 1/4, initialPage: 0); + int tappedIndex; + + Widget build() { + return Directionality( + textDirection: TextDirection.ltr, + child: PageView.builder( + controller: controller, + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () => tappedIndex = index, + child: SizedBox.expand(child: Text('$index')), + ); + }, + ), + ); + } + + Iterable visiblePages = const [0, 1, 2]; + await tester.pumpWidget(build()); + + // The first 3 items should be visible and tappable. + for (int index in visiblePages) { + expect(find.text(index.toString()), findsOneWidget); + // The center of page 2's x-coordinate is 800, so we have to manually + // offset it a bit to make sure the tap lands within the screen. + final Offset center = tester.getCenter(find.text('$index')) - const Offset(3, 0); + await tester.tapAt(center); + expect(tappedIndex, index); + } + + controller.jumpToPage(19); + await tester.pump(); + // The last 3 items should be visible and tappable. + visiblePages = const [17, 18, 19]; + for (int index in visiblePages) { + expect(find.text('$index'), findsOneWidget); + await tester.tap(find.text('$index')); + expect(tappedIndex, index); + } + }); + testWidgets('PageView does not report page changed on overscroll', (WidgetTester tester) async { final PageController controller = PageController( initialPage: kStates.length - 1, diff --git a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart index 0a6683b7c1..12778be924 100644 --- a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart +++ b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart @@ -64,7 +64,7 @@ void main() { expect( viewport.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( - 'RenderSliverFillViewport#00000 relayoutBoundary=up1\n' + '_RenderSliverFractionalPadding#00000 relayoutBoundary=up1\n' ' │ needs compositing\n' ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: SliverConstraints(AxisDirection.down,\n' @@ -76,58 +76,71 @@ void main() { ' │ geometry: SliverGeometry(scrollExtent: 12000.0, paintExtent:\n' ' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true,\n' ' │ cacheExtent: 850.0)\n' - ' │ currently live children: 0 to 1\n' ' │\n' - ' ├─child with index 0: RenderRepaintBoundary#00000\n' - ' │ │ needs compositing\n' - ' │ │ parentData: index=0; layoutOffset=0.0\n' - ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' │ │ layer: OffsetLayer#00000\n' - ' │ │ size: Size(800.0, 600.0)\n' - ' │ │ metrics: 66.7% useful (1 bad vs 2 good)\n' - ' │ │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ │ repaints)\n' - ' │ │\n' - ' │ └─child: RenderParagraph#00000\n' - ' │ │ parentData: (can use size)\n' - ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' │ │ semantics node: SemanticsNode#2\n' - ' │ │ size: Size(800.0, 600.0)\n' - ' │ │ textAlign: start\n' - ' │ │ textDirection: ltr\n' - ' │ │ softWrap: wrapping at box width\n' - ' │ │ overflow: clip\n' - ' │ │ maxLines: unlimited\n' - ' │ ╘═╦══ text ═══\n' - ' │ ║ TextSpan:\n' - ' │ ║ \n' - ' │ ║ "0"\n' - ' │ ╚═══════════\n' - ' └─child with index 1: RenderRepaintBoundary#00000\n' + ' └─child: RenderSliverFillViewport#00000 relayoutBoundary=up2\n' ' │ needs compositing\n' - ' │ parentData: index=1; layoutOffset=600.0\n' - ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' │ layer: OffsetLayer#00000 DETACHED\n' - ' │ size: Size(800.0, 600.0)\n' - ' │ metrics: 50.0% useful (1 bad vs 1 good)\n' - ' │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ repaints)\n' + ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' + ' │ constraints: SliverConstraints(AxisDirection.down,\n' + ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n' + ' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n' + ' │ crossAxisDirection: AxisDirection.right,\n' + ' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0\n' + ' │ cacheOrigin: 0.0 )\n' + ' │ geometry: SliverGeometry(scrollExtent: 12000.0, paintExtent:\n' + ' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true,\n' + ' │ cacheExtent: 850.0)\n' + ' │ currently live children: 0 to 1\n' ' │\n' - ' └─child: RenderParagraph#00000\n' - ' │ parentData: (can use size)\n' + ' ├─child with index 0: RenderRepaintBoundary#00000\n' + ' │ │ needs compositing\n' + ' │ │ parentData: index=0; layoutOffset=0.0\n' + ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' + ' │ │ layer: OffsetLayer#00000\n' + ' │ │ size: Size(800.0, 600.0)\n' + ' │ │ metrics: 66.7% useful (1 bad vs 2 good)\n' + ' │ │ diagnosis: insufficient data to draw conclusion (less than five\n' + ' │ │ repaints)\n' + ' │ │\n' + ' │ └─child: RenderParagraph#00000\n' + ' │ │ parentData: (can use size)\n' + ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' + ' │ │ semantics node: SemanticsNode#2\n' + ' │ │ size: Size(800.0, 600.0)\n' + ' │ │ textAlign: start\n' + ' │ │ textDirection: ltr\n' + ' │ │ softWrap: wrapping at box width\n' + ' │ │ overflow: clip\n' + ' │ │ maxLines: unlimited\n' + ' │ ╘═╦══ text ═══\n' + ' │ ║ TextSpan:\n' + ' │ ║ \n' + ' │ ║ "0"\n' + ' │ ╚═══════════\n' + ' └─child with index 1: RenderRepaintBoundary#00000\n' + ' │ needs compositing\n' + ' │ parentData: index=1; layoutOffset=600.0\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' - ' │ semantics node: SemanticsNode#3\n' + ' │ layer: OffsetLayer#00000 DETACHED\n' ' │ size: Size(800.0, 600.0)\n' - ' │ textAlign: start\n' - ' │ textDirection: ltr\n' - ' │ softWrap: wrapping at box width\n' - ' │ overflow: clip\n' - ' │ maxLines: unlimited\n' - ' ╘═╦══ text ═══\n' - ' ║ TextSpan:\n' - ' ║ \n' - ' ║ "1"\n' - ' ╚═══════════\n' + ' │ metrics: 50.0% useful (1 bad vs 1 good)\n' + ' │ diagnosis: insufficient data to draw conclusion (less than five\n' + ' │ repaints)\n' + ' │\n' + ' └─child: RenderParagraph#00000\n' + ' │ parentData: (can use size)\n' + ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' + ' │ semantics node: SemanticsNode#3\n' + ' │ size: Size(800.0, 600.0)\n' + ' │ textAlign: start\n' + ' │ textDirection: ltr\n' + ' │ softWrap: wrapping at box width\n' + ' │ overflow: clip\n' + ' │ maxLines: unlimited\n' + ' ╘═╦══ text ═══\n' + ' ║ TextSpan:\n' + ' ║ \n' + ' ║ "1"\n' + ' ╚═══════════\n' '' ), );