33import java .time .Duration ;
44import java .util .Optional ;
55import java .util .function .Function ;
6+ import java .util .function .Supplier ;
67
8+ import javafx .beans .property .SimpleBooleanProperty ;
79import javafx .beans .value .ObservableObjectValue ;
810import javafx .geometry .Bounds ;
911import javafx .scene .control .IndexRange ;
1012
13+ import org .reactfx .EventStream ;
1114import org .reactfx .EventStreams ;
1215import org .reactfx .Subscription ;
1316import org .reactfx .collection .LiveList ;
1417import org .reactfx .collection .MemoizationList ;
18+ import org .reactfx .util .Tuple3 ;
1519import org .reactfx .value .Val ;
1620import org .reactfx .value .ValBase ;
1721
@@ -56,9 +60,14 @@ public SizeTracker(
5660 this .viewportBounds = viewportBounds ;
5761 this .cells = lazyCells ;
5862 this .breadths = lazyCells .map (orientation ::minBreadth ).memoize ();
59- this .maxKnownMinBreadth = breadths .memoizedItems ()
60- .reduce (Math ::max )
61- .orElseConst (0.0 );
63+ LiveList <Double > knownBreadths = this .breadths .memoizedItems ();
64+
65+ this .maxKnownMinBreadth = Val .create (
66+ () -> knownBreadths .stream ().mapToDouble ( Double ::doubleValue ).max ().orElse (0.0 ),
67+ // skips spurious events resulting from cell replacement (delete then add again)
68+ knownBreadths .changes ().successionEnds ( Duration .ofMillis ( 15 ) )
69+ );
70+
6271 this .breadthForCells = Val .combine (
6372 maxKnownMinBreadth ,
6473 viewportBounds ,
@@ -69,38 +78,53 @@ public SizeTracker(
6978 .map (breadth -> cell -> orientation .prefLength (cell , breadth ));
7079
7180 this .lengths = cells .mapDynamic (lengthFn ).memoize ();
72-
7381 LiveList <Double > knownLengths = this .lengths .memoizedItems ();
74- Val <Double > sumOfKnownLengths = knownLengths .reduce ((a , b ) -> a + b ).orElseConst (0.0 );
75- Val <Integer > knownLengthCount = knownLengths .sizeProperty ();
76-
77- this .averageLengthEstimate = Val .create (
78- () -> {
79- // make sure to use pref lengths of all present cells
80- for (int i = 0 ; i < cells .getMemoizedCount (); ++i ) {
81- int j = cells .indexOfMemoizedItem (i );
82- lengths .force (j , j + 1 );
83- }
84-
85- int count = knownLengthCount .getValue ();
86- return count == 0
87- ? null
88- : sumOfKnownLengths .getValue () / count ;
89- },
90- sumOfKnownLengths , knownLengthCount );
91-
92- this .totalLengthEstimate = Val .combine (
93- averageLengthEstimate , cells .sizeProperty (),
94- (avg , n ) -> n * avg );
82+
83+ Supplier <Double > averageKnownLengths = () -> {
84+ // make sure to use pref lengths of all present cells
85+ for (int i = 0 ; i < cells .getMemoizedCount (); ++i ) {
86+ int j = cells .indexOfMemoizedItem (i );
87+ lengths .force (j , j + 1 );
88+ }
89+
90+ return knownLengths .stream ()
91+ .mapToDouble ( Double ::doubleValue )
92+ .sorted ().average ()
93+ .orElse ( 0.0 );
94+ };
95+
96+ final int AVERAGE_LENGTH = 0 , TOTAL_LENGTH = 1 ;
97+ Val <double [/*average,total*/ ]> lengthStats = Val .wrap (
98+ knownLengths .changes ().or ( cells .sizeProperty ().values () )
99+ .successionEnds ( Duration .ofMillis ( 15 ) ) // reduce noise
100+ .map ( e -> {
101+ double averageLength = averageKnownLengths .get ();
102+ int cellCount = e .isRight () ? e .getRight () : cells .size ();
103+ return new double [] { averageLength , cellCount * averageLength };
104+ } ).toBinding ( new double [] { 0.0 , 0.0 } )
105+ );
106+
107+ EventStream <double [/*average,total*/ ]> filteredLengthStats ;
108+ // briefly hold back changes that may be from spurious events coming from cell refreshes, these
109+ // are identified as those where the estimated total length is less than the previous event.
110+ filteredLengthStats = new PausableSuccessionStream <>( lengthStats .changes (), Duration .ofMillis (1000 ), chg -> {
111+ double [/*average,total*/ ] oldStats = chg .getOldValue ();
112+ double [/*average,total*/ ] newStats = chg .getNewValue ();
113+ if ( newStats [TOTAL_LENGTH ] < oldStats [TOTAL_LENGTH ] ) {
114+ return false ; // don't emit yet, first wait & prefer newer values
115+ }
116+ return true ;
117+ } )
118+ .map ( chg -> chg .getNewValue () );
119+
120+ this .averageLengthEstimate = Val .wrap ( filteredLengthStats .map ( stats -> stats [AVERAGE_LENGTH ] ).toBinding ( 0.0 ) );
121+ this .totalLengthEstimate = Val .wrap ( filteredLengthStats .map ( stats -> stats [TOTAL_LENGTH ] ).toBinding ( 0.0 ) );
95122
96123 Val <Integer > firstVisibleIndex = Val .create (
97124 () -> cells .getMemoizedCount () == 0 ? null : cells .indexOfMemoizedItem (0 ),
98125 cells , cells .memoizedItems ()); // need to observe cells.memoizedItems()
99126 // as well, because they may change without a change in cells.
100127
101- Val <? extends Cell <?, ?>> firstVisibleCell = cells .memoizedItems ()
102- .collapse (visCells -> visCells .isEmpty () ? null : visCells .get (0 ));
103-
104128 Val <Integer > knownLengthCountBeforeFirstVisibleCell = Val .create (() -> {
105129 return firstVisibleIndex .getOpt ()
106130 .map (i -> lengths .getMemoizedCountBefore (Math .min (i , lengths .size ())))
@@ -117,17 +141,23 @@ public SizeTracker(
117141 averageLengthEstimate ,
118142 (firstIdx , knownCnt , avgLen ) -> (firstIdx - knownCnt ) * avgLen );
119143
120- Val <Double > firstCellMinY = firstVisibleCell .flatMap (orientation ::minYProperty );
144+ Val <Double > firstCellMinY = cells .memoizedItems ()
145+ .collapse (visCells -> visCells .isEmpty () ? null : visCells .get (0 ))
146+ .flatMap (orientation ::minYProperty );
121147
122- lengthOffsetEstimate = Val . wrap ( EventStreams .combine (
148+ EventStream < Tuple3 < Double , Double , Double >> lengthOffsetStream = EventStreams .combine (
123149 totalKnownLengthBeforeFirstVisibleCell .values (),
124150 unknownLengthEstimateBeforeFirstVisibleCell .values (),
125151 firstCellMinY .values ()
126- )
127- .filter ( t3 -> t3 .test ( (a ,b ,minY ) -> a != null && b != null && minY != null ) )
128- .thenRetainLatestFor ( Duration .ofMillis ( 1 ) )
129- .map ( t3 -> t3 .map ( (a ,b ,minY ) -> Double .valueOf ( a + b - minY ) ) )
130- .toBinding ( 0.0 ) );
152+ );
153+
154+ lengthOffsetEstimate = Val .wrap (
155+ // skip spurious events resulting from cell replacement (delete then add again), except
156+ // when immediateUpdate is true: activated via updateNextLengthOffsetEstimateImmediately()
157+ new PausableSuccessionStream <>( lengthOffsetStream , Duration .ofMillis (15 ), immediateUpdate )
158+ .filter ( t3 -> t3 .test ( (a ,b ,minY ) -> a != null && b != null && minY != null ) )
159+ .map ( t3 -> t3 .map ( (a ,b ,minY ) -> Double .valueOf ( Math .round ( a + b - minY ) ) ) )
160+ .toBinding ( 0.0 ) );
131161
132162 // pinning totalLengthEstimate and lengthOffsetEstimate
133163 // binds it all together and enables memoization
@@ -136,6 +166,9 @@ public SizeTracker(
136166 lengthOffsetEstimate .pin ());
137167 }
138168
169+ private SimpleBooleanProperty immediateUpdate = new SimpleBooleanProperty ();
170+ void updateNextLengthOffsetEstimateImmediately () { immediateUpdate .set ( true ); }
171+
139172 private static <T > Val <T > avoidFalseInvalidations (Val <T > src ) {
140173 return new ValBase <T >() {
141174 @ Override
0 commit comments