listView같은 scroll view 에서 화면에 어떤 item(widget)이 보이고 있는지 확인해야 할 때가 있어. 현재 보여지고 있는 아이템을 헤더에 표시해야 한다거나 동영상이 자동으로 재생되어야 한다거나 할때 말야.👇
그렇게 흔하게 요구되는 기능은 아니라 몰라도 되지만 내가 만드는 앱에서는 필수적인 기능이라서 ㅎ 해볼거야.
우선 이 기능은 inview_notifier_list 패키지를 사용하면 대부분 해결될거라 생각해.
https://pub.dev/packages/inview_notifier_list
위 패키지는 ListView와 CustomSrollView를 둘 다 지원해서 Sliver list 같은 곳에도 사용할 수 있어. 테스트 해봤고 훌륭하게 동작해.
그런데 난 개인적으로 조금 찝찝한 부분이 있어서 위 패키지를 참고해서 새로 만들었어. getxcontroller 와 통합도 하고 말이지.
대략적인 동작방식을 설명하자면 👇 아래와 같은데;; ㅎㅎ 약간 복잡스럽네? ㅋㅋ
- CustomScrollView나 ListView를 NotificationListener로 감싸.
- 아이템들이 빌드될 때 LayoutBuilder를 이용해서 각각의 context와 index를 기록해.
- Scroll 이벤트가 일어났을 때 onNotification이 호출돼.
- streamController에 notification을 add해줘.
- streamController의 listener가 호출되면 notification과 각 아이템들의 context를 이용해서 각 아이템들의 위치를 알아내.
- 조건에 만족하는 아이템을 찾아내서 저장!
코드를 보면 조금 도움이 되려나? 기존 Scroll view를 NotificationListener로 감싸고 onNotification에서 streamController의 listener를 호출하도록 되어 있어.
return SafeArea(
child: Scaffold(
body: RefreshIndicator(
onRefresh: () => Future.sync(
() => controller.pagingController.refresh(),
),
child: NotificationListener<ScrollNotification>(//Notification!!!!
onNotification: (ScrollNotification notification) {
if (!controller.streamController.isClosed) {
controller.streamController.add(notification);
}
return false;
},
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
...
...
그리고 각 아이템들의 위치를 알아내기 위해 LayoutBuilder를 사용해서 context와 index를 저장해. 난 getbuilder를 사용하고 있어서 item들이 동적으로 생성/삭제되거든. 그래서 dispose에서 저장해뒀던 context와 index를 삭제할 수 있도록 해줬어.
//View
...
PagedSliverList(
pagingController: controller.pagingController,
builderDelegate: PagedChildBuilderDelegate<
QueryDocumentSnapshot<ModelInsightCard>>(
itemBuilder: (context, item, index) {
return GetBuilder(
init: InsightCardController(
userId: item.data().author.id,
cardId: item.id,
cardInfo: item.data(),
),
tag: item.id,
dispose: (state) {//아이템이 없어질 때 저장해뒀던 contextItem도 삭제
controller.removeContextItem(index);
},
builder: (getController) {
return LayoutBuilder(//LayoutBuilder
builder: (p0, p1) {//p0 == context, p1 == BoxConstraints
//contextItem 저장
controller.addContextItem(
ContextItem(context: p0, index: index));
return Container(
child: InsightCard(
navKey: navKey,
showHeader: false,
cardId: item.id,
cardInfo: item.data(),
));
},
);
},
);
},
),
),
...
//addContextItem in GetxController
void addContextItem(ContextItem contextItem) {
_contextItems.value
.removeWhere((element) => element.index == contextItem.index);
_contextItems.value.add(contextItem);
}
//removeContextItem
void removeContextItem(int index) {
_contextItems.value.removeWhere((element) => element.index == index);
}
아래는 getxcontroller의 일부인데 👇 여기에 있는 _onScroll에서 모든 계산이 이뤄지지.
...
void addContextItem(ContextItem contextItem) {
_contextItems.value
.removeWhere((element) => element.index == contextItem.index);
_contextItems.value.add(contextItem);
}
void removeContextItem(int index) {
_contextItems.value.removeWhere((element) => element.index == index);
}
...
//stream listener callback
void _onScroll(ScrollNotification notification) {
for (var contextItem in _contextItems.value) {
//렌더 오브젝트 찾고
final RenderObject? object = contextItem.context.findRenderObject();
if (object == null || !object.attached) {
return;//없으면 리턴
}
//저장된 아이템의 viewport 확보
final RenderAbstractViewport viewport =
RenderAbstractViewport.of(object)!;
//scroll notification 의 viewport 높이 확보
final double vpHeight = notification.metrics.viewportDimension;
//viewport offset 계산
final RevealedOffset vpOffset = viewport.getOffsetToReveal(object, 0.0);
//아이템 사이즈 확보
final Size size = object.semanticBounds.size;
//아이템 상단 위치 계산
final double deltaTop = vpOffset.offset - notification.metrics.pixels;
//아이템 하단 위치 계산
final double deltaBottom = deltaTop + size.height;
if (deltaTop < (0.2 * vpHeight) && deltaBottom > (0.2 * vpHeight)) {
//기준에 맞으면 현재 아이템 data를 저장.
_currentInsightCardData.value =
pagingController.itemList?[contextItem.index].data();
}
}
contextItemLength = _contextItems.value.length;
}
...
@override
void onInit() {
// TODO: implement onInit
pagingController.addPageRequestListener((pageKey) {
fetchInsightCard(pageKey);
});
streamController.stream
.audit(Duration(milliseconds: 200))//0.2초의 간격을 두고 동작하도록
.listen(_onScroll);//listener 등록
super.onInit();
}
다시 한번 말하지만 inview_notifier_list 패키지를 사용하면 대부분 해결될거야. 그런데 나는 inview_notifier_list가 제공해주는 inviewNotifierWidget을 사용할 수 없었고 나중에 많이 변경할 것 같아서 파본거임. 뭐... 이해 좀 해보겠다고 변태처럼 군 것도 있고 ㅋㅋㅋㅋ 다들 화이팅~
'Flutter > Flutter Study' 카테고리의 다른 글
[Flutter] Firestore에서 Elastic app search (0) | 2023.01.03 |
---|---|
[Flutter] Firestore pagination(무한스크롤) (1) | 2022.11.13 |
[Flutter] Bad state: Future already completed (0) | 2022.11.10 |
[Flutter] getx page&controller 재사용 (0) | 2022.11.02 |
[소셜차트] 앱 제작기 #7. 네비게이션 시스템 뒤집어 엎기. (2) | 2022.11.02 |