@@ -27,7 +27,7 @@ void main() {
2727 },
2828 );
2929 await tester.pumpWidget (widget);
30- await tester.tap (find.byType ( FloatingActionButton ));
30+ await tester.tap (find.byIcon ( Icons .add ));
3131 await tester.pumpAndSettle ();
3232
3333 final result = await observerController.dispatchOnceObserve (
@@ -164,6 +164,118 @@ void main() {
164164
165165 scrollController.dispose ();
166166 });
167+
168+ testWidgets ('Keeping position with customAdjustPositionDelta' ,
169+ (tester) async {
170+ final scrollController = ScrollController ();
171+ final observerController = ListObserverController (
172+ controller: scrollController,
173+ );
174+ final chatScrollObserver = ChatScrollObserver (observerController)
175+ ..fixedPositionOffset = - 1 ;
176+ Map <int , double > itemHeightMap = {};
177+ const double expandedItemHeight = 200 ;
178+ const double normalItemHeight = 100 ;
179+
180+ Widget widget = ChatListView (
181+ scrollController: scrollController,
182+ observerController: observerController,
183+ chatScrollObserver: chatScrollObserver,
184+ itemBuilder: (context, index) {
185+ if (itemHeightMap[index] == null ) {
186+ itemHeightMap[index] = normalItemHeight;
187+ }
188+ double itemHeight = itemHeightMap[index] ?? normalItemHeight;
189+ return SizedBox (
190+ height: itemHeight,
191+ child: Center (child: Text (index.toString ())),
192+ );
193+ },
194+ );
195+ await tester.pumpWidget (widget);
196+
197+ Future <void > setState () async {
198+ await tester.tap (find.byIcon (Icons .refresh));
199+ await tester.pumpAndSettle ();
200+ }
201+
202+ var result = await observerController.dispatchOnceObserve (
203+ isForce: true ,
204+ isDependObserveCallback: false ,
205+ );
206+ var observeResult = result.observeResult;
207+ final displayingChildModelList =
208+ observeResult? .displayingChildModelList ?? [];
209+ expect (displayingChildModelList, isNotEmpty);
210+
211+ final targetIndex = displayingChildModelList.last.index + 1 ;
212+ // Jump to targetIndex and align its bottom with the viewport bottom.
213+ observerController.jumpTo (
214+ index: targetIndex,
215+ offset: (targetOffset) {
216+ final viewportMainAxisExtent =
217+ observeResult? .firstChild? .viewportMainAxisExtent ?? 0 ;
218+ return viewportMainAxisExtent - normalItemHeight;
219+ },
220+ );
221+ await tester.pumpAndSettle ();
222+ await tester.pump (observerController.observeIntervalForScrolling);
223+
224+ // Check if the last item is aligned with the viewport bottom.
225+ result = await observerController.dispatchOnceObserve (
226+ isForce: true ,
227+ isDependObserveCallback: false ,
228+ );
229+ observeResult = result.observeResult;
230+ var lastDisplayingChildModel = observeResult? .displayingChildModelList.last;
231+ expect (lastDisplayingChildModel? .index, targetIndex);
232+ expect (lastDisplayingChildModel? .trailingMarginToViewport, 0 );
233+
234+ // Expand the last item.
235+ itemHeightMap[targetIndex] = expandedItemHeight;
236+ final refItemIndex = targetIndex;
237+ await chatScrollObserver.standby (
238+ mode: ChatScrollObserverHandleMode .specified,
239+ refIndexType: ChatScrollObserverRefIndexType .itemIndex,
240+ refItemIndex: refItemIndex,
241+ refItemIndexAfterUpdate: refItemIndex,
242+ customAdjustPositionDelta: (model) {
243+ return expandedItemHeight - normalItemHeight;
244+ },
245+ );
246+ await setState ();
247+ result = await observerController.dispatchOnceObserve (
248+ isForce: true ,
249+ isDependObserveCallback: false ,
250+ );
251+ observeResult = result.observeResult;
252+ lastDisplayingChildModel = observeResult? .displayingChildModelList.last;
253+ expect (lastDisplayingChildModel? .index, targetIndex);
254+ expect (lastDisplayingChildModel? .trailingMarginToViewport, 0 );
255+
256+ // Restore the last item to normal height.
257+ itemHeightMap[targetIndex] = normalItemHeight;
258+ await chatScrollObserver.standby (
259+ mode: ChatScrollObserverHandleMode .specified,
260+ refIndexType: ChatScrollObserverRefIndexType .itemIndex,
261+ refItemIndex: refItemIndex,
262+ refItemIndexAfterUpdate: refItemIndex,
263+ customAdjustPositionDelta: (model) {
264+ return normalItemHeight - expandedItemHeight;
265+ },
266+ );
267+ await setState ();
268+ result = await observerController.dispatchOnceObserve (
269+ isForce: true ,
270+ isDependObserveCallback: false ,
271+ );
272+ observeResult = result.observeResult;
273+ lastDisplayingChildModel = observeResult? .displayingChildModelList.last;
274+ expect (lastDisplayingChildModel? .index, targetIndex);
275+ expect (lastDisplayingChildModel? .trailingMarginToViewport, 0 );
276+
277+ scrollController.dispose ();
278+ });
167279}
168280
169281class ChatListView extends StatefulWidget {
@@ -173,12 +285,14 @@ class ChatListView extends StatefulWidget {
173285 required this .observerController,
174286 required this .chatScrollObserver,
175287 this .onReceiveScrollNotification,
288+ this .itemBuilder,
176289 }) : super (key: key);
177290
178291 final ScrollController scrollController;
179292 final ListObserverController observerController;
180293 final ChatScrollObserver chatScrollObserver;
181294 final Function ()? onReceiveScrollNotification;
295+ final NullableIndexedWidgetBuilder ? itemBuilder;
182296
183297 @override
184298 State <ChatListView > createState () => ChatListViewState ();
@@ -194,17 +308,28 @@ class ChatListViewState extends State<ChatListView> {
194308 home: Scaffold (
195309 appBar: AppBar (),
196310 body: _buildListView (),
197- floatingActionButton: FloatingActionButton (
198- onPressed: () {
199- widget.chatScrollObserver.standby (changeCount: 4 );
200- setState (() {
201- dataList.insert (0 , '-1' );
202- dataList.insert (0 , '-2' );
203- dataList.insert (0 , '-3' );
204- dataList.insert (0 , '-4' );
205- });
206- },
207- child: const Icon (Icons .add),
311+ floatingActionButton: Column (
312+ verticalDirection: VerticalDirection .up,
313+ children: [
314+ FloatingActionButton (
315+ onPressed: () {
316+ widget.chatScrollObserver.standby (changeCount: 4 );
317+ setState (() {
318+ dataList.insert (0 , '-1' );
319+ dataList.insert (0 , '-2' );
320+ dataList.insert (0 , '-3' );
321+ dataList.insert (0 , '-4' );
322+ });
323+ },
324+ child: const Icon (Icons .add),
325+ ),
326+ FloatingActionButton (
327+ onPressed: () {
328+ setState (() {});
329+ },
330+ child: const Icon (Icons .refresh),
331+ ),
332+ ],
208333 ),
209334 ),
210335 );
@@ -219,12 +344,13 @@ class ChatListViewState extends State<ChatListView> {
219344 observer: widget.chatScrollObserver,
220345 ),
221346 controller: widget.scrollController,
222- itemBuilder: (context, index) {
223- return SizedBox (
224- height: 100 ,
225- child: Center (child: Text (dataList[index])),
226- );
227- },
347+ itemBuilder: widget.itemBuilder ??
348+ (context, index) {
349+ return SizedBox (
350+ height: 100 ,
351+ child: Center (child: Text (dataList[index])),
352+ );
353+ },
228354 ),
229355 );
230356 resultWidget = NotificationListener <ScrollNotification >(
0 commit comments