Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Flutter/Flutter Study

[Flutter] Getting visible items on scroll view

방피터 2022. 12. 12. 12:12

listView같은 scroll view 에서 화면에 어떤 item(widget)이 보이고 있는지 확인해야 할 때가 있어. 현재 보여지고 있는 아이템을 헤더에 표시해야 한다거나 동영상이 자동으로 재생되어야 한다거나 할때 말야.👇

그렇게 흔하게 요구되는 기능은 아니라 몰라도 되지만 내가 만드는 앱에서는 필수적인 기능이라서 ㅎ 해볼거야.

 

우선 이 기능은 inview_notifier_list 패키지를 사용하면 대부분 해결될거라 생각해.

https://pub.dev/packages/inview_notifier_list

 

inview_notifier_list | Flutter Package

A Flutter package that builds a listview and notifies when the widgets are on screen.

pub.dev

위 패키지는 ListView와 CustomSrollView를 둘 다 지원해서 Sliver list 같은 곳에도 사용할 수 있어. 테스트 해봤고 훌륭하게 동작해.

그런데 난 개인적으로 조금 찝찝한 부분이 있어서 위 패키지를 참고해서 새로 만들었어. getxcontroller 와 통합도 하고 말이지.

 

대략적인 동작방식을 설명하자면 👇 아래와 같은데;; ㅎㅎ 약간 복잡스럽네? ㅋㅋ

  1. CustomScrollView나 ListView를 NotificationListener로 감싸.
  2. 아이템들이 빌드될 때 LayoutBuilder를 이용해서 각각의 context와 index를 기록해.
  3. Scroll 이벤트가 일어났을 때 onNotification이 호출돼.
  4. streamController에 notification을 add해줘.
  5. streamController의 listener가 호출되면 notification과 각 아이템들의 context를 이용해서 각 아이템들의 위치를 알아내.
  6. 조건에 만족하는 아이템을 찾아내서 저장!

코드를 보면 조금 도움이 되려나? 기존 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을 사용할 수 없었고 나중에 많이 변경할 것 같아서 파본거임. 뭐... 이해 좀 해보겠다고 변태처럼 군 것도 있고 ㅋㅋㅋㅋ 다들 화이팅~

반응형