33import java .time .Duration ;
44import java .util .Optional ;
55import java .util .function .Function ;
6+ import java .util .function .Supplier ;
67
78import javafx .beans .property .SimpleBooleanProperty ;
89import javafx .beans .value .ObservableObjectValue ;
@@ -63,8 +64,8 @@ public SizeTracker(
6364
6465 this .maxKnownMinBreadth = Val .create (
6566 () -> knownBreadths .stream ().mapToDouble ( Double ::doubleValue ).max ().orElse (0.0 ),
66- knownBreadths . changes (). filter ( c -> knownBreadths . size () > 0 )
67- // skip zero cell events to prevent scrollbar flicker
67+ // skips spurious events resulting from cell replacement (delete then add again )
68+ knownBreadths . changes (). successionEnds ( Duration . ofMillis ( 15 ) )
6869 );
6970
7071 this .breadthForCells = Val .combine (
@@ -77,29 +78,47 @@ public SizeTracker(
7778 .map (breadth -> cell -> orientation .prefLength (cell , breadth ));
7879
7980 this .lengths = cells .mapDynamic (lengthFn ).memoize ();
80-
8181 LiveList <Double > knownLengths = this .lengths .memoizedItems ();
8282
83- this .averageLengthEstimate = Val .create (
84- () -> {
85- // make sure to use pref lengths of all present cells
86- for (int i = 0 ; i < cells .getMemoizedCount (); ++i ) {
87- int j = cells .indexOfMemoizedItem (i );
88- lengths .force (j , j + 1 );
89- }
90-
91- return knownLengths .stream ()
92- .mapToDouble ( Double ::doubleValue )
93- .average ().orElse (0.0 );
94- },
95- knownLengths .changes ().filter ( c -> knownLengths .size () > 0 )
96- // skip zero cell events to prevent scrollbar flicker
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 } )
97105 );
98106
99- this .totalLengthEstimate = Val .combine (
100- averageLengthEstimate ,
101- cells .sizeProperty ().filter ( s -> s > 0 ),
102- (avg , n ) -> n * avg );
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 ) );
103122
104123 Val <Integer > firstVisibleIndex = Val .create (
105124 () -> cells .getMemoizedCount () == 0 ? null : cells .indexOfMemoizedItem (0 ),
0 commit comments