Skip to content

Commit 76c6474

Browse files
committed
feat(ChatScrollObserver): add customAdjustPosition
1 parent 3218de0 commit 76c6474

File tree

5 files changed

+183
-1
lines changed

5 files changed

+183
-1
lines changed

lib/src/utils/src/chat/chat_observer_scroll_physics_mixin.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,27 @@ mixin ChatObserverScrollPhysicsMixin on ScrollPhysics {
3737
));
3838
return adjustPosition;
3939
}
40+
41+
// Customize the adjustPosition.
42+
double? customAdjustPosition = observer.customAdjustPosition?.call(
43+
ChatScrollObserverCustomAdjustPositionModel(
44+
oldPosition: oldPosition,
45+
newPosition: newPosition,
46+
isScrolling: isScrolling,
47+
velocity: velocity,
48+
adjustPosition: adjustPosition,
49+
observer: observer,
50+
),
51+
);
52+
if (customAdjustPosition != null) {
53+
_handlePositionCallback(ChatScrollObserverHandlePositionResultModel(
54+
type: ChatScrollObserverHandlePositionType.keepPosition,
55+
mode: observer.innerMode,
56+
changeCount: observer.changeCount,
57+
));
58+
return customAdjustPosition;
59+
}
60+
4061
final model = observer.observeRefItem();
4162
if (model == null) {
4263
_handlePositionCallback(ChatScrollObserverHandlePositionResultModel(
@@ -60,6 +81,7 @@ mixin ChatObserverScrollPhysicsMixin on ScrollPhysics {
6081
newPosition: newPosition,
6182
isScrolling: isScrolling,
6283
velocity: velocity,
84+
adjustPosition: adjustPosition,
6385
observer: observer,
6486
currentItemModel: model,
6587
),

lib/src/utils/src/chat/chat_scroll_observer.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,15 @@ class ChatScrollObserver {
7474
ChatScrollObserverHandleMode innerMode = ChatScrollObserverHandleMode.normal;
7575

7676
/// Customize the delta of the adjustPosition.
77+
///
78+
/// If the return value is null, the default processing will be performed.
7779
ChatScrollObserverCustomAdjustPositionDelta? customAdjustPositionDelta;
7880

81+
/// Customize the scroll position for new viewport dimensions.
82+
///
83+
/// If the return value is null, the default processing will be performed.
84+
ChatScrollObserverCustomAdjustPosition? customAdjustPosition;
85+
7986
/// Observation result of reference subparts after ScrollView children update.
8087
ListViewObserveDisplayingChildModel? observeRefItem() {
8188
return observerController.observeItem(
@@ -112,6 +119,7 @@ class ChatScrollObserver {
112119
int refItemRelativeIndexAfterUpdate = 0,
113120
int refItemIndex = 0,
114121
int refItemIndexAfterUpdate = 0,
122+
ChatScrollObserverCustomAdjustPosition? customAdjustPosition,
115123
ChatScrollObserverCustomAdjustPositionDelta? customAdjustPositionDelta,
116124
}) async {
117125
innerMode = mode;
@@ -200,6 +208,7 @@ class ChatScrollObserver {
200208
innerRefItemIndex = _innerRefItemIndex;
201209
innerRefItemIndexAfterUpdate = _innerRefItemIndexAfterUpdate;
202210
innerRefItemLayoutOffset = _innerRefItemLayoutOffset;
211+
this.customAdjustPosition = customAdjustPosition;
203212
this.customAdjustPositionDelta = customAdjustPositionDelta;
204213

205214
// When the heights of items are similar, the viewport will not call

lib/src/utils/src/chat/chat_scroll_observer_model.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class ChatScrollObserverCustomAdjustPositionDeltaModel {
3939
/// The current velocity of the scroll position.
4040
final double velocity;
4141

42+
/// The scroll position should be given for new viewport dimensions.
43+
final double adjustPosition;
44+
4245
/// The [ChatScrollObserver] instance.
4346
final ChatScrollObserver observer;
4447

@@ -50,7 +53,37 @@ class ChatScrollObserverCustomAdjustPositionDeltaModel {
5053
required this.newPosition,
5154
required this.isScrolling,
5255
required this.velocity,
56+
required this.adjustPosition,
5357
required this.observer,
5458
required this.currentItemModel,
5559
});
5660
}
61+
62+
class ChatScrollObserverCustomAdjustPositionModel {
63+
/// The old position.
64+
final ScrollMetrics oldPosition;
65+
66+
/// The new position.
67+
final ScrollMetrics newPosition;
68+
69+
/// Whether the ScrollView is scrolling.
70+
final bool isScrolling;
71+
72+
/// The current velocity of the scroll position.
73+
final double velocity;
74+
75+
/// The scroll position should be given for new viewport dimensions.
76+
final double adjustPosition;
77+
78+
/// The [ChatScrollObserver] instance.
79+
final ChatScrollObserver observer;
80+
81+
ChatScrollObserverCustomAdjustPositionModel({
82+
required this.oldPosition,
83+
required this.newPosition,
84+
required this.isScrolling,
85+
required this.velocity,
86+
required this.adjustPosition,
87+
required this.observer,
88+
});
89+
}

lib/src/utils/src/chat/chat_scroll_observer_typedefs.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77
import 'package:scrollview_observer/src/utils/src/chat/chat_scroll_observer_model.dart';
88

99
/// Customize the delta of the adjustPosition.
10-
typedef ChatScrollObserverCustomAdjustPositionDelta = double Function(
10+
typedef ChatScrollObserverCustomAdjustPositionDelta = double? Function(
1111
ChatScrollObserverCustomAdjustPositionDeltaModel,
1212
);
1313

14+
/// Customize the scroll position should be given new viewport dimensions.
15+
typedef ChatScrollObserverCustomAdjustPosition = double? Function(
16+
ChatScrollObserverCustomAdjustPositionModel,
17+
);
18+
1419
enum ChatScrollObserverHandlePositionType {
1520
/// Nothing will be done.
1621
none,

test/chat_scroll_observer_test.dart

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ void main() {
167167

168168
testWidgets('Keeping position with customAdjustPositionDelta',
169169
(tester) async {
170+
GlobalKey<ChatListViewState> key = GlobalKey();
170171
final scrollController = ScrollController();
171172
final observerController = ListObserverController(
172173
controller: scrollController,
@@ -178,6 +179,7 @@ void main() {
178179
const double normalItemHeight = 100;
179180

180181
Widget widget = ChatListView(
182+
key: key,
181183
scrollController: scrollController,
182184
observerController: observerController,
183185
chatScrollObserver: chatScrollObserver,
@@ -207,6 +209,35 @@ void main() {
207209
final displayingChildModelList =
208210
observeResult?.displayingChildModelList ?? [];
209211
expect(displayingChildModelList, isNotEmpty);
212+
expect(observeResult?.firstChild?.index, 0);
213+
expect(observeResult?.firstChild?.leadingMarginToViewport, 0);
214+
215+
// Check adjustPosition.
216+
key.currentState?.updateData(
217+
index: 1,
218+
needSetState: true,
219+
);
220+
double? adjustPosition;
221+
await chatScrollObserver.standby(
222+
mode: ChatScrollObserverHandleMode.specified,
223+
refIndexType: ChatScrollObserverRefIndexType.itemIndex,
224+
refItemIndex: 0,
225+
refItemIndexAfterUpdate: 0,
226+
customAdjustPositionDelta: (model) {
227+
adjustPosition = model.adjustPosition;
228+
return null;
229+
},
230+
);
231+
await tester.pumpAndSettle();
232+
expect(adjustPosition, 0);
233+
234+
result = await observerController.dispatchOnceObserve(
235+
isForce: true,
236+
isDependObserveCallback: false,
237+
);
238+
observeResult = result.observeResult;
239+
expect(observeResult?.firstChild?.index, 0);
240+
expect(observeResult?.firstChild?.leadingMarginToViewport, 0);
210241

211242
final targetIndex = displayingChildModelList.last.index + 1;
212243
// Jump to targetIndex and align its bottom with the viewport bottom.
@@ -261,6 +292,7 @@ void main() {
261292
refItemIndex: refItemIndex,
262293
refItemIndexAfterUpdate: refItemIndex,
263294
customAdjustPositionDelta: (model) {
295+
adjustPosition = model.adjustPosition;
264296
return normalItemHeight - expandedItemHeight;
265297
},
266298
);
@@ -273,6 +305,76 @@ void main() {
273305
lastDisplayingChildModel = observeResult?.displayingChildModelList.last;
274306
expect(lastDisplayingChildModel?.index, targetIndex);
275307
expect(lastDisplayingChildModel?.trailingMarginToViewport, 0);
308+
expect(adjustPosition, greaterThan(0));
309+
310+
scrollController.dispose();
311+
});
312+
313+
testWidgets('Keeping position with customAdjustPosition', (tester) async {
314+
GlobalKey<ChatListViewState> key = GlobalKey();
315+
final scrollController = ScrollController();
316+
final observerController = ListObserverController(
317+
controller: scrollController,
318+
);
319+
ChatScrollObserverHandlePositionResultModel? onHandlePositionResultModel;
320+
final chatScrollObserver = ChatScrollObserver(observerController)
321+
..onHandlePositionResultCallback = (model) {
322+
onHandlePositionResultModel = model;
323+
};
324+
325+
Widget widget = ChatListView(
326+
key: key,
327+
scrollController: scrollController,
328+
observerController: observerController,
329+
chatScrollObserver: chatScrollObserver,
330+
itemBuilder: (context, index) {
331+
final dataList = key.currentState?.dataList ?? [];
332+
return Text(dataList[index]);
333+
},
334+
);
335+
await tester.pumpWidget(widget);
336+
337+
final chatListViewState = key.currentState;
338+
expect(chatListViewState, isNotNull);
339+
340+
final dataListLength = chatListViewState?.dataList.length ?? 0;
341+
final lastIndex = dataListLength - 1;
342+
observerController.jumpTo(index: lastIndex);
343+
await tester.pumpAndSettle();
344+
345+
var result = await observerController.dispatchOnceObserve(
346+
isForce: true,
347+
isDependObserveCallback: false,
348+
);
349+
var lastModel = result.observeResult?.displayingChildModelList.last;
350+
expect(lastModel?.index, lastIndex);
351+
expect(lastModel?.trailingMarginToViewport, 0);
352+
expect(onHandlePositionResultModel, isNull);
353+
354+
chatListViewState?.updateData(
355+
index: lastIndex,
356+
needSetState: true,
357+
);
358+
await chatScrollObserver.standby(
359+
customAdjustPosition: (model) {
360+
final delta =
361+
model.newPosition.extentAfter - model.oldPosition.extentAfter;
362+
return model.adjustPosition + delta;
363+
},
364+
);
365+
await tester.pumpAndSettle();
366+
367+
result = await observerController.dispatchOnceObserve(
368+
isForce: true,
369+
isDependObserveCallback: false,
370+
);
371+
lastModel = result.observeResult?.displayingChildModelList.last;
372+
expect(lastModel?.index, lastIndex);
373+
expect(lastModel?.trailingMarginToViewport, 0);
374+
expect(
375+
onHandlePositionResultModel?.type,
376+
ChatScrollObserverHandlePositionType.keepPosition,
377+
);
276378

277379
scrollController.dispose();
278380
});
@@ -302,6 +404,17 @@ class ChatListViewState extends State<ChatListView> {
302404
List<String> dataList =
303405
List.generate(100, (index) => index.toString()).toList();
304406

407+
void updateData({
408+
int index = 0,
409+
String? appendStr,
410+
bool needSetState = true,
411+
}) {
412+
dataList[index] += appendStr ?? 'updateData' * 10;
413+
if (needSetState) {
414+
setState(() {});
415+
}
416+
}
417+
305418
@override
306419
Widget build(BuildContext context) {
307420
return MaterialApp(

0 commit comments

Comments
 (0)