Unverified Commit d1d426e5 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[cupertino] improve cupertino picker performance by using at most one opacity layer (#124719)

Fixes https://github.com/flutter/flutter/issues/124703

Rather than applying an opacity layer per picker item, apply them to all partially opaque picker items at once. This dramatically improves performance for impeller, by reducing the number of required subpasses and texture allocations signficantly.

Before:
Doesn't finish, 100s of passes

After:

![image](https://user-images.githubusercontent.com/8975114/231569280-91f55c9a-53a5-4b75-8728-59a4dceebe81.png)

![image](https://user-images.githubusercontent.com/8975114/231569309-7c82e5ff-46c7-4f00-80f0-9b4096aa4b14.png)

See also:

https://github.com/flutter/flutter/issues/124658
parent 68ec71f4
......@@ -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;
......
......@@ -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: <Widget>[
for (int i = 0; i < 20; i++)
Container(),
],
),
);
expect(tester.layers.whereType<OpacityLayer>(), hasLength(1));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment