diff --git a/packages/flutter/lib/src/rendering/list_wheel_viewport.dart b/packages/flutter/lib/src/rendering/list_wheel_viewport.dart index a1a6abb038267..9028214759ce6 100644 --- a/packages/flutter/lib/src/rendering/list_wheel_viewport.dart +++ b/packages/flutter/lib/src/rendering/list_wheel_viewport.dart @@ -828,22 +828,41 @@ class RenderListWheelViewport /// Paints all children visible in the current viewport. void _paintVisibleChildren(PaintingContext context, Offset offset) { + // The magnifier cannot be turned off if the opacity is less than 1.0. + if (overAndUnderCenterOpacity >= 1) { + _paintAllChildren(context, offset); + return; + } + + // In order to reduce the number of opacity layers, we first paint all + // partially opaque children, then finally paint the fully opaque children. + context.pushOpacity(offset, (overAndUnderCenterOpacity * 255).round(), (PaintingContext context, Offset offset) { + _paintAllChildren(context, offset, center: false); + }); + _paintAllChildren(context, offset, center: true); + } + + void _paintAllChildren(PaintingContext context, Offset offset, { bool? center }) { RenderBox? childToPaint = firstChild; while (childToPaint != null) { final ListWheelParentData childParentData = childToPaint.parentData! as ListWheelParentData; - _paintTransformedChild(childToPaint, context, offset, childParentData.offset); + _paintTransformedChild(childToPaint, context, offset, childParentData.offset, center: center); childToPaint = childAfter(childToPaint); } } - /// Takes in a child with a **scrollable layout offset** and paints it in the - /// **transformed cylindrical space viewport painting coordinates**. + // Takes in a child with a **scrollable layout offset** and paints it in the + // **transformed cylindrical space viewport painting coordinates**. + // + // The value of `center` is passed through to _paintChildWithMagnifier only + // if the magnifier is enabled and/or opacity is < 1.0. void _paintTransformedChild( RenderBox child, PaintingContext context, Offset offset, - Offset layoutOffset, - ) { + Offset layoutOffset, { + required bool? center, + }) { final Offset untransformedPaintingCoordinates = offset + Offset( layoutOffset.dx, @@ -876,22 +895,35 @@ class RenderListWheelViewport final bool shouldApplyOffCenterDim = overAndUnderCenterOpacity < 1; if (useMagnifier || shouldApplyOffCenterDim) { - _paintChildWithMagnifier(context, offset, child, transform, offsetToCenter, untransformedPaintingCoordinates); + _paintChildWithMagnifier(context, offset, child, transform, offsetToCenter, untransformedPaintingCoordinates, center: center); } else { + assert(center == null); _paintChildCylindrically(context, offset, child, transform, offsetToCenter); } } - /// Paint child with the magnifier active - the child will be rendered - /// differently if it intersects with the magnifier. + // Paint child with the magnifier active - the child will be rendered + // differently if it intersects with the magnifier. + // + // `center` controls how items that partially intersect the center magnifier + // are rendered. If `center` is false, items are only painted cynlindrically. + // If `center` is true, only the clipped magnifier items are painted. + // If `center` is null, partially intersecting items are painted both as the + // magnifier and cynlidrical item, while non-intersecting items are painted + // only cylindrically. + // + // This property is used to lift the opacity that would be applied to each + // cylindrical item into a single layer, reducing the rendering cost of the + // pickers which use this viewport. void _paintChildWithMagnifier( PaintingContext context, Offset offset, RenderBox child, Matrix4 cylindricalTransform, Offset offsetToCenter, - Offset untransformedPaintingCoordinates, - ) { + Offset untransformedPaintingCoordinates, { + required bool? center, + }) { final double magnifierTopLinePosition = size.height / 2 - _itemExtent * _magnification / 2; final double magnifierBottomLinePosition = @@ -902,27 +934,28 @@ class RenderListWheelViewport final bool isBeforeMagnifierBottomLine = untransformedPaintingCoordinates.dy <= magnifierBottomLinePosition; + final Rect centerRect = Rect.fromLTWH( + 0.0, + magnifierTopLinePosition, + size.width, + _itemExtent * _magnification, + ); + final Rect topHalfRect = Rect.fromLTWH( + 0.0, + 0.0, + size.width, + magnifierTopLinePosition, + ); + final Rect bottomHalfRect = Rect.fromLTWH( + 0.0, + magnifierBottomLinePosition, + size.width, + magnifierTopLinePosition, + ); // Some part of the child is in the center magnifier. - if (isAfterMagnifierTopLine && isBeforeMagnifierBottomLine) { - final Rect centerRect = Rect.fromLTWH( - 0.0, - magnifierTopLinePosition, - size.width, - _itemExtent * _magnification, - ); - final Rect topHalfRect = Rect.fromLTWH( - 0.0, - 0.0, - size.width, - magnifierTopLinePosition, - ); - final Rect bottomHalfRect = Rect.fromLTWH( - 0.0, - magnifierBottomLinePosition, - size.width, - magnifierTopLinePosition, - ); + final bool inCenter = isAfterMagnifierTopLine && isBeforeMagnifierBottomLine; + if ((center == null || center) && inCenter) { // Clipping the part in the center. context.pushClipRect( needsCompositing, @@ -939,8 +972,10 @@ class RenderListWheelViewport ); }, ); + } - // Clipping the part in either the top-half or bottom-half of the wheel. + // Clipping the part in either the top-half or bottom-half of the wheel. + if ((center == null || !center) && inCenter) { context.pushClipRect( needsCompositing, offset, @@ -948,16 +983,18 @@ class RenderListWheelViewport ? topHalfRect : bottomHalfRect, (PaintingContext context, Offset offset) { - _paintChildCylindrically( - context, - offset, - child, - cylindricalTransform, - offsetToCenter, - ); + _paintChildCylindrically( + context, + offset, + child, + cylindricalTransform, + offsetToCenter, + ); }, ); - } else { + } + + if ((center == null || !center) && !inCenter) { _paintChildCylindrically( context, offset, @@ -987,17 +1024,12 @@ class RenderListWheelViewport ); } - // Paint child cylindrically, with [overAndUnderCenterOpacity]. - void opacityPainter(PaintingContext context, Offset offset) { - context.pushOpacity(offset, (overAndUnderCenterOpacity * 255).round(), painter); - } - context.pushTransform( needsCompositing, offset, _centerOriginTransform(cylindricalTransform), // Pre-transform painting function. - overAndUnderCenterOpacity == 1 ? painter : opacityPainter, + painter, ); final ListWheelParentData childParentData = child.parentData! as ListWheelParentData; diff --git a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart index c904a60d0b712..89e64cdb63f78 100644 --- a/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart +++ b/packages/flutter/test/widgets/list_wheel_scroll_view_test.dart @@ -1608,4 +1608,19 @@ void main() { expect(pageController.page, 1.0); }); }); + + testWidgets('ListWheelScrollView creates only one opacity layer for all children', (WidgetTester tester) async { + await tester.pumpWidget( + ListWheelScrollView( + overAndUnderCenterOpacity: 0.5, + itemExtent: 20.0, + children: [ + for (int i = 0; i < 20; i++) + Container(), + ], + ), + ); + + expect(tester.layers.whereType(), hasLength(1)); + }); }