[Flutter] 스크롤 방향에 따라 앱바 애니메이션 효과 주기

CHOI·2022년 12월 28일
0

[Flutter] 위젯

목록 보기
5/6
post-thumbnail

W Concept

W컨셉 어플을 보면 스크롤 방향을 하단으로 하면 앱바와 내비게이션 바가 사라지고
상단 방향으로 이동하면 다시 노출되는 애니메이션 효과를 볼 수 있다.

‼️ 이러한 애니메이션 효과는 사용자가 보고 싶은 화면을 더 집중적이고 크게 노출시킬 수 있기 때문에 다양한 곳에서 사용 가능해 보여서 구현해 보기로 결정했다.


SliverAppBar

sliver app bar 위젯에서 제공해 주는 pinned, floating, snap 인자를 통해서 이 UI를 구현할 수 있다고 생각하여 sliver app bar로 시도를 해보았다.

SliverAppBar
SliverAppBar는 도구 모음과 잠재적으로 TabBar 및 FlexibleSpaceBar와 같은 기타 위젯으로 구성됩니다. 앱 바는 일반적으로 덜 일반적인 작업을 위해 선택적으로 PopupMenuButton이 뒤따르는 IconButton과 함께 하나 이상의 일반적인 작업을 노출합니다.

⛔️ 생각과 다르게 내가 원하는 구현 방식과 전혀 다르게 구현되었다.
sliver app bar에서 제공하는 기능으로 구현 시, 스크롤의 위치가 최상단에서는 노출이 되고 스크롤 시 비노출은 가능하지만,
스크롤 위치가 중간 부분에서 스크롤 방향을 상단으로 스크롤 해도 앱 바를 노출시킬 수 없었다.
내가 원하는 구현 방식과 다르기 때문에 다른 방식을 선택했다.


UI 구현

내가 원하는 UI를 구현할 수 있는 네이티브 위젯이 없기 때문에 직접 구현했다.
앱 바를 커스텀 해서 AnimatedCrossFad을 통해서 애니메이션 효과를 주기로 했다.

AnimatedCrossFade
AnimatedCrossFade는 두 자식들을 지정을 하고 상태 변경에 따라서 두 자식이 교차 페이드 애니메이션 효과를 간단하게 적용할 수 있는 위젯이다.

AnimatedCrossFade(
  duration: const Duration(seconds: 3),
  firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0),
  secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
  crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
)

💡 대략적인 구조는 아래와 같다.

Scaffold
ㄴ AnimatedCrossFade
	ㄴ AppBar
	ㄴ SizedBox.shrink

구조를 토대로 코드를 작성하면 이러한 코드가 나온다.

/// 앱바의 노출 여부 확인 변수.
bool _exposureAppBar = true;


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AnimatedCrossFade(
      firstChild: AppBar(
        leading: const Icon(Icons.arrow_back_ios_new_rounded),
        backgroundColor: Colors.blueAccent,
        title: const Text('앱바'),
      ),
      secondChild: const SizedBox.shrink(),
      crossFadeState: _exposureAppBar ? CrossFadeState.showFirst : CrossFadeState.showSecond,
      duration: const Duration(milliseconds: 200),
    ),
    body: _body(),
  );
}

⛔️ 하지만 AnimatedCrossFade 위젯에서 The argument type 'AnimatedCrossFade' can't be assigned to the parameter type 'PreferredSizeWidget?'. 오류가 발생한다.

오류는 Scaffold의 매개변수인 appBar는 인수로 PreferredSizeWidget 타입만 받을 수 있다는 것이다.

PreferredSize
원하는 크기의 위젯.

이 위젯은 자식에 제약을 두지 않으며 자식의 레이아웃에 어떤 식으로든 영향을 주지 않습니다. 부모가 사용할 수 있는 기본 크기를 광고할 뿐입니다.

Scaffold와 같은 부모는 PreferredSizeWidget을 사용하여 자식이 해당 인터페이스를 구현하도록 요구합니다. 임의의 위젯에 기본 크기를 지정하여 해당 유형의 하위 속성에서 사용할 수 있도록 하려면 이 위젯인 PreferredSize를 사용할 수 있습니다.

AppBar와 같은 위젯은 PreferredSizeWidget을 구현하므로 이 PreferredSize 위젯이 필요하지 않습니다.

📌 오류를 해결하는 방법은 두 가지이다.
1️⃣ AppBar 위젯처럼 PreferredSizeWidget을 구현하는 위젯을 생성.

class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {}

2️⃣ PreferredSize을 부모로 지정한 위젯을 생성.

PreferredSize(
  preferredSize: const Size.fromHeight(48),
  child: Container(),
),

간단하게 구현하는 방식은 2번이기 때문에 저는 2번 방식으로 오류를 해결했습니다.


Widget build(BuildContext context) {
  return Scaffold(
    appBar: PreferredSize(
      preferredSize: const Size.fromHeight(48),
      child: AnimatedCrossFade(
        firstChild: AppBar(
          leading: const Icon(Icons.arrow_back_ios_new_rounded),
          backgroundColor: Colors.blueAccent,
          title: const Text('앱바'),
        ),
        secondChild: const SizedBox.shrink(),
        crossFadeState: _exposureAppBar ? CrossFadeState.showFirst : CrossFadeState.showSecond,
        duration: const Duration(milliseconds: 200),
      ),
    ),
    body: _body(),
  );
}


스크롤 방향

이제 스크롤 방향에 따라서 앱바 노출 여부 변수 상태를 변경해 주면 끝난다.
스크롤의 변경 상태를 확인할 수 있는 addListener() 함수를 통해서 스크롤 데이터들을 확인할 것이다.

/// 스크롤 컨트롤러.
late ScrollController _scrollController;


void initState() {
  _scrollController = ScrollController()..addListener((){});
  super.initState();
}


void dispose() {
  _scrollController.dispose();
  super.dispose();
}

/// 스크롤 가능한 body.
ListView.separated(
  controller: _scrollController,
  ...
);

스크롤 방향을 확인할 수 있는 방법은 ScrollDirection 열거형 데이터로 확인이 가능하다.

📌 총 3가지의 값을 가지고 있다.
1️⃣ idle: 스크롤 하지 않음.
2️⃣ forward: 상단 방향으로 스크롤.
3️⃣ reverse: 하단 방향으로 스크롤.

enum ScrollDirection {
  /// No scrolling is underway.
  idle,

  /// Scrolling is happening in the positive scroll offset direction.
  ///
  /// For example, for the [GrowthDirection.forward] part of a vertical
  /// [AxisDirection.down] list, this means the content is moving up, exposing
  /// lower content.
  forward,

  /// Scrolling is happening in the negative scroll offset direction.
  ///
  /// For example, for the [GrowthDirection.forward] part of a vertical
  /// [AxisDirection.down] list, this means the content is moving down, exposing
  /// earlier content.
  reverse,
}

✅ 최종적으로 앱바의 애니메이션 효과 상태를 변경하는 로직
1️⃣ addListener() 함수 내부에서 스크롤 방향을 확인.
2️⃣ 스크롤이 하단 방향으로 진행되면 앱바를 노출하지 않음.
3️⃣ 스크롤이 상단 방향으로 진행되면 앱바를 노출.
4️⃣ 스크롤이 진행되지 않으면 기존 상태를 유지.

_scrollController = ScrollController()..addListener((){
  try {
    if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) {
      if (_exposureAppBar) {
        setState(() {
          _exposureAppBar = false;
        });
      }
    } else if (_scrollController.position.userScrollDirection == ScrollDirection.forward) {
      if (!_exposureAppBar) {
        setState(() {
          _exposureAppBar = true;
        });
      }
    }
  } catch (_) {}
});

상태바 영역

위의 영상을 보면 스크롤 방향에 따라서 앱바의 노출 애니메이션이 잘 작동하는 것을 확인할 수 있다.
살짝 더 보완하자면, 앱바가 숨김 처리되었을 때 리스트 타일들이 상태바 영역에서까지 보여지고 있다.
이 부분 수정을 원하면 SafeArea 를 통해서 보완이 가능하다.

/// 애니메이션 앱바.
PreferredSize _animationAppBar() {
  return PreferredSize(
    preferredSize: const Size.fromHeight(48),
    child: SafeArea(
      child: AnimatedCrossFade(
        ...
      ),
    ),
  );
}

🔥 +++ 추가적으로
W Concept의 애니메이션 효과를 보면 앱바는 물론이고 하단의 내비게이션 바도 함께 효과가 있다. 위의 방식과 비슷하게 내비게이션 바에 도 적용을 하면 동일한 UI를 구현할 수 있다.


Github
https://github.com/cyb9701/animation-appbar

profile
Mobile App Developer

0개의 댓글