본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓41(Item detail & PageView) - 3

지난번에는 SmoothPageIndicator 를 backgroud 로 구현한 경우와 title 로 구현한 경우를 비교해 보았습니다.

이번에는 title 로 구현한 인디케이터를 숨기기 위해서 Scaffold 를 추가, 화면 상단을 조금 부드럽게 표현하기 위해서 Container 를 추가해보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

 

구현한 화면은 아래와 같습니다. 

초기 화면 상단에 검은색 계열로 appBar 영역만큼 그라데이션 처리, 화면 스크롤시 이미지가 화면 밖으로 사라지면 appBar 영역이 흰색으로 변경됨.

 

 

 

 

./src/screens/home/item_detail_page.dart - 전체 코드는 아래 "더보기" 를 클릭하세요

 

더보기
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';

import '../../models/item_model.dart';
import '../../repo/item_service.dart';

import '../../utils/logger.dart';

class ItemDetailPage extends StatefulWidget {
  const ItemDetailPage({Key? key}) : super(key: key);

  @override
  _ItemDetailPageState createState() => _ItemDetailPageState();
}

class _ItemDetailPageState extends State<ItemDetailPage> {
  final PageController _pageController = PageController();

  // 스크롤이 얼마나 되었는지 알기 위해서 컨트롤러 등록,
  final ScrollController _scrollController = ScrollController();

  // isAppbarCollapsed 이미지가 화면에서 사라졌는지 확인,
  bool isAppbarCollapsed = false;
  Size? _size;

  num? _statusBarHeight;
  late String newItemKey;

  @override
  void initState() {
    newItemKey = Get.arguments['itemKey'];
    logger.d('$_size!.width, $kToolbarHeight, $_statusBarHeight, ${isAppbarCollapsed.toString()}');

    // 스크롤이 발생할때 마다 addListener 가 실행됨,
    _scrollController.addListener(() {
      if (_size == null && _statusBarHeight == null) return;

      if (isAppbarCollapsed) {
        // 여기는 이미지가 앱바 아래로 보여지기 시작하는 시점,
        // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
        if (_scrollController.offset < _size!.width - kToolbarHeight - _statusBarHeight!) {
          isAppbarCollapsed = false;
          setState(() {});
        }
      } else {
        // 여기는 이미지가 앱바에 위로 올라가서 안보이기 시작하는 시점,
        // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
        if (_scrollController.offset > _size!.width - kToolbarHeight - _statusBarHeight!) {
          isAppbarCollapsed = true;
          setState(() {});
        }
      }
    });
    super.initState();
  }

  @override
  void dispose() {
    _pageController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    logger.d('item detail screen >> build >>> [$newItemKey]');

    return FutureBuilder<ItemModel2>(
      future: ItemService().getItem(newItemKey),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          // FutureBuilder 의 snapshot 에서 게시글 데이터 가져오기
          ItemModel2 itemModel = snapshot.data!;

          return LayoutBuilder(
            builder: (context, constraints) {
              _size = MediaQuery.of(context).size;
              // 상태바 길이 가져오는 공식,
              _statusBarHeight = MediaQuery.of(context).padding.top;
              return Stack(
                // fit 은 Stack 에 있는 모든 아이콘들이 화면에 가득차게 하는 옵션,
                fit: StackFit.expand,
                children: [
                  // 메인 정보를 표시하는 영역
                  Scaffold(
                    // 메인정보를 표시, CustomScrollView 는 listView 유사함
                    // listView 대신에 CustomScrollView 사용하는 이유는
                    // slivers 를 이용해서 화면을 구역으로 나눠서 각 구역마다 슬라이스를 구현할 수 있다,
                    body: CustomScrollView(
                      controller: _scrollController,
                      // children 을 대신하는 slivers 있고, slivers 안에는 sliver 형식의 위젯을 넣어줘야 한다
                      slivers: [
                        // 업로드한 사진 정보를 표시하는 영역
                        _imageAppBar(itemModel),
                        // 일반위젯을 sliver 안에 넣으러면 SliverToBoxAdapter 로 wrapping 해야 함,
                        SliverToBoxAdapter(
                          child: Container(
                            // 스크롤 테스트를 위해서 높이를 길게 적용함,
                            height: _size!.height,
                            color: Colors.cyan,
                            child: Center(child: Text(newItemKey)),
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Container(
                            // 스크롤 테스트를 위해서 높이를 길게 적용함,
                            height: _size!.height,
                            color: Colors.redAccent,
                            child: Center(child: Text(newItemKey)),
                          ),
                        ),
                      ],
                    ),
                  ),
                  // 앱바 영역에 그라데이션 표현 추가
                  Positioned(
                    left: 0,
                    right: 0,
                    top: 0,
                    height: kToolbarHeight + _statusBarHeight!,
                    child: Container(
                      height: kToolbarHeight + _statusBarHeight!,
                      decoration: const BoxDecoration(
                        gradient: LinearGradient(
                          begin: Alignment.topCenter,
                          end: Alignment.bottomCenter,
                          colors: [
                            Colors.black45,
                            Colors.black38,
                            Colors.black26,
                            Colors.black12,
                            Colors.transparent,
                          ],
                        ),
                      ),
                    ),
                  ),
                  // 화면 스크롤업 하면 앱바를 힌색으로 변경.
                  // 이전에 구현한 인디케이터가 appBar 타이틀위치에서 보여주던걸 숨김,
                  Positioned(
                    left: 0,
                    right: 0,
                    top: 0,
                    height: kToolbarHeight + _statusBarHeight!,
                    child: Scaffold(
                      backgroundColor: Colors.transparent,
                      appBar: AppBar(
                        shadowColor: Colors.transparent,
                        backgroundColor: isAppbarCollapsed
                            ? Colors.white
                            : Colors.transparent,
                        foregroundColor:
                        isAppbarCollapsed ? Colors.black87 : Colors.white,
                      ),
                    ),
                  )
                ],
              );
            },
          );
        }
        return Container(
          color: Colors.white,
          child: const Center(child: CircularProgressIndicator()),
        );
      },
    );
  }

  SliverAppBar _imageAppBar(ItemModel2 itemModel) {
    return SliverAppBar(
      // expandedHeight 에서는 세로 길이를 정해줄 수 있음,
      expandedHeight: _size!.width,
      // pinned: true 면 앱바 역역을 남기는 역할, false 면 스크롤시 같이 사라짐,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        centerTitle: true,
        // title: const Text('testing', style: TextStyle(color: Colors.black)),
        // 타이틀 부분에 인디케이터 표시하고 아래에 위치함, 패키지 추가 필요함,
        title: SizedBox(
          child: SmoothPageIndicator(
              controller: _pageController,
              // PageController
              count: itemModel.imageDownloadUrls.length,
              effect: const WormEffect(
                activeDotColor: Colors.black,
                //Theme.of(context).primaryColor,
                dotColor: Colors.black45,
                //Theme.of(context).colorScheme.background,
                radius: 3,
                dotHeight: 6,
                dotWidth: 6,
              ),
              // your preferred effect
              onDotClicked: (index) {}),
        ),

        // background 로 이미지를 넣으면 됨, 이미지 표시
        background: PageView.builder(
              // 좌/우로 스크롤 가능하게 처리,
              controller: _pageController,
              // 옆페이지로 이동시 포커스를 옆페이지로 이동시켜 로딩을 미리하게 설정함,
              allowImplicitScrolling: true,
              itemBuilder: (BuildContext context, int index) {
                return ExtendedImage.network(
                  itemModel.imageDownloadUrls[index],
                  fit: BoxFit.cover,
                  // 캐싱을 했지만 다시 로딩하는 경우가 있어서 이미지 사이즈를 줄여줌,
                  scale: 0.1,
                );
              },
              itemCount: itemModel.imageDownloadUrls.length,
        ),
      ),
    );
  }
}

 

 

주요 부분은 아래와 같습니다.

 

1. 스크롤 컨트롤러 추가 및 리스너 등록을 통하여 스크롤 position 실시간 확인

 

@override
void initState() {
  newItemKey = Get.arguments['itemKey'];
  logger.d('$_size!.width, $kToolbarHeight, $_statusBarHeight, ${isAppbarCollapsed.toString()}');

  // 스크롤이 발생할때 마다 addListener 가 실행됨,
  _scrollController.addListener(() {
    if (_size == null && _statusBarHeight == null) return;

    if (isAppbarCollapsed) {
      // 여기는 이미지가 앱바 아래로 보여지기 시작하는 시점, 
      // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
      if (_scrollController.offset < _size!.width - kToolbarHeight - _statusBarHeight!) {
        isAppbarCollapsed = false;
        setState(() {});
      }
    } else {
      // 여기는 이미지가 앱바에 위로 올라가서 안보이기 시작하는 시점,
      // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
      if (_scrollController.offset > _size!.width - kToolbarHeight - _statusBarHeight!) {
        isAppbarCollapsed = true;
        setState(() {});
      }
    }
  });
  super.initState();
}

 

2. Scaffold 를 Stack 으로 wrapping 하여 appBar 영역을 구현.

 

return LayoutBuilder(
  builder: (context, constraints) {
    _size = MediaQuery.of(context).size;
    // 상태바 길이 가져오는 공식,
    _statusBarHeight = MediaQuery.of(context).padding.top;
    return Stack(
      // fit 은 Stack 에 있는 모든 아이콘들이 화면에 가득차게 하는 옵션,
      fit: StackFit.expand,
      children: [
        // 메인 정보를 표시하는 영역
        Scaffold(
          // 메인정보를 표시, CustomScrollView 는 listView 유사함
          // listView 대신에 CustomScrollView 사용하는 이유는
          // slivers 를 이용해서 화면을 구역으로 나눠서 각 구역마다 슬라이스를 구현할 수 있다,
          body: CustomScrollView(
            controller: _scrollController,
            // children 을 대신하는 slivers 있고, slivers 안에는 sliver 형식의 위젯을 넣어줘야 한다
            slivers: [
              // 업로드한 사진 정보를 표시하는 영역
              _imageAppBar(itemModel),
              // 일반위젯을 sliver 안에 넣으러면 SliverToBoxAdapter 로 wrapping 해야 함,
              SliverToBoxAdapter(
                child: Container(
                  // 스크롤 테스트를 위해서 높이를 길게 적용함,
                  height: _size!.height,
                  color: Colors.cyan,
                  child: Center(child: Text(newItemKey)),
                ),
              ),
              SliverToBoxAdapter(
                child: Container(
                  // 스크롤 테스트를 위해서 높이를 길게 적용함,
                  height: _size!.height,
                  color: Colors.redAccent,
                  child: Center(child: Text(newItemKey)),
                ),
              ),
            ],
          ),
        ),
        // 앱바 영역에 그라데이션 표현 추가
        Positioned(
          left: 0,
          right: 0,
          top: 0,
          height: kToolbarHeight + _statusBarHeight!,
          child: Container(
            height: kToolbarHeight + _statusBarHeight!,
            decoration: const BoxDecoration(
              gradient: LinearGradient(
                begin: Alignment.topCenter,
                end: Alignment.bottomCenter,
                colors: [
                  Colors.black45,
                  Colors.black38,
                  Colors.black26,
                  Colors.black12,
                  Colors.transparent,
                ],
              ),
            ),
          ),
        ),
        // 화면 스크롤업 하면 앱바를 힌색으로 변경.
        // 이전에 구현한 인디케이터가 appBar 타이틀위치에서 보여주던걸 숨김,
        Positioned(
          left: 0,
          right: 0,
          top: 0,
          height: kToolbarHeight + _statusBarHeight!,
          child: Scaffold(
            backgroundColor: Colors.transparent,
            appBar: AppBar(
              shadowColor: Colors.transparent,
              backgroundColor: isAppbarCollapsed
                  ? Colors.white
                  : Colors.transparent,
              foregroundColor:
              isAppbarCollapsed ? Colors.black87 : Colors.white,
            ),
          ),
        )
      ],
    );
  },
);