Skip to content

Commit 92676fd

Browse files
committed
scroll [nfc]: Copy RenderViewport.performLayout and friends from upstream
Some of the behavior we'd like to customize isn't currently cleanly exposed to subclasses any more than it is to parent widgets passing constructor arguments. In particular, we'll want to change a few bits of logic in [RenderViewport.performLayout], replacing the handling of the `anchor` field with something more flexible. In order to do that, we'll start from a copy of that method, so that we can edit the copy. Then the base class's `performLayout` refers to a private helper method `_attemptLayout`, so we need a copy of that too; and they each refer to a number of private fields, so we need copies of those too; and to make those work correctly, we need copies of all the other members that refer to those fields, so that they're all referring correctly to the same version of those fields (namely the one on the subclass) rather than to a mix of the versions on the base class and those on the subclass. Fortunately, flood-filling that graph of members which refer to private members, which are referred to by other members, etc., terminates with a connected component which is... not small, but a lot smaller and less unwieldy than if we had to copy the whole upstream file these are defined in.
1 parent 8702ad3 commit 92676fd

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed

lib/widgets/scrolling.dart

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter/foundation.dart';
14
import 'package:flutter/material.dart';
25
import 'package:flutter/rendering.dart';
36

@@ -322,6 +325,8 @@ class MessageListViewport extends CustomPaintOrderViewport {
322325

323326
/// The version of [RenderViewport] that underlies [MessageListViewport]
324327
/// and [MessageListScrollView].
328+
// TODO(upstream): Devise upstream APIs to obviate the duplicated code here;
329+
// use `git log -L` to see what edits we've made locally.
325330
class RenderMessageListViewport extends RenderCustomPaintOrderViewport {
326331
RenderMessageListViewport({
327332
super.axisDirection,
@@ -335,4 +340,211 @@ class RenderMessageListViewport extends RenderCustomPaintOrderViewport {
335340
super.clipBehavior,
336341
required super.paintOrder_,
337342
});
343+
344+
double? _calculatedCacheExtent;
345+
346+
@override
347+
Rect describeSemanticsClip(RenderSliver? child) {
348+
if (_calculatedCacheExtent == null) {
349+
return semanticBounds;
350+
}
351+
352+
switch (axis) {
353+
case Axis.vertical:
354+
return Rect.fromLTRB(
355+
semanticBounds.left,
356+
semanticBounds.top - _calculatedCacheExtent!,
357+
semanticBounds.right,
358+
semanticBounds.bottom + _calculatedCacheExtent!,
359+
);
360+
case Axis.horizontal:
361+
return Rect.fromLTRB(
362+
semanticBounds.left - _calculatedCacheExtent!,
363+
semanticBounds.top,
364+
semanticBounds.right + _calculatedCacheExtent!,
365+
semanticBounds.bottom,
366+
);
367+
}
368+
}
369+
370+
static const int _maxLayoutCyclesPerChild = 10;
371+
372+
// Out-of-band data computed during layout.
373+
late double _minScrollExtent;
374+
late double _maxScrollExtent;
375+
bool _hasVisualOverflow = false;
376+
377+
@override
378+
void performLayout() {
379+
// Ignore the return value of applyViewportDimension because we are
380+
// doing a layout regardless.
381+
switch (axis) {
382+
case Axis.vertical:
383+
offset.applyViewportDimension(size.height);
384+
case Axis.horizontal:
385+
offset.applyViewportDimension(size.width);
386+
}
387+
388+
if (center == null) {
389+
assert(firstChild == null);
390+
_minScrollExtent = 0.0;
391+
_maxScrollExtent = 0.0;
392+
_hasVisualOverflow = false;
393+
offset.applyContentDimensions(0.0, 0.0);
394+
return;
395+
}
396+
assert(center!.parent == this);
397+
398+
final (double mainAxisExtent, double crossAxisExtent) = switch (axis) {
399+
Axis.vertical => (size.height, size.width),
400+
Axis.horizontal => (size.width, size.height),
401+
};
402+
403+
final double centerOffsetAdjustment = center!.centerOffsetAdjustment;
404+
final int maxLayoutCycles = _maxLayoutCyclesPerChild * childCount;
405+
406+
double correction;
407+
int count = 0;
408+
do {
409+
correction = _attemptLayout(
410+
mainAxisExtent,
411+
crossAxisExtent,
412+
offset.pixels + centerOffsetAdjustment,
413+
);
414+
if (correction != 0.0) {
415+
offset.correctBy(correction);
416+
} else {
417+
if (offset.applyContentDimensions(
418+
math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
419+
math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
420+
)) {
421+
break;
422+
}
423+
}
424+
count += 1;
425+
} while (count < maxLayoutCycles);
426+
assert(() {
427+
if (count >= maxLayoutCycles) {
428+
assert(count != 1);
429+
throw FlutterError(
430+
'A RenderViewport exceeded its maximum number of layout cycles.\n'
431+
'RenderViewport render objects, during layout, can retry if either their '
432+
'slivers or their ViewportOffset decide that the offset should be corrected '
433+
'to take into account information collected during that layout.\n'
434+
'In the case of this RenderViewport object, however, this happened $count '
435+
'times and still there was no consensus on the scroll offset. This usually '
436+
'indicates a bug. Specifically, it means that one of the following three '
437+
'problems is being experienced by the RenderViewport object:\n'
438+
' * One of the RenderSliver children or the ViewportOffset have a bug such'
439+
' that they always think that they need to correct the offset regardless.\n'
440+
' * Some combination of the RenderSliver children and the ViewportOffset'
441+
' have a bad interaction such that one applies a correction then another'
442+
' applies a reverse correction, leading to an infinite loop of corrections.\n'
443+
' * There is a pathological case that would eventually resolve, but it is'
444+
' so complicated that it cannot be resolved in any reasonable number of'
445+
' layout passes.',
446+
);
447+
}
448+
return true;
449+
}());
450+
}
451+
452+
double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
453+
assert(!mainAxisExtent.isNaN);
454+
assert(mainAxisExtent >= 0.0);
455+
assert(crossAxisExtent.isFinite);
456+
assert(crossAxisExtent >= 0.0);
457+
assert(correctedOffset.isFinite);
458+
_minScrollExtent = 0.0;
459+
_maxScrollExtent = 0.0;
460+
_hasVisualOverflow = false;
461+
462+
// centerOffset is the offset from the leading edge of the RenderViewport
463+
// to the zero scroll offset (the line between the forward slivers and the
464+
// reverse slivers).
465+
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
466+
final double reverseDirectionRemainingPaintExtent = clampDouble(
467+
centerOffset,
468+
0.0,
469+
mainAxisExtent,
470+
);
471+
final double forwardDirectionRemainingPaintExtent = clampDouble(
472+
mainAxisExtent - centerOffset,
473+
0.0,
474+
mainAxisExtent,
475+
);
476+
477+
_calculatedCacheExtent = switch (cacheExtentStyle) {
478+
CacheExtentStyle.pixel => cacheExtent,
479+
CacheExtentStyle.viewport => mainAxisExtent * cacheExtent!,
480+
};
481+
482+
final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!;
483+
final double centerCacheOffset = centerOffset + _calculatedCacheExtent!;
484+
final double reverseDirectionRemainingCacheExtent = clampDouble(
485+
centerCacheOffset,
486+
0.0,
487+
fullCacheExtent,
488+
);
489+
final double forwardDirectionRemainingCacheExtent = clampDouble(
490+
fullCacheExtent - centerCacheOffset,
491+
0.0,
492+
fullCacheExtent,
493+
);
494+
495+
final RenderSliver? leadingNegativeChild = childBefore(center!);
496+
497+
if (leadingNegativeChild != null) {
498+
// negative scroll offsets
499+
final double result = layoutChildSequence(
500+
child: leadingNegativeChild,
501+
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
502+
overlap: 0.0,
503+
layoutOffset: forwardDirectionRemainingPaintExtent,
504+
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
505+
mainAxisExtent: mainAxisExtent,
506+
crossAxisExtent: crossAxisExtent,
507+
growthDirection: GrowthDirection.reverse,
508+
advance: childBefore,
509+
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
510+
cacheOrigin: clampDouble(mainAxisExtent - centerOffset, -_calculatedCacheExtent!, 0.0),
511+
);
512+
if (result != 0.0) {
513+
return -result;
514+
}
515+
}
516+
517+
// positive scroll offsets
518+
return layoutChildSequence(
519+
child: center,
520+
scrollOffset: math.max(0.0, -centerOffset),
521+
overlap: leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
522+
layoutOffset:
523+
centerOffset >= mainAxisExtent ? centerOffset : reverseDirectionRemainingPaintExtent,
524+
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
525+
mainAxisExtent: mainAxisExtent,
526+
crossAxisExtent: crossAxisExtent,
527+
growthDirection: GrowthDirection.forward,
528+
advance: childAfter,
529+
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
530+
cacheOrigin: clampDouble(centerOffset, -_calculatedCacheExtent!, 0.0),
531+
);
532+
}
533+
534+
@override
535+
bool get hasVisualOverflow => _hasVisualOverflow;
536+
537+
@override
538+
void updateOutOfBandData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
539+
switch (growthDirection) {
540+
case GrowthDirection.forward:
541+
_maxScrollExtent += childLayoutGeometry.scrollExtent;
542+
case GrowthDirection.reverse:
543+
_minScrollExtent -= childLayoutGeometry.scrollExtent;
544+
}
545+
if (childLayoutGeometry.hasVisualOverflow) {
546+
_hasVisualOverflow = true;
547+
}
548+
}
549+
338550
}

0 commit comments

Comments
 (0)