blob: 7ae0aabf973e3fcc7674946779fa9cd3225c1a44 [file] [log] [blame]
// Copyright 2014 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.
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'box.dart';
import 'object.dart';
import 'viewport.dart';
import 'viewport_offset.dart';
typedef _ChildSizingFunction = double Function(RenderBox child);
/// A delegate used by [RenderListWheelViewport] to manage its children.
///
/// [RenderListWheelViewport] during layout will ask the delegate to create
/// children that are visible in the viewport and remove those that are not.
abstract class ListWheelChildManager {
/// The maximum number of children that can be provided to
/// [RenderListWheelViewport].
///
/// If non-null, the children will have index in the range [0, childCount - 1].
///
/// If null, then there's no explicit limits to the range of the children
/// except that it has to be contiguous. If [childExistsAt] for a certain
/// index returns false, that index is already past the limit.
int get childCount;
/// Checks whether the delegate is able to provide a child widget at the given
/// index.
///
/// This function is not about whether the child at the given index is
/// attached to the [RenderListWheelViewport] or not.
bool childExistsAt(int index);
/// Creates a new child at the given index and updates it to the child list
/// of [RenderListWheelViewport]. If no child corresponds to `index`, then do
/// nothing.
///
/// It is possible to create children with negative indices.
void createChild(int index, { @required RenderBox after });
/// Removes the child element corresponding with the given RenderBox.
void removeChild(RenderBox child);
}
/// [ParentData] for use with [RenderListWheelViewport].
class ListWheelParentData extends ContainerBoxParentData<RenderBox> {
/// Index of this child in its parent's child list.
int index;
}
/// Render, onto a wheel, a bigger sequential set of objects inside this viewport.
///
/// Takes a scrollable set of fixed sized [RenderBox]es and renders them
/// sequentially from top down on a vertical scrolling axis.
///
/// It starts with the first scrollable item in the center of the main axis
/// and ends with the last scrollable item in the center of the main axis. This
/// is in contrast to typical lists that start with the first scrollable item
/// at the start of the main axis and ends with the last scrollable item at the
/// end of the main axis.
///
/// Instead of rendering its children on a flat plane, it renders them
/// as if each child is broken into its own plane and that plane is
/// perpendicularly fixed onto a cylinder which rotates along the scrolling
/// axis.
///
/// This class works in 3 coordinate systems:
///
/// 1. The **scrollable layout coordinates**. This coordinate system is used to
/// communicate with [ViewportOffset] and describes its children's abstract
/// offset from the beginning of the scrollable list at (0.0, 0.0).
///
/// The list is scrollable from the start of the first child item to the
/// start of the last child item.
///
/// Children's layout coordinates don't change as the viewport scrolls.
///
/// 2. The **untransformed plane's viewport painting coordinates**. Children are
/// not painted in this coordinate system. It's an abstract intermediary used
/// before transforming into the next cylindrical coordinate system.
///
/// This system is the **scrollable layout coordinates** translated by the
/// scroll offset such that (0.0, 0.0) is the top left corner of the
/// viewport.
///
/// Because the viewport is centered at the scrollable list's scroll offset
/// instead of starting at the scroll offset, there are paintable children
/// ~1/2 viewport length before and after the scroll offset instead of ~1
/// viewport length after the scroll offset.
///
/// Children's visibility inclusion in the viewport is determined in this
/// system regardless of the cylinder's properties such as [diameterRatio]
/// or [perspective]. In other words, a 100px long viewport will always
/// paint 10-11 visible 10px children if there are enough children in the
/// viewport.
///
/// 3. The **transformed cylindrical space viewport painting coordinates**.
/// Children from system 2 get their positions transformed into a cylindrical
/// projection matrix instead of its Cartesian offset with respect to the
/// scroll offset.
///
/// Children in this coordinate system are painted.
///
/// The wheel's size and the maximum and minimum visible angles are both
/// controlled by [diameterRatio]. Children visible in the **untransformed
/// plane's viewport painting coordinates**'s viewport will be radially
/// evenly laid out between the maximum and minimum angles determined by
/// intersecting the viewport's main axis length with a cylinder whose
/// diameter is [diameterRatio] times longer, as long as those angles are
/// between -pi/2 and pi/2.
///
/// For example, if [diameterRatio] is 2.0 and this [RenderListWheelViewport]
/// is 100.0px in the main axis, then the diameter is 200.0. And children
/// will be evenly laid out between that cylinder's -arcsin(1/2) and
/// arcsin(1/2) angles.
///
/// The cylinder's 0 degree side is always centered in the
/// [RenderListWheelViewport]. The transformation from **untransformed
/// plane's viewport painting coordinates** is also done such that the child
/// in the center of that plane will be mostly untransformed with children
/// above and below it being transformed more as the angle increases.
class RenderListWheelViewport
extends RenderBox
with ContainerRenderObjectMixin<RenderBox, ListWheelParentData>
implements RenderAbstractViewport {
/// Creates a [RenderListWheelViewport] which renders children on a wheel.
///
/// All arguments must not be null. Optional arguments have reasonable defaults.
RenderListWheelViewport({
@required this.childManager,
@required ViewportOffset offset,
double diameterRatio = defaultDiameterRatio,
double perspective = defaultPerspective,
double offAxisFraction = 0,
bool useMagnifier = false,
double magnification = 1,
double overAndUnderCenterOpacity = 1,
@required double itemExtent,
double squeeze = 1,
bool clipToSize = true,
bool renderChildrenOutsideViewport = false,
List<RenderBox> children,
}) : assert(childManager != null),
assert(offset != null),
assert(diameterRatio != null),
assert(diameterRatio > 0, diameterRatioZeroMessage),
assert(perspective != null),
assert(perspective > 0),
assert(perspective <= 0.01, perspectiveTooHighMessage),
assert(offAxisFraction != null),
assert(useMagnifier != null),
assert(magnification != null),
assert(magnification > 0),
assert(overAndUnderCenterOpacity != null),
assert(overAndUnderCenterOpacity >= 0 && overAndUnderCenterOpacity <= 1),
assert(itemExtent != null),
assert(squeeze != null),
assert(squeeze > 0),
assert(itemExtent > 0),
assert(clipToSize != null),
assert(renderChildrenOutsideViewport != null),
assert(
!renderChildrenOutsideViewport || !clipToSize,
clipToSizeAndRenderChildrenOutsideViewportConflict,
),
_offset = offset,
_diameterRatio = diameterRatio,
_perspective = perspective,
_offAxisFraction = offAxisFraction,
_useMagnifier = useMagnifier,
_magnification = magnification,
_overAndUnderCenterOpacity = overAndUnderCenterOpacity,
_itemExtent = itemExtent,
_squeeze = squeeze,
_clipToSize = clipToSize,
_renderChildrenOutsideViewport = renderChildrenOutsideViewport {
addAll(children);
}
/// An arbitrary but aesthetically reasonable default value for [diameterRatio].
static const double defaultDiameterRatio = 2.0;
/// An arbitrary but aesthetically reasonable default value for [perspective].
static const double defaultPerspective = 0.003;
/// An error message to show when the provided [diameterRatio] is zero.
static const String diameterRatioZeroMessage = "You can't set a diameterRatio "
'of 0 or of a negative number. It would imply a cylinder of 0 in diameter '
'in which case nothing will be drawn.';
/// An error message to show when the [perspective] value is too high.
static const String perspectiveTooHighMessage = 'A perspective too high will '
'be clipped in the z-axis and therefore not renderable. Value must be '
'between 0 and 0.01.';
/// An error message to show when [clipToSize] and [renderChildrenOutsideViewport]
/// are set to conflicting values.
static const String clipToSizeAndRenderChildrenOutsideViewportConflict =
'Cannot renderChildrenOutsideViewport and clipToSize since children '
'rendered outside will be clipped anyway.';
/// The delegate that manages the children of this object.
final ListWheelChildManager childManager;
/// The associated ViewportOffset object for the viewport describing the part
/// of the content inside that's visible.
///
/// The [ViewportOffset.pixels] value determines the scroll offset that the
/// viewport uses to select which part of its content to display. As the user
/// scrolls the viewport, this value changes, which changes the content that
/// is displayed.
///
/// Must not be null.
ViewportOffset get offset => _offset;
ViewportOffset _offset;
set offset(ViewportOffset value) {
assert(value != null);
if (value == _offset)
return;
if (attached)
_offset.removeListener(_hasScrolled);
_offset = value;
if (attached)
_offset.addListener(_hasScrolled);
markNeedsLayout();
}
/// {@template flutter.rendering.wheelList.diameterRatio}
/// A ratio between the diameter of the cylinder and the viewport's size
/// in the main axis.
///
/// A value of 1 means the cylinder has the same diameter as the viewport's
/// size.
///
/// A value smaller than 1 means items at the edges of the cylinder are
/// entirely contained inside the viewport.
///
/// A value larger than 1 means angles less than ±[pi] / 2 from the
/// center of the cylinder are visible.
///
/// The same number of children will be visible in the viewport regardless of
/// the [diameterRatio]. The number of children visible is based on the
/// viewport's length along the main axis divided by the children's
/// [itemExtent]. Then the children are evenly distributed along the visible
/// angles up to ±[pi] / 2.
///
/// Just as it's impossible to stretch a paper to cover the an entire
/// half of a cylinder's surface where the cylinder has the same diameter
/// as the paper's length, choosing a [diameterRatio] smaller than [pi]
/// will leave same gaps between the children.
///
/// Defaults to an arbitrary but aesthetically reasonable number of 2.0.
///
/// Must not be null and must be positive.
/// {@endtemplate}
double get diameterRatio => _diameterRatio;
double _diameterRatio;
set diameterRatio(double value) {
assert(value != null);
assert(
value > 0,
diameterRatioZeroMessage,
);
if (value == _diameterRatio)
return;
_diameterRatio = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// {@template flutter.rendering.wheelList.perspective}
/// Perspective of the cylindrical projection.
///
/// A number between 0 and 0.01 where 0 means looking at the cylinder from
/// infinitely far with an infinitely small field of view and 1 means looking
/// at the cylinder from infinitely close with an infinitely large field of
/// view (which cannot be rendered).
///
/// Defaults to an arbitrary but aesthetically reasonable number of 0.003.
/// A larger number brings the vanishing point closer and a smaller number
/// pushes the vanishing point further.
///
/// Must not be null and must be positive.
/// {@endtemplate}
double get perspective => _perspective;
double _perspective;
set perspective(double value) {
assert(value != null);
assert(value > 0);
assert(
value <= 0.01,
perspectiveTooHighMessage,
);
if (value == _perspective)
return;
_perspective = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// {@template flutter.rendering.wheelList.offAxisFraction}
/// How much the wheel is horizontally off-center, as a fraction of its width.
/// This property creates the visual effect of looking at a vertical wheel from
/// its side where its vanishing points at the edge curves to one side instead
/// of looking at the wheel head-on.
///
/// The value is horizontal distance between the wheel's center and the vertical
/// vanishing line at the edges of the wheel, represented as a fraction of the
/// wheel's width.
///
/// The value `0.0` means the wheel is looked at head-on and its vanishing
/// line runs through the center of the wheel. Negative values means moving
/// the wheel to the left of the observer, thus the edges curve to the right.
/// Positive values means moving the wheel to the right of the observer,
/// thus the edges curve to the left.
///
/// The visual effect causes the wheel's edges to curve rather than moving
/// the center. So a value of `0.5` means the edges' vanishing line will touch
/// the wheel's size's left edge.
///
/// Defaults to 0.0, which means looking at the wheel head-on.
/// The visual effect can be unaesthetic if this value is too far from the
/// range [-0.5, 0.5].
/// {@endtemplate}
double get offAxisFraction => _offAxisFraction;
double _offAxisFraction = 0.0;
set offAxisFraction(double value) {
assert(value != null);
if (value == _offAxisFraction)
return;
_offAxisFraction = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.useMagnifier}
/// Whether to use the magnifier for the center item of the wheel.
/// {@endtemplate}
bool get useMagnifier => _useMagnifier;
bool _useMagnifier = false;
set useMagnifier(bool value) {
assert(value != null);
if (value == _useMagnifier)
return;
_useMagnifier = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.magnification}
/// The zoomed-in rate of the magnifier, if it is used.
///
/// The default value is 1.0, which will not change anything.
/// If the value is > 1.0, the center item will be zoomed in by that rate, and
/// it will also be rendered as flat, not cylindrical like the rest of the list.
/// The item will be zoomed out if magnification < 1.0.
///
/// Must be positive.
/// {@endtemplate}
double get magnification => _magnification;
double _magnification = 1.0;
set magnification(double value) {
assert(value != null);
assert(value > 0);
if (value == _magnification)
return;
_magnification = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.overAndUnderCenterOpacity}
/// The opacity value that will be applied to the wheel that appears below and
/// above the magnifier.
///
/// The default value is 1.0, which will not change anything.
///
/// Must be greater than or equal to 0, and less than or equal to 1.
/// {@endtemplate}
double get overAndUnderCenterOpacity => _overAndUnderCenterOpacity;
double _overAndUnderCenterOpacity = 1.0;
set overAndUnderCenterOpacity(double value) {
assert(value != null);
assert(value >= 0 && value <= 1);
if (value == _overAndUnderCenterOpacity)
return;
_overAndUnderCenterOpacity = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.itemExtent}
/// The size of the children along the main axis. Children [RenderBox]es will
/// be given the [BoxConstraints] of this exact size.
///
/// Must not be null and must be positive.
/// {@endtemplate}
double get itemExtent => _itemExtent;
double _itemExtent;
set itemExtent(double value) {
assert(value != null);
assert(value > 0);
if (value == _itemExtent)
return;
_itemExtent = value;
markNeedsLayout();
}
/// {@template flutter.rendering.wheelList.squeeze}
/// The angular compactness of the children on the wheel.
///
/// This denotes a ratio of the number of children on the wheel vs the number
/// of children that would fit on a flat list of equivalent size, assuming
/// [diameterRatio] of 1.
///
/// For instance, if this RenderListWheelViewport has a height of 100px and
/// [itemExtent] is 20px, 5 items would fit on an equivalent flat list.
/// With a [squeeze] of 1, 5 items would also be shown in the
/// RenderListWheelViewport. With a [squeeze] of 2, 10 items would be shown
/// in the RenderListWheelViewport.
///
/// Changing this value will change the number of children built and shown
/// inside the wheel.
///
/// Must not be null and must be positive.
/// {@endtemplate}
///
/// Defaults to 1.
double get squeeze => _squeeze;
double _squeeze;
set squeeze(double value) {
assert(value != null);
assert(value > 0);
if (value == _squeeze)
return;
_squeeze = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}
/// {@template flutter.rendering.wheelList.clipToSize}
/// Whether to clip painted children to the inside of this viewport.
///
/// Defaults to [true]. Must not be null.
///
/// If this is false and [renderChildrenOutsideViewport] is false, the
/// first and last children may be painted partly outside of this scroll view.
/// {@endtemplate}
bool get clipToSize => _clipToSize;
bool _clipToSize;
set clipToSize(bool value) {
assert(value != null);
assert(
!renderChildrenOutsideViewport || !clipToSize,
clipToSizeAndRenderChildrenOutsideViewportConflict,
);
if (value == _clipToSize)
return;
_clipToSize = value;
markNeedsPaint();
markNeedsSemanticsUpdate();
}
/// {@template flutter.rendering.wheelList.renderChildrenOutsideViewport}
/// Whether to paint children inside the viewport only.
///
/// If false, every child will be painted. However the [Scrollable] is still
/// the size of the viewport and detects gestures inside only.
///
/// Defaults to [false]. Must not be null. Cannot be true if [clipToSize]
/// is also true since children outside the viewport will be clipped, and
/// therefore cannot render children outside the viewport.
/// {@endtemplate}
bool get renderChildrenOutsideViewport => _renderChildrenOutsideViewport;
bool _renderChildrenOutsideViewport;
set renderChildrenOutsideViewport(bool value) {
assert(value != null);
assert(
!renderChildrenOutsideViewport || !clipToSize,
clipToSizeAndRenderChildrenOutsideViewportConflict,
);
if (value == _renderChildrenOutsideViewport)
return;
_renderChildrenOutsideViewport = value;
markNeedsLayout();
markNeedsSemanticsUpdate();
}
void _hasScrolled() {
markNeedsLayout();
markNeedsSemanticsUpdate();
}
@override
void setupParentData(RenderObject child) {
if (child.parentData is! ListWheelParentData)
child.parentData = ListWheelParentData();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(_hasScrolled);
}
@override
void detach() {
_offset.removeListener(_hasScrolled);
super.detach();
}
@override
bool get isRepaintBoundary => true;
/// Main axis length in the untransformed plane.
double get _viewportExtent {
assert(hasSize);
return size.height;
}
/// Main axis scroll extent in the **scrollable layout coordinates** that puts
/// the first item in the center.
double get _minEstimatedScrollExtent {
assert(hasSize);
if (childManager.childCount == null)
return double.negativeInfinity;
return 0.0;
}
/// Main axis scroll extent in the **scrollable layout coordinates** that puts
/// the last item in the center.
double get _maxEstimatedScrollExtent {
assert(hasSize);
if (childManager.childCount == null)
return double.infinity;
return math.max(0.0, (childManager.childCount - 1) * _itemExtent);
}
/// Scroll extent distance in the untransformed plane between the center
/// position in the viewport and the top position in the viewport.
///
/// It's also the distance in the untransformed plane that children's painting
/// is offset by with respect to those children's [BoxParentData.offset].
double get _topScrollMarginExtent {
assert(hasSize);
// Consider adding alignment options other than center.
return -size.height / 2.0 + _itemExtent / 2.0;
}
/// Transforms a **scrollable layout coordinates**' y position to the
/// **untransformed plane's viewport painting coordinates**' y position given
/// the current scroll offset.
double _getUntransformedPaintingCoordinateY(double layoutCoordinateY) {
return layoutCoordinateY - _topScrollMarginExtent - offset.pixels;
}
/// Given the _diameterRatio, return the largest absolute angle of the item
/// at the edge of the portion of the visible cylinder.
///
/// For a _diameterRatio of 1 or less than 1 (i.e. the viewport is bigger
/// than the cylinder diameter), this value reaches and clips at pi / 2.
///
/// When the center of children passes this angle, they are no longer painted
/// if [renderChildrenOutsideViewport] is false.
double get _maxVisibleRadian {
if (_diameterRatio < 1.0)
return math.pi / 2.0;
return math.asin(1.0 / _diameterRatio);
}
double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
double extent = 0.0;
RenderBox child = firstChild;
while (child != null) {
extent = math.max(extent, childSize(child));
child = childAfter(child);
}
return extent;
}
@override
double computeMinIntrinsicWidth(double height) {
return _getIntrinsicCrossAxis(
(RenderBox child) => child.getMinIntrinsicWidth(height)
);
}
@override
double computeMaxIntrinsicWidth(double height) {
return _getIntrinsicCrossAxis(
(RenderBox child) => child.getMaxIntrinsicWidth(height)
);
}
@override
double computeMinIntrinsicHeight(double width) {
if (childManager.childCount == null)
return 0.0;
return childManager.childCount * _itemExtent;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (childManager.childCount == null)
return 0.0;
return childManager.childCount * _itemExtent;
}
@override
bool get sizedByParent => true;
@override
void performResize() {
size = constraints.biggest;
}
/// Gets the index of a child by looking at its parentData.
int indexOf(RenderBox child) {
assert(child != null);
final ListWheelParentData childParentData = child.parentData as ListWheelParentData;
assert(childParentData.index != null);
return childParentData.index;
}
/// Returns the index of the child at the given offset.
int scrollOffsetToIndex(double scrollOffset) => (scrollOffset / itemExtent).floor();
/// Returns the scroll offset of the child with the given index.
double indexToScrollOffset(int index) => index * itemExtent;
void _createChild(int index, { RenderBox after }) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) {
assert(constraints == this.constraints);
childManager.createChild(index, after: after);
});
}
void _destroyChild(RenderBox child) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints constraints) {
assert(constraints == this.constraints);
childManager.removeChild(child);
});
}
void _layoutChild(RenderBox child, BoxConstraints constraints, int index) {
child.layout(constraints, parentUsesSize: true);
final ListWheelParentData childParentData = child.parentData as ListWheelParentData;
// Centers the child horizontally.
final double crossPosition = size.width / 2.0 - child.size.width / 2.0;
childParentData.offset = Offset(crossPosition, indexToScrollOffset(index));
}
/// Performs layout based on how [childManager] provides children.
///
/// From the current scroll offset, the minimum index and maximum index that
/// is visible in the viewport can be calculated. The index range of the
/// currently active children can also be acquired by looking directly at
/// the current child list. This function has to modify the current index
/// range to match the target index range by removing children that are no
/// longer visible and creating those that are visible but not yet provided
/// by [childManager].
@override
void performLayout() {
final BoxConstraints childConstraints =
constraints.copyWith(
minHeight: _itemExtent,
maxHeight: _itemExtent,
minWidth: 0.0,
);
// The height, in pixel, that children will be visible and might be laid out
// and painted.
double visibleHeight = size.height * _squeeze;
// If renderChildrenOutsideViewport is true, we spawn extra children by
// doubling the visibility range, those that are in the backside of the
// cylinder won't be painted anyway.
if (renderChildrenOutsideViewport)
visibleHeight *= 2;
final double firstVisibleOffset =
offset.pixels + _itemExtent / 2 - visibleHeight / 2;
final double lastVisibleOffset = firstVisibleOffset + visibleHeight;
// The index range that we want to spawn children. We find indexes that
// are in the interval [firstVisibleOffset, lastVisibleOffset).
int targetFirstIndex = scrollOffsetToIndex(firstVisibleOffset);
int targetLastIndex = scrollOffsetToIndex(lastVisibleOffset);
// Because we exclude lastVisibleOffset, if there's a new child starting at
// that offset, it is removed.
if (targetLastIndex * _itemExtent == lastVisibleOffset)
targetLastIndex--;
// Validates the target index range.
while (!childManager.childExistsAt(targetFirstIndex) && targetFirstIndex <= targetLastIndex)
targetFirstIndex++;
while (!childManager.childExistsAt(targetLastIndex) && targetFirstIndex <= targetLastIndex)
targetLastIndex--;
// If it turns out there's no children to layout, we remove old children and
// return.
if (targetFirstIndex > targetLastIndex) {
while (firstChild != null)
_destroyChild(firstChild);
return;
}
// Now there are 2 cases:
// - The target index range and our current index range have intersection:
// We shorten and extend our current child list so that the two lists
// match. Most of the time we are in this case.
// - The target list and our current child list have no intersection:
// We first remove all children and then add one child from the target
// list => this case becomes the other case.
// Case when there is no intersection.
if (childCount > 0 &&
(indexOf(firstChild) > targetLastIndex || indexOf(lastChild) < targetFirstIndex)) {
while (firstChild != null)
_destroyChild(firstChild);
}
// If there is no child at this stage, we add the first one that is in
// target range.
if (childCount == 0) {
_createChild(targetFirstIndex);
_layoutChild(firstChild, childConstraints, targetFirstIndex);
}
int currentFirstIndex = indexOf(firstChild);
int currentLastIndex = indexOf(lastChild);
// Remove all unnecessary children by shortening the current child list, in
// both directions.
while (currentFirstIndex < targetFirstIndex) {
_destroyChild(firstChild);
currentFirstIndex++;
}
while (currentLastIndex > targetLastIndex) {
_destroyChild(lastChild);
currentLastIndex--;
}
// Relayout all active children.
RenderBox child = firstChild;
while (child != null) {
child.layout(childConstraints, parentUsesSize: true);
child = childAfter(child);
}
// Spawning new children that are actually visible but not in child list yet.
while (currentFirstIndex > targetFirstIndex) {
_createChild(currentFirstIndex - 1);
_layoutChild(firstChild, childConstraints, --currentFirstIndex);
}
while (currentLastIndex < targetLastIndex) {
_createChild(currentLastIndex + 1, after: lastChild);
_layoutChild(lastChild, childConstraints, ++currentLastIndex);
}
offset.applyViewportDimension(_viewportExtent);
// Applying content dimensions bases on how the childManager builds widgets:
// if it is available to provide a child just out of target range, then
// we don't know whether there's a limit yet, and set the dimension to the
// estimated value. Otherwise, we set the dimension limited to our target
// range.
final double minScrollExtent = childManager.childExistsAt(targetFirstIndex - 1)
? _minEstimatedScrollExtent
: indexToScrollOffset(targetFirstIndex);
final double maxScrollExtent = childManager.childExistsAt(targetLastIndex + 1)
? _maxEstimatedScrollExtent
: indexToScrollOffset(targetLastIndex);
offset.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
bool _shouldClipAtCurrentOffset() {
final double highestUntransformedPaintY =
_getUntransformedPaintingCoordinateY(0.0);
return highestUntransformedPaintY < 0.0
|| size.height < highestUntransformedPaintY + _maxEstimatedScrollExtent + _itemExtent;
}
@override
void paint(PaintingContext context, Offset offset) {
if (childCount > 0) {
if (_clipToSize && _shouldClipAtCurrentOffset()) {
context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintVisibleChildren,
);
} else {
_paintVisibleChildren(context, offset);
}
}
}
/// Paints all children visible in the current viewport.
void _paintVisibleChildren(PaintingContext context, Offset offset) {
RenderBox childToPaint = firstChild;
ListWheelParentData childParentData = childToPaint?.parentData as ListWheelParentData;
while (childParentData != null) {
_paintTransformedChild(childToPaint, context, offset, childParentData.offset);
childToPaint = childAfter(childToPaint);
childParentData = childToPaint?.parentData as ListWheelParentData;
}
}
/// Takes in a child with a **scrollable layout offset** and paints it in the
/// **transformed cylindrical space viewport painting coordinates**.
void _paintTransformedChild(
RenderBox child,
PaintingContext context,
Offset offset,
Offset layoutOffset,
) {
final Offset untransformedPaintingCoordinates = offset
+ Offset(
layoutOffset.dx,
_getUntransformedPaintingCoordinateY(layoutOffset.dy),
);
// Get child's center as a fraction of the viewport's height.
final double fractionalY =
(untransformedPaintingCoordinates.dy + _itemExtent / 2.0) / size.height;
final double angle = -(fractionalY - 0.5) * 2.0 * _maxVisibleRadian / squeeze;
// Don't paint the backside of the cylinder when
// renderChildrenOutsideViewport is true. Otherwise, only children within
// suitable angles (via _first/lastVisibleLayoutOffset) reach the paint
// phase.
if (angle > math.pi / 2.0 || angle < -math.pi / 2.0)
return;
final Matrix4 transform = MatrixUtils.createCylindricalProjectionTransform(
radius: size.height * _diameterRatio / 2.0,
angle: angle,
perspective: _perspective,
);
// Offset that helps painting everything in the center (e.g. angle = 0).
final Offset offsetToCenter = Offset(
untransformedPaintingCoordinates.dx,
-_topScrollMarginExtent,
);
final bool shouldApplyOffCenterDim = overAndUnderCenterOpacity < 1;
if (useMagnifier || shouldApplyOffCenterDim) {
_paintChildWithMagnifier(context, offset, child, transform, offsetToCenter, untransformedPaintingCoordinates);
} else {
_paintChildCylindrically(context, offset, child, transform, offsetToCenter);
}
}
/// Paint child with the magnifier active - the child will be rendered
/// differently if it intersects with the magnifier.
void _paintChildWithMagnifier(
PaintingContext context,
Offset offset,
RenderBox child,
Matrix4 cylindricalTransform,
Offset offsetToCenter,
Offset untransformedPaintingCoordinates,
) {
final double magnifierTopLinePosition =
size.height / 2 - _itemExtent * _magnification / 2;
final double magnifierBottomLinePosition =
size.height / 2 + _itemExtent * _magnification / 2;
final bool isAfterMagnifierTopLine = untransformedPaintingCoordinates.dy
>= magnifierTopLinePosition - _itemExtent * _magnification;
final bool isBeforeMagnifierBottomLine = untransformedPaintingCoordinates.dy
<= magnifierBottomLinePosition;
// 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);
// Clipping the part in the center.
context.pushClipRect(
needsCompositing,
offset,
centerRect,
(PaintingContext context, Offset offset) {
context.pushTransform(
needsCompositing,
offset,
_magnifyTransform(),
(PaintingContext context, Offset offset) {
context.paintChild(child, offset + untransformedPaintingCoordinates);
});
});
// Clipping the part in either the top-half or bottom-half of the wheel.
context.pushClipRect(
needsCompositing,
offset,
untransformedPaintingCoordinates.dy <= magnifierTopLinePosition
? topHalfRect
: bottomHalfRect,
(PaintingContext context, Offset offset) {
_paintChildCylindrically(
context,
offset,
child,
cylindricalTransform,
offsetToCenter);
},
);
} else {
_paintChildCylindrically(
context,
offset,
child,
cylindricalTransform,
offsetToCenter);
}
}
// / Paint the child cylindrically at given offset.
void _paintChildCylindrically(
PaintingContext context,
Offset offset,
RenderBox child,
Matrix4 cylindricalTransform,
Offset offsetToCenter,
) {
// Paint child cylindrically, without [overAndUnderCenterOpacity].
final PaintingContextCallback painter = (PaintingContext context, Offset offset) {
context.paintChild(
child,
// Paint everything in the center (e.g. angle = 0), then transform.
offset + offsetToCenter,
);
};
// Paint child cylindrically, with [overAndUnderCenterOpacity].
final PaintingContextCallback 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,
);
}
/// Return the Matrix4 transformation that would zoom in content in the
/// magnified area.
Matrix4 _magnifyTransform() {
final Matrix4 magnify = Matrix4.identity();
magnify.translate(size.width * (-_offAxisFraction + 0.5), size.height / 2);
magnify.scale(_magnification, _magnification, _magnification);
magnify.translate(-size.width * (-_offAxisFraction + 0.5), -size.height / 2);
return magnify;
}
/// Apply incoming transformation with the transformation's origin at the
/// viewport's center or horizontally off to the side based on offAxisFraction.
Matrix4 _centerOriginTransform(Matrix4 originalMatrix) {
final Matrix4 result = Matrix4.identity();
final Offset centerOriginTranslation = Alignment.center.alongSize(size);
result.translate(centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1),
centerOriginTranslation.dy);
result.multiply(originalMatrix);
result.translate(-centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1),
-centerOriginTranslation.dy);
return result;
}
/// This returns the matrices relative to the **untransformed plane's viewport
/// painting coordinates** system.
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final ListWheelParentData parentData = child?.parentData as ListWheelParentData;
transform.translate(0.0, _getUntransformedPaintingCoordinateY(parentData.offset.dy));
}
@override
Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtCurrentOffset()) {
return Offset.zero & size;
}
return null;
}
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) => false;
@override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect rect }) {
// `target` is only fully revealed when in the selected/center position. Therefore,
// this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values.
rect ??= target.paintBounds;
// `child` will be the last RenderObject before the viewport when walking up from `target`.
RenderObject child = target;
while (child.parent != this)
child = child.parent as RenderObject;
final ListWheelParentData parentData = child.parentData as ListWheelParentData;
final double targetOffset = parentData.offset.dy; // the so-called "centerPosition"
final Matrix4 transform = target.getTransformTo(child);
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Rect targetRect = bounds.translate(0.0, (size.height - itemExtent) / 2);
return RevealedOffset(offset: targetOffset, rect: targetRect);
}
@override
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (descendant != null) {
// Shows the descendant in the selected/center position.
final RevealedOffset revealedOffset = getOffsetToReveal(descendant, 0.5, rect: rect);
if (duration == Duration.zero) {
offset.jumpTo(revealedOffset.offset);
} else {
offset.animateTo(revealedOffset.offset, duration: duration, curve: curve);
}
rect = revealedOffset.rect;
}
super.showOnScreen(
rect: rect,
duration: duration,
curve: curve,
);
}
}