Unverified Commit e766ae74 authored by Dan Field's avatar Dan Field Committed by GitHub

Automatically caching viewport (#45327)

parent 84ce3f60
...@@ -15,6 +15,14 @@ import 'object.dart'; ...@@ -15,6 +15,14 @@ import 'object.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
/// The unit of measurement for a [Viewport.cacheExtent].
enum CacheExtentStyle {
/// Treat the [Viewport.cacheExtent] as logical pixels.
pixel,
/// Treat the [Viewport.cacheExtent] as a multiplier of the main axis extent.
viewport,
}
/// An interface for render objects that are bigger on the inside. /// An interface for render objects that are bigger on the inside.
/// ///
/// Some render objects, such as [RenderViewport], present a portion of their /// Some render objects, such as [RenderViewport], present a portion of their
...@@ -75,6 +83,7 @@ abstract class RenderAbstractViewport extends RenderObject { ...@@ -75,6 +83,7 @@ abstract class RenderAbstractViewport extends RenderObject {
/// ///
/// * [RenderViewportBase.cacheExtent] for a definition of the cache extent. /// * [RenderViewportBase.cacheExtent] for a definition of the cache extent.
@protected @protected
@visibleForTesting
static const double defaultCacheExtent = 250.0; static const double defaultCacheExtent = 250.0;
} }
...@@ -160,14 +169,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -160,14 +169,18 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@required AxisDirection crossAxisDirection, @required AxisDirection crossAxisDirection,
@required ViewportOffset offset, @required ViewportOffset offset,
double cacheExtent, double cacheExtent,
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
}) : assert(axisDirection != null), }) : assert(axisDirection != null),
assert(crossAxisDirection != null), assert(crossAxisDirection != null),
assert(offset != null), assert(offset != null),
assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)), assert(axisDirectionToAxis(axisDirection) != axisDirectionToAxis(crossAxisDirection)),
assert(cacheExtentStyle != null),
assert(cacheExtent != null || cacheExtentStyle == CacheExtentStyle.pixel),
_axisDirection = axisDirection, _axisDirection = axisDirection,
_crossAxisDirection = crossAxisDirection, _crossAxisDirection = crossAxisDirection,
_offset = offset, _offset = offset,
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent; _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
_cacheExtentStyle = cacheExtentStyle;
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
...@@ -272,6 +285,34 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -272,6 +285,34 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
markNeedsLayout(); markNeedsLayout();
} }
/// This value is set during layout based on the [CacheExtentStyle].
///
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
/// of the viewport multiplied by the requested cache extent, which is still
/// expressed in pixels.
double _calculatedCacheExtent;
/// {@template flutter.rendering.viewport.cacheExtentStyle}
/// Controls how the [cacheExtent] is interpreted.
///
/// If set to [CacheExtentStyle.pixels], the [cacheExtent] will be treated as
/// a logical pixels.
///
/// If set to [CacheExtentStyle.viewport], the [cacheExtent] will be treated
/// as a multiplier for the main axis extent of the viewport. In this case,
/// the [cacheExtent] must not be null.
/// {@endtemplate}
CacheExtentStyle get cacheExtentStyle => _cacheExtentStyle;
CacheExtentStyle _cacheExtentStyle;
set cacheExtentStyle(CacheExtentStyle value) {
assert(value != null);
if (value == _cacheExtentStyle) {
return;
}
_cacheExtentStyle = value;
markNeedsLayout();
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
...@@ -494,20 +535,25 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -494,20 +535,25 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@override @override
Rect describeSemanticsClip(RenderSliver child) { Rect describeSemanticsClip(RenderSliver child) {
assert (axis != null); assert(axis != null);
if (_calculatedCacheExtent == null) {
return semanticBounds;
}
switch (axis) { switch (axis) {
case Axis.vertical: case Axis.vertical:
return Rect.fromLTRB( return Rect.fromLTRB(
semanticBounds.left, semanticBounds.left,
semanticBounds.top - cacheExtent, semanticBounds.top - _calculatedCacheExtent,
semanticBounds.right, semanticBounds.right,
semanticBounds.bottom + cacheExtent, semanticBounds.bottom + _calculatedCacheExtent,
); );
case Axis.horizontal: case Axis.horizontal:
return Rect.fromLTRB( return Rect.fromLTRB(
semanticBounds.left - cacheExtent, semanticBounds.left - _calculatedCacheExtent,
semanticBounds.top, semanticBounds.top,
semanticBounds.right + cacheExtent, semanticBounds.right + _calculatedCacheExtent,
semanticBounds.bottom, semanticBounds.bottom,
); );
} }
...@@ -1076,11 +1122,19 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1076,11 +1122,19 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
List<RenderSliver> children, List<RenderSliver> children,
RenderSliver center, RenderSliver center,
double cacheExtent, double cacheExtent,
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
}) : assert(anchor != null), }) : assert(anchor != null),
assert(anchor >= 0.0 && anchor <= 1.0), assert(anchor >= 0.0 && anchor <= 1.0),
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
_anchor = anchor, _anchor = anchor,
_center = center, _center = center,
super(axisDirection: axisDirection, crossAxisDirection: crossAxisDirection, offset: offset, cacheExtent: cacheExtent) { super(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection,
offset: offset,
cacheExtent: cacheExtent,
cacheExtentStyle: cacheExtentStyle,
) {
addAll(children); addAll(children);
if (center == null && firstChild != null) if (center == null && firstChild != null)
_center = firstChild; _center = firstChild;
...@@ -1337,8 +1391,17 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1337,8 +1391,17 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent); final double reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent);
final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); final double forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
final double fullCacheExtent = mainAxisExtent + 2 * cacheExtent; switch (cacheExtentStyle) {
final double centerCacheOffset = centerOffset + cacheExtent; case CacheExtentStyle.pixel:
_calculatedCacheExtent = cacheExtent;
break;
case CacheExtentStyle.viewport:
_calculatedCacheExtent = mainAxisExtent * cacheExtent;
break;
}
final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent;
final double centerCacheOffset = centerOffset + _calculatedCacheExtent;
final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent); final double reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent);
final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); final double forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
...@@ -1357,7 +1420,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1357,7 +1420,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
growthDirection: GrowthDirection.reverse, growthDirection: GrowthDirection.reverse,
advance: childBefore, advance: childBefore,
remainingCacheExtent: reverseDirectionRemainingCacheExtent, remainingCacheExtent: reverseDirectionRemainingCacheExtent,
cacheOrigin: (mainAxisExtent - centerOffset).clamp(-cacheExtent, 0.0), cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent, 0.0),
); );
if (result != 0.0) if (result != 0.0)
return -result; return -result;
...@@ -1375,7 +1438,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat ...@@ -1375,7 +1438,7 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
growthDirection: GrowthDirection.forward, growthDirection: GrowthDirection.forward,
advance: childAfter, advance: childAfter,
remainingCacheExtent: forwardDirectionRemainingCacheExtent, remainingCacheExtent: forwardDirectionRemainingCacheExtent,
cacheOrigin: centerOffset.clamp(-cacheExtent, 0.0), cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent, 0.0),
); );
} }
......
...@@ -57,10 +57,13 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -57,10 +57,13 @@ class Viewport extends MultiChildRenderObjectWidget {
@required this.offset, @required this.offset,
this.center, this.center,
this.cacheExtent, this.cacheExtent,
this.cacheExtentStyle = CacheExtentStyle.pixel,
List<Widget> slivers = const <Widget>[], List<Widget> slivers = const <Widget>[],
}) : assert(offset != null), }) : assert(offset != null),
assert(slivers != null), assert(slivers != null),
assert(center == null || slivers.where((Widget child) => child.key == center).length == 1), assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),
assert(cacheExtentStyle != null),
assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
super(key: key, children: slivers); super(key: key, children: slivers);
/// The direction in which the [offset]'s [ViewportOffset.pixels] increases. /// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
...@@ -112,6 +115,9 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -112,6 +115,9 @@ class Viewport extends MultiChildRenderObjectWidget {
/// {@macro flutter.rendering.viewport.cacheExtent} /// {@macro flutter.rendering.viewport.cacheExtent}
final double cacheExtent; final double cacheExtent;
/// {@macro flutter.rendering.viewport.cacheExtentStyle}
final CacheExtentStyle cacheExtentStyle;
/// Given a [BuildContext] and an [AxisDirection], determine the correct cross /// Given a [BuildContext] and an [AxisDirection], determine the correct cross
/// axis direction. /// axis direction.
/// ///
...@@ -140,6 +146,7 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -140,6 +146,7 @@ class Viewport extends MultiChildRenderObjectWidget {
anchor: anchor, anchor: anchor,
offset: offset, offset: offset,
cacheExtent: cacheExtent, cacheExtent: cacheExtent,
cacheExtentStyle: cacheExtentStyle,
); );
} }
...@@ -150,7 +157,8 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -150,7 +157,8 @@ class Viewport extends MultiChildRenderObjectWidget {
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection) ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
..anchor = anchor ..anchor = anchor
..offset = offset ..offset = offset
..cacheExtent = cacheExtent; ..cacheExtent = cacheExtent
..cacheExtentStyle = cacheExtentStyle;
} }
@override @override
...@@ -168,6 +176,8 @@ class Viewport extends MultiChildRenderObjectWidget { ...@@ -168,6 +176,8 @@ class Viewport extends MultiChildRenderObjectWidget {
} else if (children.isNotEmpty && children.first.key != null) { } else if (children.isNotEmpty && children.first.key != null) {
properties.add(DiagnosticsProperty<Key>('center', children.first.key, tooltip: 'implicit')); properties.add(DiagnosticsProperty<Key>('center', children.first.key, tooltip: 'implicit'));
} }
properties.add(DiagnosticsProperty<double>('cacheExtent', cacheExtent));
properties.add(DiagnosticsProperty<CacheExtentStyle>('cacheExtentStyle', cacheExtentStyle));
} }
} }
......
...@@ -190,7 +190,6 @@ class TestCallbackPainter extends CustomPainter { ...@@ -190,7 +190,6 @@ class TestCallbackPainter extends CustomPainter {
bool shouldRepaint(TestCallbackPainter oldPainter) => true; bool shouldRepaint(TestCallbackPainter oldPainter) => true;
} }
class RenderSizedBox extends RenderBox { class RenderSizedBox extends RenderBox {
RenderSizedBox(this._size); RenderSizedBox(this._size);
......
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file is separate from viewport_test.dart because we can't use both
// testWidgets and rendering_tester in the same file - testWidgets will
// initialize a binding, which rendering_tester will attempt to re-initialize
// (or vice versa).
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() {
const double width = 800;
const double height = 600;
Rect rectExpandedOnAxis(double value) => Rect.fromLTRB(0.0, 0.0 - value, width, height + value);
List<RenderSliver> children;
setUp(() {
children = <RenderSliver>[
RenderSliverToBoxAdapter(
child: RenderSizedBox(const Size(800, 400)),
),
];
});
test('Cache extent - null, pixels', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
children: children,
);
layout(renderViewport, phase: EnginePhase.flushSemantics);
expect(
renderViewport.describeSemanticsClip(null),
rectExpandedOnAxis(RenderAbstractViewport.defaultCacheExtent),
);
});
test('Cache extent - 0, pixels', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: 0.0,
children: children,
);
layout(renderViewport, phase: EnginePhase.flushSemantics);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(0.0));
});
test('Cache extent - 500, pixels', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: 500.0,
children: children,
);
layout(renderViewport, phase: EnginePhase.flushSemantics);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(500.0));
});
test('Cache extent - nullx viewport', () async {
await expectLater(() => RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: null,
cacheExtentStyle: CacheExtentStyle.viewport,
children: children,
),
throwsAssertionError
);
});
test('Cache extent - 0x viewport', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
children: children,
);
layout(renderViewport);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(0));
});
test('Cache extent - .5x viewport', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: .5,
cacheExtentStyle: CacheExtentStyle.viewport,
children: children,
);
layout(renderViewport);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(height / 2));
});
test('Cache extent - 1x viewport', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: 1.0,
cacheExtentStyle: CacheExtentStyle.viewport,
children: children,
);
layout(renderViewport);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(height));
});
test('Cache extent - 2.5x viewport', () async {
final RenderViewport renderViewport = RenderViewport(
crossAxisDirection: AxisDirection.left,
offset: ViewportOffset.zero(),
cacheExtent: 2.5,
cacheExtentStyle: CacheExtentStyle.viewport,
children: children,
);
layout(renderViewport);
expect(renderViewport.describeSemanticsClip(null), rectExpandedOnAxis(height * 2.5));
});
}
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// This file is separate from viewport_caching_test.dart because we can't use
// both testWidgets and rendering_tester in the same file - testWidgets will
// initialize a binding, which rendering_tester will attempt to re-initialize
// (or vice versa).
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
......
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