[Flutter] 오버레이로 자동 완성 검색창 만들기

Broccolism·2021년 11월 30일
12

dev-story

목록 보기
4/9
post-custom-banner

발단: 이거 밑에 레이어가 있어야 해요

🧑🏻‍🎨 자동 완성 검색창이에요! 검색어 입력하면 실시간으로 밑에 키워드가 나오고, 그 아래 레이어에 결과가 보였으면 좋겠어요.
👩🏻‍💻 그러니까 구글이나 네이버 검색창 같은거 말씀이시죠?
🧑🏻‍🎨 네 맞아요!

  1. Stack을 사용해서 아래쪽에는 검색 결과, 위쪽에는 자동완성 키워드를 보여주자!
  2. 그런데 stack을 사용하면 전체 페이지를 아래쪽, 자동완성 키워드 목록을 위쪽에 놓아야 하는데... 이렇게 하지 말고 검색바 + 자동완성 키워드 목록 세트를 단일 위젯으로 분리시킬 방법은 없나?

=> 플러터에서 제공하는 위젯만 잘 써먹으면 스택을 사용하지 않고 구현할 수 있다.

1. Overlay로 위에 쌓기

Overlay는 Stack의 일종이다. Overlay에게 전달된 child 위젯은 각각 독립적인 상태를 가질 수 있으며, UI상에서 마치 떠있는 것처럼 보여지게 된다. Overlay의 child로 들어갈 수 있는 위젯 클래스는 OverlayEntry, Overlay의 상태에 대한 정보를 담는 클래스는 OverlayState이다.

그러니까 Overlay가 위젯을 담는 스택이 되고 그 안에 우리가 원하는 위젯을 OverlayEntry로 넣으면 된다. 단, Overlay에는 다른 위젯과 달리 children이라는 파라미터가 없어서 위젯을 바로 넘겨줄 수 없다. 대신 Overlay의 상태를 나타내는 OverlayState의 멤버 함수인 insert 를 사용해야 한다. 이렇게 해야 하는 이유는 개발자가 Overlay의 children을 원하는 시점에 보여줄 수 있어야 하기 때문이다.

아래는 OverlayState.insert 함수의 코드다. 먼저 entry._overlay = this;를 통해 주어진 OverlayEntry가 현재 OverlayState의 주인인 this Overlay에 속한다는 것을 등록한다. OverlayEntry 하나는 절대 2개 이상의 서로 다른 Overlay에 속할 수 없기 때문에 어떤 Overlay에 속하는지 알 수 있어야 한다. 이 함수의 마지막 assert 문에서 이 사실을 확인하고 있다.

void insert(OverlayEntry entry, { OverlayEntry? below, OverlayEntry? above }) {
    assert(_debugVerifyInsertPosition(above, below));
    assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.');
    assert(entry._overlay == null, 'The specified entry is already present in another Overlay.');
    entry._overlay = this;
    setState(() {
      _entries.insert(_insertionIndex(below, above), entry);
    });
  }

그 다음으로 setState를 호출하면서 주어진 OverlayEntry를 OverlayState._entries에 삽입하고 있다. 따라서 우리가 insert 함수를 실행하면 setState가 호출되고, Overlay는 한번 더 build 되어서 OverlayEntry가 화면에 보이게 된다. 즉, Overlay는 child 위젯이 보여지는 시점을 조절할 수 있는 Stack이라고 볼 수 있다.

insert로 넣은 위젯은 OverlayEntry.remove로 뺄 수 있다. 이 때에도 build가 한번 더 일어나서 OverlayEntry의 위젯이 사라진다. 기억할 점은 insert는 OverlayState 클래스에, remove는 OverlayEntry 클래스에 있다는 점이다.

핵심 코드

// stateful widget

  late final OverlayEntry overlayEntry =
      OverlayEntry(builder: _overlayEntryBuilder);

  
  void dispose() {
    overlayEntry.dispose();
    super.dispose();
  }
  
  void insertOverlay() { // 적절한 타이밍에 호출
    if (!overlayEntry.mounted) {
      OverlayState overlayState = Overlay.of(context)!;
      overlayState.insert(overlayEntry);
    }
  }

  void removeOverlay() { // 적절한 타이밍에 호출
    if (overlayEntry.mounted) {
      overlayEntry.remove();
    }
  }

  Widget _overlayEntryBuilder(BuildContext context) {
    Offset position = _getOverlayEntryPosition();
    Size size = _getOverlayEntrySize();

    return Positioned(
      left: position.dx,
      top: position.dy,
      width: Get.size.width - MyConstants.SCREEN_HORIZONTAL_MARGIN.horizontal,
      child: AutoCompleteKeywordList(),
    );
  }

근데 이거 스크롤하니까 이상해요

네.. 그러네요.

overlayEntry로만 구현했을 때

Overlay의 역할은 "스택처럼 보여주기"에서 끝난다. 그래서 여기까지만 하면 검색바와 키워드 목록이 분리되어 따로 돌아다닐 수 있다. 하지만 우리가 원하는건 스크롤했을 때에도 검색바와 키워드 목록이 분리되지 않고 잘 따라다니는 UI니까 추가 작업이 필요하다.

2. link로 위젯 연결하기

CompositedTransformTargetCompositedTransformFollower를 사용하면 쉽게 이 둘을 연결할 수 있다. 이름에서 짐작할 수 있듯이, follower 위젯이 target 위젯을 따라다닌다. 이 둘을 연결하려면 LayerLink 를 사용한다. layerLink에 target과 follower를 등록해서 사용하는 방식이다.

기존 코드에서 검색바를 CompositedTransfromTarget, 키워드 목록을 CompositedTransfromFollower로 감싸주고 link에 서로 같은 LayerLink 오브젝트를 넘겨주면 된다. 이 때 follower의 offset을 통해 상대적인 위치를 정할 수 있다. 검색바의 높이만큼 dy가 필요하기 때문에 Offset(0.0, size.height)를 넘겨주었다. 여기서 size는 검색바의 사이즈를 의미한다.

핵심 코드

// 기존 stateful widget에 추가
  final LayerLink _searchBarLink = LayerLink();

// 기존 검색바 위젯의 build 함수
  
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: _searchBarLink,
      child: // (기존 검색바 위젯)
    );
  }

// 키워드 목록 overlayEntryBuilder
  Widget _overlayEntryBuilder(BuildContext context) {
    Offset position = _getOverlayEntryPosition();
    Size size = _getOverlayEntrySize();

    return Positioned(
      left: position.dx,
      top: position.dy,
      width: Get.size.width - MyConstants.SCREEN_HORIZONTAL_MARGIN.horizontal,
      child: CompositedTransformFollower(
        link: _searchBarLink,
        showWhenUnlinked: false,
        offset: Offset(0.0, size.height),
        child: AutoCompleteKeywordList(),
      ),
    );
  }

결과

드디어 검색바와 키워드 목록은 뗄 수 없는 관계가 되었다.🤗 검색 결과와 키워드 목록을 보여주기 위해 google search api를 연동했다.

↗️ 깃허브에서 소스코드 보기 ↗️

결과

profile
설계를 좋아합니다. 코드도 적고 그림도 그리고 글도 씁니다. 넓고 얕은 경험을 쌓고 있습니다.
post-custom-banner

0개의 댓글