NestedScrollView

샤워실의 바보·2024년 2월 11일
0
post-thumbnail
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:tiktok_clone/constants/gaps.dart';
import 'package:tiktok_clone/constants/sizes.dart';
import 'package:tiktok_clone/features/users/widgets/persistent_tab_bar.dart';

class UserProfileScreen extends StatefulWidget {
  const UserProfileScreen({super.key});

  
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  
  Widget build(BuildContext context) {
    return SafeArea(
      child: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return [
              SliverAppBar(
                title: const Text('니꼬'),
                actions: [
                  IconButton(
                    onPressed: () {},
                    icon: const FaIcon(
                      FontAwesomeIcons.gear,
                      size: Sizes.size20,
                    ),
                  )
                ],
              ),
              SliverToBoxAdapter(
                child: Column(
                  children: [
                    const CircleAvatar(
                      radius: 50,
                      foregroundImage: NetworkImage(
                          "https://avatars.githubusercontent.com/u/3612017"),
                      child: Text("니꼬"),
                    ),
                    Gaps.v20,
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text(
                          "@니꼬",
                          style: TextStyle(
                            fontWeight: FontWeight.w600,
                            fontSize: Sizes.size18,
                          ),
                        ),
                        Gaps.h5,
                        FaIcon(
                          FontAwesomeIcons.solidCircleCheck,
                          size: Sizes.size16,
                          color: Colors.blue.shade500,
                        )
                      ],
                    ),
                    Gaps.v24,
                    SizedBox(
                      height: Sizes.size48,
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Column(
                            children: [
                              const Text(
                                "97",
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: Sizes.size18,
                                ),
                              ),
                              Gaps.v1,
                              Text("Following",
                                  style: TextStyle(
                                    color: Colors.grey.shade500,
                                  ))
                            ],
                          ),
                          VerticalDivider(
                            width: Sizes.size32,
                            thickness: Sizes.size1,
                            color: Colors.grey.shade400,
                            indent: Sizes.size14,
                            endIndent: Sizes.size14,
                          ),
                          Column(
                            children: [
                              const Text(
                                "10M",
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: Sizes.size18,
                                ),
                              ),
                              Gaps.v1,
                              Text(
                                "Followers",
                                style: TextStyle(
                                  color: Colors.grey.shade500,
                                ),
                              )
                            ],
                          ),
                          VerticalDivider(
                            width: Sizes.size32,
                            thickness: Sizes.size1,
                            color: Colors.grey.shade400,
                            indent: Sizes.size14,
                            endIndent: Sizes.size14,
                          ),
                          Column(
                            children: [
                              const Text(
                                "194.3M",
                                style: TextStyle(
                                  fontWeight: FontWeight.bold,
                                  fontSize: Sizes.size18,
                                ),
                              ),
                              Gaps.v1,
                              Text(
                                "Likes",
                                style: TextStyle(
                                  color: Colors.grey.shade500,
                                ),
                              )
                            ],
                          )
                        ],
                      ),
                    ),
                    Gaps.v14,
                    FractionallySizedBox(
                      widthFactor: 0.33,
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                          vertical: Sizes.size12,
                        ),
                        decoration: BoxDecoration(
                          color: Theme.of(context).primaryColor,
                          borderRadius: const BorderRadius.all(
                            Radius.circular(Sizes.size4),
                          ),
                        ),
                        child: const Text(
                          'Follow',
                          style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.w600,
                          ),
                          textAlign: TextAlign.center,
                        ),
                      ),
                    ),
                    Gaps.v14,
                    const Padding(
                      padding: EdgeInsets.symmetric(
                        horizontal: Sizes.size32,
                      ),
                      child: Text(
                        "All highlights and where to watch live matches on FIFA+ I wonder how it would loook",
                        textAlign: TextAlign.center,
                      ),
                    ),
                    Gaps.v14,
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: const [
                        FaIcon(
                          FontAwesomeIcons.link,
                          size: Sizes.size12,
                        ),
                        Gaps.h4,
                        Text(
                          "https://nomadcoders.co",
                          style: TextStyle(
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                      ],
                    ),
                    Gaps.v20,
                  ],
                ),
              ),
              SliverPersistentHeader(
                delegate: PersistentTabBar(),
                pinned: true,
              ),
            ];
          },
          body: TabBarView(
            children: [
              GridView.builder(
                itemCount: 20,
                padding: EdgeInsets.zero,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 3,
                  crossAxisSpacing: Sizes.size2,
                  mainAxisSpacing: Sizes.size2,
                  childAspectRatio: 9 / 14,
                ),
                itemBuilder: (context, index) => Column(
                  children: [
                    AspectRatio(
                      aspectRatio: 9 / 14,
                      child: FadeInImage.assetNetwork(
                        fit: BoxFit.cover,
                        placeholder: "assets/images/placeholder.jpg",
                        image:
                            "https://images.unsplash.com/photo-1673844969019-c99b0c933e90?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1480&q=80",
                      ),
                    ),
                  ],
                ),
              ),
              const Center(
                child: Text('Page two'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

NestedScrollView는 헤더와 본문 모두에 스크롤 동작이 있는 복잡한 스크롤 뷰를 만들 때 유용한 위젯입니다. UserProfileScreen에서는 사용자 프로필의 다양한 정보와 함께 그리드 뷰를 보여주기 위해 NestedScrollView를 사용하고 있습니다.

  1. headerSliverBuilder: 이 함수는 Sliver 위젯의 리스트를 반환하며, SliverAppBar, SliverToBoxAdapter, SliverPersistentHeader 등의 위젯들을 반환할 수 있습니다.

    • SliverAppBar: 이 앱바는 사용자가 스크롤할 때 축소되고 확장됩니다. 여기서는 사용자 이름을 제목으로 표시하며, 우측에는 설정 아이콘을 가진 액션 버튼이 있습니다.

    • SliverToBoxAdapter: 일반 위젯을 슬리버 형태로 변환할 수 있게 해줍니다. 여기서는 사용자의 프로필 사진, 사용자 이름, 팔로잉/팔로워/좋아요 숫자, 프로필 설명 등의 정보가 포함되어 있습니다.

    • SliverPersistentHeader: 스크롤 시 상단에 고정되는 헤더를 구현할 수 있습니다. 여기서는 PersistentTabBar 라는 커스텀 위젯을 사용하여 탭 바를 표시합니다.

  2. body: 여기에는 TabBarView가 위치해 있어, 사용자가 탭을 변경할 때 다양한 뷰를 보여줄 수 있습니다.

    • 첫 번째 탭에는 GridView.builder를 사용하여 그리드 형식의 목록을 표시합니다. 각 그리드 항목은 Unsplash에서 가져온 이미지를 보여줍니다.

    • 두 번째 탭에는 텍스트 위젯만 포함되어 있어, "Page two"라는 텍스트를 표시합니다.

즉, NestedScrollView를 사용하여 사용자 프로필의 상세 정보와 함께 스크롤 가능한 이미지 그리드를 보여주는 UI를 구현하고 있습니다. 이를 통해 사용자는 프로필 정보와 함께 게시한 이미지 목록을 확인할 수 있습니다.

NestedScrollView는 Flutter에서 복잡한 스크롤 뷰를 구현할 수 있게 해주는 위젯입니다. 그것은 특히 헤더와 본문의 스크롤 동작을 함께 조절해야 할 때 유용합니다. 이 위젯의 주요 기능과 특징에 대해 자세히 알아보겠습니다.

주요 구성 요소

  1. headerSliverBuilder:

    • 이 함수는 스크롤뷰의 상단에 위치하는 slivers의 리스트를 반환합니다. 대표적으로 SliverAppBar가 여기에 포함됩니다.
    • SliverAppBar는 사용자가 스크롤할 때 축소되거나 확장될 수 있습니다.
  2. body:

    • 이 부분은 NestedScrollView의 본문에 해당하며, 일반적으로 ListView, GridView, CustomScrollView 등의 스크롤 가능한 위젯이 위치합니다.
    • headerSliverBuilder에서 정의된 헤더 아래에 위치하며, 헤더와 함께 스크롤됩니다.

동작 원리

  • NestedScrollView는 두 개의 스크롤 뷰를 "중첩"하는 방식으로 동작합니다. 첫 번째 스크롤 뷰는 헤더를 처리하며, 두 번째 스크롤 뷰는 본문을 처리합니다.
  • 사용자가 스크롤을 시작하면, 먼저 헤더의 스크롤이 처리됩니다. 헤더가 완전히 축소되면 이후의 스크롤 동작은 본문에 적용됩니다.
  • 반대로, 본문이 최상단에 도달하면 이후의 스크롤 동작은 헤더를 확장시키는 데 사용됩니다.

특징 및 주의 사항

  • NestedScrollView는 중첩된 스크롤 동작을 처리하기 위해 특별한 논리를 포함하고 있습니다. 그렇기 때문에 ListView.builderGridView.builder 같은 빌더 패턴을 사용할 때는 SliverChildBuilderDelegate와 함께 CustomScrollView를 본문에 사용하는 것이 좋습니다.

  • NestedScrollView 내에서 스크롤 뷰의 controller에 직접 접근하려면, NestedScrollView.controller를 사용해야 합니다.

  • SliverOverlapAbsorberSliverOverlapInjector는 중첩된 스크롤에서 영역 겹침 문제를 해결하는 데 도움이 될 수 있습니다.

결론

NestedScrollView는 복잡한 스크롤 동작을 가진 UI를 구현할 때 매우 유용합니다. 그러나 그것의 동작 원리와 특징을 잘 이해하고 사용해야 원하는 대로 동작하는 UI를 만들 수 있습니다.

profile
공부하는 개발자

0개의 댓글