diff --git a/lib/common/widgets/chat_list_view.dart b/lib/common/widgets/chat_list_view.dart new file mode 100644 index 000000000..84c7efc89 --- /dev/null +++ b/lib/common/widgets/chat_list_view.dart @@ -0,0 +1,384 @@ +// 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; + } +} diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 3a0e1b96d..dd411ae68 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -73,6 +73,30 @@ class WhisperDetailController extends CommonListController { int? msgType, int? index, }) async { + // debug + // if (loadingState.value case Success(:final response)) { + // final list = List.of(response ?? []) + // ..insert( + // 0, + // Msg.create()..mergeFromProto3Json({ + // "senderUid": "${account.mid}", + // "receiverType": 1, + // "receiverId": "$mid", + // "msgType": msgType, + // "content": jsonEncode({"content": message}), + // "msgSeqno": "1", + // "timestamp": "${DateTime.now().millisecondsSinceEpoch ~/ 1000}", + // "atUids": ["0"], + // "msgKey": "2", + // "msgSource": msgType, + // }), + // ); + // loadingState.value = Success(list); + // } + // onClearText(); + // scrollController.jumpToTop(); + // SmartDialog.showToast('发送成功'); + // return; assert((message != null) ^ (picMsg != null)); if (_isSending) return; _isSending = true; diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 1cdc836cd..e62751487 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io' show File; +import 'package:PiliPlus/common/widgets/chat_list_view.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -153,9 +154,7 @@ class _WhisperDetailPageState Loading() => loadingWidget, Success(:final response) => response != null && response.isNotEmpty - ? ListView.separated( - shrinkWrap: true, - reverse: true, + ? ChatListView.separated( itemCount: response.length, padding: const EdgeInsets.all(14), physics: const AlwaysScrollableScrollPhysics(