-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlist_wheel_viewport.dart
1059 lines (961 loc) · 38.7 KB
/
list_wheel_viewport.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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));
}