mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
CupertinoPicker part 1 - create a generic ListWheelScrollView (#13783)
* Create ListWheelScrollView
* fix missing doc
* tweak docs a bit
* fix imports
* Add some tests
* review
* review and transform tests
* fix test
* repatch lost 977701cf69
* review
* Review
This commit is contained in:
parent
de023bc6f4
commit
e64d44ca78
@ -44,6 +44,7 @@ export 'src/rendering/flow.dart';
|
||||
export 'src/rendering/image.dart';
|
||||
export 'src/rendering/layer.dart';
|
||||
export 'src/rendering/list_body.dart';
|
||||
export 'src/rendering/list_wheel_viewport.dart';
|
||||
export 'src/rendering/object.dart';
|
||||
export 'src/rendering/paragraph.dart';
|
||||
export 'src/rendering/performance_overlay.dart';
|
||||
|
||||
636
packages/flutter/lib/src/rendering/list_wheel_viewport.dart
Normal file
636
packages/flutter/lib/src/rendering/list_wheel_viewport.dart
Normal file
@ -0,0 +1,636 @@
|
||||
// Copyright 2017 The Chromium 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/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 double _ChildSizingFunction(RenderBox child);
|
||||
|
||||
/// [ParentData] for use with [RenderListWheelViewport].
|
||||
class ListWheelParentData extends ContainerBoxParentData<RenderBox> { }
|
||||
|
||||
/// 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>,
|
||||
RenderBoxContainerDefaultsMixin<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 ViewportOffset offset,
|
||||
double diameterRatio: defaultDiameterRatio,
|
||||
double perspective: defaultPerspective,
|
||||
@required double itemExtent,
|
||||
bool clipToSize: true,
|
||||
bool renderChildrenOutsideViewport: false,
|
||||
List<RenderBox> children,
|
||||
}) : assert(offset != null),
|
||||
assert(diameterRatio != null),
|
||||
assert(diameterRatio > 0, diameterRatioZeroMessage),
|
||||
assert(perspective != null),
|
||||
assert(perspective > 0),
|
||||
assert(perspective <= 0.01, perspectiveTooHighMessage),
|
||||
assert(itemExtent != null),
|
||||
assert(itemExtent > 0),
|
||||
assert(clipToSize != null),
|
||||
assert(renderChildrenOutsideViewport != null),
|
||||
assert(
|
||||
!renderChildrenOutsideViewport || !clipToSize,
|
||||
clipToSizeAndRenderChildrenOutsideViewportConflict,
|
||||
),
|
||||
_offset = offset,
|
||||
_diameterRatio = diameterRatio,
|
||||
_perspective = perspective,
|
||||
_itemExtent = itemExtent,
|
||||
_clipToSize = clipToSize,
|
||||
_renderChildrenOutsideViewport = renderChildrenOutsideViewport {
|
||||
addAll(children);
|
||||
}
|
||||
|
||||
/// An arbitrary but aesthetically reasonable default value for [diameterRatio].
|
||||
static const double defaultDiameterRatio = 2.0;
|
||||
|
||||
/// Ar 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. 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 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;
|
||||
_hasScrolled();
|
||||
}
|
||||
|
||||
/// {@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;
|
||||
_hasScrolled();
|
||||
}
|
||||
|
||||
/// {@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.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;
|
||||
_hasScrolled();
|
||||
}
|
||||
|
||||
/// {@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;
|
||||
_hasScrolled();
|
||||
}
|
||||
|
||||
void _hasScrolled() {
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
if (child.parentData is! ListWheelParentData)
|
||||
child.parentData = new 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 _minScrollExtent {
|
||||
assert(hasSize);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// Main axis scroll extent in the **scrollable layout coordinates** that puts
|
||||
/// the last item in the center.
|
||||
double get _maxScrollExtent {
|
||||
assert(hasSize);
|
||||
if (!(childCount > 0))
|
||||
return 0.0;
|
||||
|
||||
return math.max(0.0, (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 _minScrollExtent - 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;
|
||||
}
|
||||
|
||||
/// Children with offsets larger than this value in the **scrollable layout
|
||||
/// coordinates** can be painted.
|
||||
double get _firstVisibleLayoutOffset {
|
||||
assert(hasSize);
|
||||
if (_renderChildrenOutsideViewport)
|
||||
return double.negativeInfinity;
|
||||
return _minScrollExtent - size.height / 2.0 - _itemExtent / 2.0 + offset.pixels;
|
||||
}
|
||||
|
||||
/// Children with offsets smaller than this value in the **scrollable layout
|
||||
/// coordinates** can be painted.
|
||||
double get _lastVisibleLayoutOffset {
|
||||
assert(hasSize);
|
||||
if (_renderChildrenOutsideViewport)
|
||||
return double.infinity;
|
||||
return _minScrollExtent + size.height / 2.0 + _itemExtent / 2.0 + 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 (childCount > 0)
|
||||
return childCount * _itemExtent;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
if (childCount > 0)
|
||||
return childCount * _itemExtent;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
size = constraints.biggest;
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
double currentOffset = 0.0;
|
||||
RenderBox child = firstChild;
|
||||
final BoxConstraints innerConstraints =
|
||||
constraints.copyWith(
|
||||
minHeight: _itemExtent,
|
||||
maxHeight: _itemExtent,
|
||||
minWidth: 0.0,
|
||||
);
|
||||
while (child != null) {
|
||||
child.layout(innerConstraints, parentUsesSize: true);
|
||||
final ListWheelParentData childParentData = child.parentData;
|
||||
// Centers the child in the cross axis. Consider making it configurable.
|
||||
final double crossPosition = size.width / 2.0 - child.size.width / 2.0;
|
||||
childParentData.offset = new Offset(crossPosition, currentOffset);
|
||||
currentOffset += _itemExtent;
|
||||
child = childAfter(child);
|
||||
}
|
||||
|
||||
offset.applyViewportDimension(_viewportExtent);
|
||||
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
|
||||
}
|
||||
|
||||
bool _shouldClipAtCurrentOffset() {
|
||||
final double highestUntransformedPaintY =
|
||||
_getUntransformedPaintingCoordinateY(0.0);
|
||||
return highestUntransformedPaintY < 0.0
|
||||
|| size.height < highestUntransformedPaintY + _maxScrollExtent + _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Visits all the children until one is partially visible in the viewport.
|
||||
RenderBox _getFirstVisibleChild() {
|
||||
assert(childCount > 0);
|
||||
final double firstVisibleLayoutOffset = _firstVisibleLayoutOffset;
|
||||
|
||||
RenderBox child = firstChild;
|
||||
ListWheelParentData childParentData = child.parentData;
|
||||
|
||||
while (childParentData != null
|
||||
&& childParentData.offset.dy <= firstVisibleLayoutOffset) {
|
||||
child = childAfter(child);
|
||||
childParentData = child?.parentData;
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/// Paints all children visible in the current viewport.
|
||||
void _paintVisibleChildren(PaintingContext context, Offset offset) {
|
||||
assert(childCount > 0);
|
||||
final double lastVisibleLayoutOffset = _lastVisibleLayoutOffset;
|
||||
|
||||
RenderBox childToPaint = _getFirstVisibleChild();
|
||||
ListWheelParentData childParentData = childToPaint?.parentData;
|
||||
|
||||
while (childParentData != null
|
||||
&& childParentData.offset.dy < lastVisibleLayoutOffset) {
|
||||
_paintTransformedChild(childToPaint, context, offset, childParentData.offset);
|
||||
childToPaint = childAfter(childToPaint);
|
||||
childParentData = childToPaint?.parentData;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
+ new 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;
|
||||
// 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,
|
||||
);
|
||||
|
||||
context.pushTransform(
|
||||
needsCompositing,
|
||||
offset,
|
||||
_centerOriginTransform(transform),
|
||||
// Pre-transform painting function.
|
||||
(PaintingContext context, Offset offset) {
|
||||
context.paintChild(
|
||||
child,
|
||||
offset + new Offset(
|
||||
untransformedPaintingCoordinates.dx,
|
||||
// Paint everything in the center (e.g. angle = 0), then transform.
|
||||
-_topScrollMarginExtent,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Apply incoming transformation with the transformation's origin at the
|
||||
/// viewport's center.
|
||||
Matrix4 _centerOriginTransform(Matrix4 originalMatrix) {
|
||||
final Matrix4 result = new Matrix4.identity();
|
||||
final Offset centerOriginTranslation = Alignment.center.alongSize(size);
|
||||
result.translate(centerOriginTranslation.dx, centerOriginTranslation.dy);
|
||||
result.multiply(originalMatrix);
|
||||
result.translate(-centerOriginTranslation.dx, -centerOriginTranslation.dy);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
transform.translate(0.0, _getUntransformedPaintingCoordinateY(0.0));
|
||||
}
|
||||
|
||||
@override
|
||||
Rect describeApproximatePaintClip(RenderObject child) {
|
||||
if (child != null && _shouldClipAtCurrentOffset()) {
|
||||
return Offset.zero & size;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(HitTestResult result, { Offset position }) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
double getOffsetToReveal(RenderObject target, double alignment) {
|
||||
final ListWheelParentData parentData = target.parentData;
|
||||
final double centerPosition = parentData.offset.dy;
|
||||
|
||||
if (alignment < 0.5) {
|
||||
return centerPosition + _topScrollMarginExtent * alignment * 2.0;
|
||||
} else if (alignment > 0.5) {
|
||||
return centerPosition - _topScrollMarginExtent * (alignment - 0.5) * 2.0;
|
||||
} else {
|
||||
return centerPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void showOnScreen([RenderObject child]) {
|
||||
if (child != null) {
|
||||
// Shows the child in the selected/center position.
|
||||
offset.jumpTo(getOffsetToReveal(child, 0.5));
|
||||
}
|
||||
|
||||
// Make sure the viewport itself is on screen.
|
||||
super.showOnScreen();
|
||||
}
|
||||
}
|
||||
189
packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart
Normal file
189
packages/flutter/lib/src/widgets/list_wheel_scroll_view.dart
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright 2017 The Chromium 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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'framework.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scrollable.dart';
|
||||
|
||||
/// A box in which children on a wheel can be scrolled.
|
||||
///
|
||||
/// This widget is similar to a [ListView] but with the restriction that all
|
||||
/// children must be the same size along the scrolling axis.
|
||||
///
|
||||
/// When the list is at the zero scroll offset, the first child is aligned with
|
||||
/// the middle of the viewport. When the list is at the final scroll offset,
|
||||
/// the last child is aligned with the middle of the viewport
|
||||
///
|
||||
/// The children are rendered as if rotating on a wheel instead of scrolling on
|
||||
/// a plane.
|
||||
class ListWheelScrollView extends StatelessWidget {
|
||||
/// Creates a box in which children are scrolled on a wheel.
|
||||
const ListWheelScrollView({
|
||||
Key key,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.diameterRatio: RenderListWheelViewport.defaultDiameterRatio,
|
||||
this.perspective: RenderListWheelViewport.defaultPerspective,
|
||||
@required this.itemExtent,
|
||||
this.clipToSize: true,
|
||||
this.renderChildrenOutsideViewport: false,
|
||||
@required this.children,
|
||||
}) : assert(diameterRatio != null),
|
||||
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
|
||||
assert(perspective != null),
|
||||
assert(perspective > 0),
|
||||
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
|
||||
assert(itemExtent != null),
|
||||
assert(itemExtent > 0),
|
||||
assert(clipToSize != null),
|
||||
assert(renderChildrenOutsideViewport != null),
|
||||
assert(
|
||||
!renderChildrenOutsideViewport || !clipToSize,
|
||||
RenderListWheelViewport.clipToSizeAndRenderChildrenOutsideViewportConflict,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// An object that can be used to control the position to which this scroll
|
||||
/// view is scrolled.
|
||||
///
|
||||
/// A [ScrollController] serves several purposes. It can be used to control
|
||||
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
|
||||
/// It can be used to control whether the scroll view should automatically
|
||||
/// save and restore its scroll position in the [PageStorage] (see
|
||||
/// [ScrollController.keepScrollOffset]). It can be used to read the current
|
||||
/// scroll position (see [ScrollController.offset]), or change it (see
|
||||
/// [ScrollController.animateTo]).
|
||||
final ScrollController controller;
|
||||
|
||||
/// How the scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
/// user stops dragging the scroll view.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics physics;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.diameterRatio}
|
||||
final double diameterRatio;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.perspective}
|
||||
final double perspective;
|
||||
|
||||
/// Size of each child in the main axis. Must not be null and must be
|
||||
/// positive.
|
||||
final double itemExtent;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.clipToSize}
|
||||
final bool clipToSize;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.renderChildrenOutsideViewport}
|
||||
final bool renderChildrenOutsideViewport;
|
||||
|
||||
/// List of children to scroll on top of the cylinder.
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Scrollable(
|
||||
controller: controller,
|
||||
physics: physics,
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return new ListWheelViewport(
|
||||
diameterRatio: diameterRatio,
|
||||
perspective: perspective,
|
||||
itemExtent: itemExtent,
|
||||
clipToSize: clipToSize,
|
||||
renderChildrenOutsideViewport: renderChildrenOutsideViewport,
|
||||
offset: offset,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A viewport showing a subset of children on a wheel.
|
||||
///
|
||||
/// Typically used with [ListWheelScrollView], this viewport is similar to
|
||||
/// [Viewport] in that it shows a subset of children in a scrollable based
|
||||
/// on the scrolling offset and the childrens' dimensions. But uses
|
||||
/// [RenderListWheelViewport] to display the children on a wheel.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListWheelScrollView], widget that combines this viewport with a scrollable.
|
||||
/// * [RenderListWheelViewport], the render object that renders the children
|
||||
/// on a wheel.
|
||||
class ListWheelViewport extends MultiChildRenderObjectWidget {
|
||||
ListWheelViewport({
|
||||
Key key,
|
||||
this.diameterRatio: RenderListWheelViewport.defaultDiameterRatio,
|
||||
this.perspective: RenderListWheelViewport.defaultPerspective,
|
||||
@required this.itemExtent,
|
||||
this.clipToSize: true,
|
||||
this.renderChildrenOutsideViewport: false,
|
||||
@required this.offset,
|
||||
List<Widget> children,
|
||||
}) : assert(offset != null),
|
||||
assert(diameterRatio != null),
|
||||
assert(diameterRatio > 0, RenderListWheelViewport.diameterRatioZeroMessage),
|
||||
assert(perspective != null),
|
||||
assert(perspective > 0),
|
||||
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
|
||||
assert(itemExtent != null),
|
||||
assert(itemExtent > 0),
|
||||
assert(clipToSize != null),
|
||||
assert(renderChildrenOutsideViewport != null),
|
||||
assert(
|
||||
!renderChildrenOutsideViewport || !clipToSize,
|
||||
RenderListWheelViewport.clipToSizeAndRenderChildrenOutsideViewportConflict,
|
||||
),
|
||||
super(key: key, children: children);
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.diameterRatio}
|
||||
final double diameterRatio;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.perspective}
|
||||
final double perspective;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.itemExtent}
|
||||
final double itemExtent;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.clipToSize}
|
||||
final bool clipToSize;
|
||||
|
||||
/// {@macro flutter.rendering.wheelList.renderChildrenOutsideViewport}
|
||||
final bool renderChildrenOutsideViewport;
|
||||
|
||||
/// [ViewportOffset] object describing the content that should be visible
|
||||
/// in the viewport.
|
||||
final ViewportOffset offset;
|
||||
|
||||
@override
|
||||
RenderListWheelViewport createRenderObject(BuildContext context) {
|
||||
return new RenderListWheelViewport(
|
||||
diameterRatio: diameterRatio,
|
||||
perspective: perspective,
|
||||
itemExtent: itemExtent,
|
||||
clipToSize: clipToSize,
|
||||
renderChildrenOutsideViewport: renderChildrenOutsideViewport,
|
||||
offset: offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderListWheelViewport renderObject) {
|
||||
renderObject
|
||||
..diameterRatio = diameterRatio
|
||||
..perspective = perspective
|
||||
..itemExtent = itemExtent
|
||||
..clipToSize = clipToSize
|
||||
..renderChildrenOutsideViewport = renderChildrenOutsideViewport
|
||||
..offset = offset;
|
||||
}
|
||||
}
|
||||
@ -45,6 +45,7 @@ export 'src/widgets/image.dart';
|
||||
export 'src/widgets/image_icon.dart';
|
||||
export 'src/widgets/implicit_animations.dart';
|
||||
export 'src/widgets/layout_builder.dart';
|
||||
export 'src/widgets/list_wheel_scroll_view.dart';
|
||||
export 'src/widgets/localizations.dart';
|
||||
export 'src/widgets/media_query.dart';
|
||||
export 'src/widgets/modal_barrier.dart';
|
||||
|
||||
@ -74,6 +74,21 @@ typedef void _CanvasPainterFunction(Canvas canvas);
|
||||
/// Patterns are subset matches, meaning that any calls not described by the
|
||||
/// pattern are ignored. This allows, for instance, transforms to be skipped.
|
||||
abstract class PaintPattern {
|
||||
/// Indicates that a transform is expected next.
|
||||
///
|
||||
/// Calls are skipped until a call to [Canvas.transform] is found. The call's
|
||||
/// arguments are compared to those provided here. If any fail to match, or if
|
||||
/// no call to [Canvas.transform] is found, then the matcher fails.
|
||||
///
|
||||
/// Dynamic so matchers can be more easily passed in.
|
||||
///
|
||||
/// The `matrix4` argument is dynamic so it can be either a [Matcher], or a
|
||||
/// [Float64List] of [double]s. If it is a [Float64List] of [double]s then
|
||||
/// each value in the matrix must match in the expected matrix. A deep
|
||||
/// matching [Matcher] such as [equals] can be used to test each value in the
|
||||
/// matrix with utilities such as [moreOrLessEquals].
|
||||
void transform({ dynamic matrix4 });
|
||||
|
||||
/// Indicates that a translation transform is expected next.
|
||||
///
|
||||
/// Calls are skipped until a call to [Canvas.translate] is found. The call's
|
||||
@ -574,6 +589,11 @@ class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher {
|
||||
class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher implements PaintPattern {
|
||||
final List<_PaintPredicate> _predicates = <_PaintPredicate>[];
|
||||
|
||||
@override
|
||||
void transform({ dynamic matrix4 }) {
|
||||
_predicates.add(new _FunctionPaintPredicate(#transform, <dynamic>[matrix4]));
|
||||
}
|
||||
|
||||
@override
|
||||
void translate({ double x, double y }) {
|
||||
_predicates.add(new _FunctionPaintPredicate(#translate, <dynamic>[x, y]));
|
||||
|
||||
@ -110,6 +110,14 @@ class TestRecordingPaintingContext implements PaintingContext {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) {
|
||||
canvas.save();
|
||||
canvas.transform(transform.storage);
|
||||
painter(this, offset);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
void noSuchMethod(Invocation invocation) { }
|
||||
}
|
||||
|
||||
380
packages/flutter/test/widgets/list_wheel_scroll_view_test.dart
Normal file
380
packages/flutter/test/widgets/list_wheel_scroll_view_test.dart
Normal file
@ -0,0 +1,380 @@
|
||||
// Copyright 2017 The Chromium 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 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../rendering/rendering_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('ListWheelScrollView needs positive diameter ratio', (WidgetTester tester) async {
|
||||
try {
|
||||
new ListWheelScrollView(
|
||||
diameterRatio: -2.0,
|
||||
itemExtent: 20.0,
|
||||
children: <Widget>[],
|
||||
);
|
||||
fail('Expected failure with negative diameterRatio');
|
||||
} on AssertionError catch (exception) {
|
||||
expect(exception.message, contains("You can't set a diameterRatio of 0"));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('ListWheelScrollView needs positive item extent', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
new ListWheelScrollView(
|
||||
itemExtent: null,
|
||||
children: <Widget>[new Container()],
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("ListWheelScrollView takes parent's size with small children", (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
// Inner children smaller than the outer window.
|
||||
itemExtent: 50.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 50.0,
|
||||
color: const Color(0xFFFFFFFF),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(ListWheelScrollView)), const Offset(0.0, 0.0));
|
||||
// Standard test screen size.
|
||||
expect(tester.getBottomRight(find.byType(ListWheelScrollView)), const Offset(800.0, 600.0));
|
||||
});
|
||||
|
||||
testWidgets("ListWheelScrollView takes parent's size with large children", (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
// Inner children 5000.0px.
|
||||
itemExtent: 50.0,
|
||||
children: new List<Widget>.generate(100, (int index) {
|
||||
return new Container(
|
||||
height: 50.0,
|
||||
color: const Color(0xFFFFFFFF),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(tester.getTopLeft(find.byType(ListWheelScrollView)), const Offset(0.0, 0.0));
|
||||
// Still fills standard test screen size.
|
||||
expect(tester.getBottomRight(find.byType(ListWheelScrollView)), const Offset(800.0, 600.0));
|
||||
});
|
||||
|
||||
testWidgets("ListWheelScrollView children can't be bigger than itemExtent", (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
itemExtent: 50.0,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
height: 200.0,
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSize(find.byType(SizedBox)), const Size(200.0, 50.0));
|
||||
expect(find.text('blah'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ListWheelScrollView can have zero child', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
itemExtent: 50.0,
|
||||
children: <Widget>[],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.getSize(find.byType(ListWheelScrollView)), const Size(800.0, 600.0));
|
||||
});
|
||||
|
||||
testWidgets('ListWheelScrollView starts and ends from the middle', (WidgetTester tester) async {
|
||||
final ScrollController controller = new ScrollController();
|
||||
final List<int> paintedChildren = <int>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
itemExtent: 100.0,
|
||||
children: new List<Widget>.generate(100, (int index) {
|
||||
return new CustomPaint(
|
||||
painter: new TestCallbackPainter(onPaint: () {
|
||||
paintedChildren.add(index);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen is 600px tall and the first item starts at 250px. The first 4
|
||||
// children are visible.
|
||||
expect(paintedChildren, <int>[0, 1, 2, 3]);
|
||||
|
||||
controller.jumpTo(1000.0);
|
||||
paintedChildren.clear();
|
||||
|
||||
await tester.pump();
|
||||
// Item number 10 is now in the middle of the screen at 250px. 9, 8, 7 are
|
||||
// visible before it and 11, 12, 13 are visible after it.
|
||||
expect(paintedChildren, <int>[7, 8, 9, 10, 11, 12, 13]);
|
||||
|
||||
// Move to the last item.
|
||||
controller.jumpTo(9900.0);
|
||||
paintedChildren.clear();
|
||||
|
||||
await tester.pump();
|
||||
// Item 99 is in the middle at 250px.
|
||||
expect(paintedChildren, <int>[96, 97, 98, 99]);
|
||||
});
|
||||
|
||||
testWidgets('A child gets painted as soon as its first pixel is in the viewport', (WidgetTester tester) async {
|
||||
final ScrollController controller = new ScrollController(initialScrollOffset: 50.0);
|
||||
final List<int> paintedChildren = <int>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
itemExtent: 100.0,
|
||||
children: new List<Widget>.generate(10, (int index) {
|
||||
return new CustomPaint(
|
||||
painter: new TestCallbackPainter(onPaint: () {
|
||||
paintedChildren.add(index);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen is 600px tall and the first item starts at 200px. The first 4
|
||||
// children are visible.
|
||||
expect(paintedChildren, <int>[0, 1, 2, 3]);
|
||||
|
||||
paintedChildren.clear();
|
||||
// Move down by 1px.
|
||||
await tester.drag(find.byType(ListWheelScrollView), const Offset(0.0, -1.0));
|
||||
await tester.pump();
|
||||
|
||||
// Now the first pixel of item 5 enters the viewport.
|
||||
expect(paintedChildren, <int>[0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
testWidgets('A child is no longer painted after its last pixel leaves the viewport', (WidgetTester tester) async {
|
||||
final ScrollController controller = new ScrollController(initialScrollOffset: 250.0);
|
||||
final List<int> paintedChildren = <int>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
itemExtent: 100.0,
|
||||
children: new List<Widget>.generate(10, (int index) {
|
||||
return new CustomPaint(
|
||||
painter: new TestCallbackPainter(onPaint: () {
|
||||
paintedChildren.add(index);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// The first item is at 0px and the 600px screen is full in the
|
||||
// **untransformed plane's viewport painting coordinates**
|
||||
expect(paintedChildren, <int>[0, 1, 2, 3, 4, 5]);
|
||||
|
||||
paintedChildren.clear();
|
||||
// Go down another 99px.
|
||||
controller.jumpTo(349.0);
|
||||
await tester.pump();
|
||||
|
||||
// One more item now visible with the last pixel of 0 showing.
|
||||
expect(paintedChildren, <int>[0, 1, 2, 3, 4, 5, 6]);
|
||||
|
||||
paintedChildren.clear();
|
||||
// Go down one more pixel.
|
||||
controller.jumpTo(350.0);
|
||||
await tester.pump();
|
||||
|
||||
// Item 0 no longer visible.
|
||||
expect(paintedChildren, <int>[1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
testWidgets('Default middle transform', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
itemExtent: 100.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent;
|
||||
expect(viewport, paints..transform(
|
||||
matrix4: equals(<dynamic>[
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
-1.2 /* origin centering multiplied */, -0.9/* origin centering multiplied*/, 1.0, -0.003 /* inverse of perspective */,
|
||||
moreOrLessEquals(0.0), moreOrLessEquals(0.0), 0.0, moreOrLessEquals(1.0),
|
||||
]),
|
||||
));
|
||||
});
|
||||
|
||||
testWidgets('Scrolling, diameterRatio, perspective all changes matrix', (WidgetTester tester) async {
|
||||
final ScrollController controller = new ScrollController(initialScrollOffset: 200.0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
itemExtent: 100.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent;
|
||||
expect(viewport, paints..transform(
|
||||
matrix4: equals(<dynamic>[
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
moreOrLessEquals(-0.41042417199080244), moreOrLessEquals(0.6318744917928065), moreOrLessEquals(0.3420201433256687), moreOrLessEquals(-0.0010260604299770061),
|
||||
moreOrLessEquals(-1.12763114494309), moreOrLessEquals(-1.1877435020329863), moreOrLessEquals(0.9396926207859084), moreOrLessEquals(-0.0028190778623577253),
|
||||
moreOrLessEquals(166.54856463138663), moreOrLessEquals(-62.20844875763376), moreOrLessEquals(-138.79047052615562), moreOrLessEquals(1.4163714115784667),
|
||||
]),
|
||||
));
|
||||
|
||||
// Increase diameter.
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
diameterRatio: 3.0,
|
||||
itemExtent: 100.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(viewport, paints..transform(
|
||||
matrix4: equals(<dynamic>[
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
moreOrLessEquals(-0.26954971336161726), moreOrLessEquals(0.7722830529455648), moreOrLessEquals(0.22462476113468105), moreOrLessEquals(-0.0006738742834040432),
|
||||
moreOrLessEquals(-1.1693344055601331), moreOrLessEquals(-1.101625565304781), moreOrLessEquals(0.9744453379667777), moreOrLessEquals(-0.002923336013900333),
|
||||
moreOrLessEquals(108.46394900436536), moreOrLessEquals(-113.14792465797223), moreOrLessEquals(-90.38662417030434), moreOrLessEquals(1.2711598725109134),
|
||||
]),
|
||||
));
|
||||
|
||||
// Decrease perspective.
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
perspective: 0.0001,
|
||||
itemExtent: 100.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(viewport, paints..transform(
|
||||
matrix4: equals(<dynamic>[
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
moreOrLessEquals(-0.01368080573302675), moreOrLessEquals(0.9294320164861384), moreOrLessEquals(0.3420201433256687), moreOrLessEquals(-0.000034202014332566874),
|
||||
moreOrLessEquals(-0.03758770483143634), moreOrLessEquals(-0.370210921949246), moreOrLessEquals(0.9396926207859084), moreOrLessEquals(-0.00009396926207859085),
|
||||
moreOrLessEquals(5.551618821046304), moreOrLessEquals(-182.95615811538906), moreOrLessEquals(-138.79047052615562), moreOrLessEquals(1.0138790470526158),
|
||||
]),
|
||||
));
|
||||
|
||||
// Scroll a bit.
|
||||
controller.jumpTo(300.0);
|
||||
await tester.pumpWidget(
|
||||
new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new ListWheelScrollView(
|
||||
controller: controller,
|
||||
itemExtent: 100.0,
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
width: 200.0,
|
||||
child: const Center(
|
||||
child: const Text('blah'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(viewport, paints..transform(
|
||||
matrix4: equals(<dynamic>[
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
-0.6, moreOrLessEquals(0.41602540378443875), moreOrLessEquals(0.5), moreOrLessEquals(-0.0015),
|
||||
moreOrLessEquals(-1.0392304845413265), moreOrLessEquals(-1.2794228634059948), moreOrLessEquals(0.8660254037844387), moreOrLessEquals(-0.0025980762113533163),
|
||||
moreOrLessEquals(276.46170927520404), moreOrLessEquals(-52.46133917892857), moreOrLessEquals(-230.38475772933677), moreOrLessEquals(1.69115427318801),
|
||||
]),
|
||||
));
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user