Files
PiliPlus/lib/common/widgets/chat_list_view.dart
2026-02-27 15:55:40 +08:00

385 lines
12 KiB
Dart

// 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ChatListView extends BoxScrollView {
ChatListView.separated({
super.key,
super.scrollDirection,
super.controller,
super.primary,
super.physics,
super.padding,
required NullableIndexedWidgetBuilder itemBuilder,
@Deprecated(
'Use findItemIndexCallback instead. '
'findChildIndexCallback returns child indices (which include separators), '
'while findItemIndexCallback returns item indices (which do not). '
'If you were multiplying results by 2 to account for separators, '
'you can remove that workaround when migrating to findItemIndexCallback. '
'This feature was deprecated after v3.37.0-1.0.pre.',
)
ChildIndexGetter? findChildIndexCallback,
ChildIndexGetter? findItemIndexCallback,
required IndexedWidgetBuilder separatorBuilder,
required int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
super.cacheExtent,
super.dragStartBehavior,
super.keyboardDismissBehavior,
super.restorationId,
super.clipBehavior,
super.hitTestBehavior,
}) : assert(itemCount >= 0),
assert(
findItemIndexCallback == null || findChildIndexCallback == null,
'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
),
childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
final int itemIndex = index ~/ 2;
if (index.isEven) {
return itemBuilder(context, itemIndex);
}
return separatorBuilder(context, itemIndex);
},
findChildIndexCallback: findItemIndexCallback != null
? (Key key) {
final int? itemIndex = findItemIndexCallback(key);
return itemIndex == null ? null : itemIndex * 2;
}
: findChildIndexCallback,
childCount: _computeActualChildCount(itemCount),
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
semanticIndexCallback: (Widget widget, int index) {
return index.isEven ? index ~/ 2 : null;
},
),
super(semanticChildCount: itemCount, reverse: true);
final SliverChildDelegate childrenDelegate;
@override
Widget buildChildLayout(BuildContext context) {
return SliverChatList(delegate: childrenDelegate);
}
static int _computeActualChildCount(int itemCount) {
return math.max(0, itemCount * 2 - 1);
}
}
class SliverChatList extends SliverMultiBoxAdaptorWidget {
const SliverChatList({super.key, required super.delegate});
@override
SliverMultiBoxAdaptorElement createElement() =>
SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);
@override
RenderSliverChatList createRenderObject(BuildContext context) {
final element = context as SliverMultiBoxAdaptorElement;
return RenderSliverChatList(childManager: element);
}
}
class RenderSliverChatList extends RenderSliverMultiBoxAdaptor
with ExtendedRenderObjectMixin {
RenderSliverChatList({required super.childManager});
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
childManager
..didStartLayout()
..setDidUnderflow(false);
final double scrollOffset =
constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
var leadingGarbage = 0;
var trailingGarbage = 0;
var reachedEnd = false;
if (firstChild == null) {
if (!addInitialChild()) {
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
///
handleCloseToTrailingBegin();
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
RenderBox? earliestUsefulChild = firstChild;
if (childScrollOffset(firstChild!) == null) {
var leadingChildrenWithoutLayoutOffset = 0;
while (earliestUsefulChild != null &&
childScrollOffset(earliestUsefulChild) == null) {
earliestUsefulChild = childAfter(earliestUsefulChild);
leadingChildrenWithoutLayoutOffset += 1;
}
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
if (firstChild == null) {
if (!addInitialChild()) {
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
}
earliestUsefulChild = firstChild;
for (
double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!
) {
earliestUsefulChild = insertAndLayoutLeadingChild(
childConstraints,
parentUsesSize: true,
);
if (earliestUsefulChild == null) {
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
firstChild!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
geometry = SliverGeometry(scrollOffsetCorrection: -scrollOffset);
return;
}
}
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
final childParentData =
earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
if (scrollOffset < precisionErrorTolerance) {
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
earliestUsefulChild = insertAndLayoutLeadingChild(
childConstraints,
parentUsesSize: true,
);
assert(earliestUsefulChild != null);
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
final childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
return;
}
}
}
assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
var inLayoutRange = true;
var child = earliestUsefulChild;
int index = indexOf(child!);
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
bool advance() {
assert(child != null);
if (child == trailingChildWithLayout) {
inLayoutRange = false;
}
child = childAfter(child!);
if (child == null) {
inLayoutRange = false;
}
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
child = insertAndLayoutChild(
childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
return false;
}
} else {
child!.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final childParentData =
child!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent =
childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
geometry = SliverGeometry(scrollExtent: extent, maxPaintExtent: extent);
return;
}
}
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
if (child != null) {
child = childAfter(child!);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child!);
}
}
collectGarbage(leadingGarbage, trailingGarbage);
assert(debugAssertChildListIsNonEmptyAndContiguous());
final double estimatedMaxScrollOffset;
///
endScrollOffset = handleCloseToTrailingEnd(endScrollOffset);
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild!),
lastIndex: indexOf(lastChild!),
leadingScrollOffset: childScrollOffset(firstChild!),
trailingScrollOffset: endScrollOffset,
);
assert(
estimatedMaxScrollOffset >=
endScrollOffset - childScrollOffset(firstChild!)!,
);
}
final double firstChildScrollOffset = childScrollOffset(firstChild!)!;
double paintExtent = calculatePaintOffset(
constraints,
from: firstChildScrollOffset,
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: firstChildScrollOffset,
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint =
constraints.scrollOffset + constraints.remainingPaintExtent;
///
paintExtent += _closeToTrailingDistance;
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
hasVisualOverflow:
endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
);
if (estimatedMaxScrollOffset == endScrollOffset) {
childManager.setDidUnderflow(true);
}
childManager.didFinishLayout();
}
}
/// from https://github.com/fluttercandies/extended_list
mixin ExtendedRenderObjectMixin on RenderSliverMultiBoxAdaptor {
void handleCloseToTrailingBegin() {
_closeToTrailingDistance = 0.0;
}
double handleCloseToTrailingEnd(double endScrollOffset) {
if (endScrollOffset < constraints.remainingPaintExtent) {
final double distance =
constraints.remainingPaintExtent - endScrollOffset;
_closeToTrailingDistance = distance;
return constraints.remainingPaintExtent;
}
return endScrollOffset;
}
double _closeToTrailingDistance = 0.0;
@override
double? childScrollOffset(RenderObject child) {
return (super.childScrollOffset(child) ?? 0.0) + _closeToTrailingDistance;
}
}