@@ -27,7 +27,7 @@ void main() {
27
27
},
28
28
);
29
29
await tester.pumpWidget (widget);
30
- await tester.tap (find.byType ( FloatingActionButton ));
30
+ await tester.tap (find.byIcon ( Icons .add ));
31
31
await tester.pumpAndSettle ();
32
32
33
33
final result = await observerController.dispatchOnceObserve (
@@ -164,6 +164,118 @@ void main() {
164
164
165
165
scrollController.dispose ();
166
166
});
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
+ });
167
279
}
168
280
169
281
class ChatListView extends StatefulWidget {
@@ -173,12 +285,14 @@ class ChatListView extends StatefulWidget {
173
285
required this .observerController,
174
286
required this .chatScrollObserver,
175
287
this .onReceiveScrollNotification,
288
+ this .itemBuilder,
176
289
}) : super (key: key);
177
290
178
291
final ScrollController scrollController;
179
292
final ListObserverController observerController;
180
293
final ChatScrollObserver chatScrollObserver;
181
294
final Function ()? onReceiveScrollNotification;
295
+ final NullableIndexedWidgetBuilder ? itemBuilder;
182
296
183
297
@override
184
298
State <ChatListView > createState () => ChatListViewState ();
@@ -194,17 +308,28 @@ class ChatListViewState extends State<ChatListView> {
194
308
home: Scaffold (
195
309
appBar: AppBar (),
196
310
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
+ ],
208
333
),
209
334
),
210
335
);
@@ -219,12 +344,13 @@ class ChatListViewState extends State<ChatListView> {
219
344
observer: widget.chatScrollObserver,
220
345
),
221
346
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
+ },
228
354
),
229
355
);
230
356
resultWidget = NotificationListener <ScrollNotification >(
0 commit comments