강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

코딩린이님의 프로필 이미지
코딩린이

작성한 질문수

안녕하세요

작성

·

25

0

안녕하세요.

 

현재 코드입니다.

import 'package:flutter/material.dart';
import 'dart:developer' as developer;

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

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

class _TestSearchScreenState extends State<TestSearchScreen>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  late ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _scrollController = ScrollController()..addListener(_scrollListener);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _logScreenInfo();
    });
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    _tabController.dispose();
    super.dispose();
  }

  void _scrollListener() {
    developer.log('현재 스크롤 위치: ${_scrollController.offset}', name: '스크롤 디버그');
    developer.log('최대 스크롤 범위: ${_scrollController.position.maxScrollExtent}',
        name: '스크롤 디버그');
    developer.log('최소 스크롤 범위: ${_scrollController.position.minScrollExtent}',
        name: '스크롤 디버그');
    developer.log(
        '현재 스크롤 방향: ${_scrollController.position.userScrollDirection}',
        name: '스크롤 디버그');
    developer.log('스크롤 위치가 범위 내에 있음: ${_scrollController.position.outOfRange}',
        name: '스크롤 디버그');
    developer.log('현재 뷰포트 크기: ${_scrollController.position.viewportDimension}',
        name: '스크롤 디버그');
  }

  void _logScreenInfo() {
    final mediaQuery = MediaQuery.of(context);
    developer.log('화면 정보:', name: '스크롤 디버그');
    developer.log('  화면 크기: ${mediaQuery.size}', name: '스크롤 디버그');
    developer.log('  화면 너비: ${mediaQuery.size.width}', name: '스크롤 디버그');
    developer.log('  화면 높이: ${mediaQuery.size.height}', name: '스크롤 디버그');
    developer.log('  상태 표시줄 높이: ${mediaQuery.padding.top}', name: '스크롤 디버그');
    developer.log('  바닥 패딩: ${mediaQuery.padding.bottom}', name: '스크롤 디버그');
    developer.log(
        '  SafeArea 높이: ${mediaQuery.size.height - mediaQuery.padding.top - mediaQuery.padding.bottom}',
        name: '스크롤 디버그');
  }

  @override
  Widget build(BuildContext context) {
    developer.log('TestSearchScreen 빌드 호출됨', name: '스크롤 디버그');
    return Scaffold(
      body: SafeArea(
        child: NestedScrollView(
          controller: _scrollController,
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            // 헤더 부분에 들어갈 슬리버 위젯들
            return <Widget>[
              SliverAppBar(
                title: const Text('Test Search'),
                floating: true,
                snap: true,
                forceElevated: innerBoxIsScrolled,
              ),
              SliverToBoxAdapter(
                child: _buildProfileInfo(),
              ),
              SliverPersistentHeader(
                pinned: true,
                delegate: _TabBarDelegate(
                  TabBar(
                    controller: _tabController,
                    onTap: (index) {
                      developer.log('탭 변경됨: $index', name: '스크롤 디버그');
                    },
                    tabs: const [
                      Tab(text: 'Tab 1'),
                      Tab(text: 'Tab 2'),
                      Tab(text: 'Tab 3'),
                    ],
                  ),
                ),
              ),
              SliverToBoxAdapter(
                child: _buildFilter(),
              ),
            ];
          },
          // 본문 부분에는 TabBarView 배치
          body: TabBarView(
            controller: _tabController,
            children: [
              _buildTab(1, Colors.red),
              _buildTab(5, Colors.green),
              _buildTab(1, Colors.blue),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildProfileInfo() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('User Profile',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          Container(height: 100, color: Colors.grey[300]),
          const SizedBox(height: 16),
        ],
      ),
    );
  }

  Widget _buildFilter() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Row(
        children: [
          Expanded(
              child: ElevatedButton(
                  onPressed: () {}, child: const Text('Filter 1'))),
          const SizedBox(width: 8),
          Expanded(
              child: ElevatedButton(
                  onPressed: () {}, child: const Text('Filter 2'))),
        ],
      ),
    );
  }

  // 탭 컨텐츠를 위한 새로운 메서드
  Widget _buildTab(int count, Color color) {
    return Builder(builder: (BuildContext context) {
      // NestedScrollView의 내부 스크롤 동작을 위해 Builder 사용
      return GridView.builder(
        padding: const EdgeInsets.all(16),
        // NestedScrollView 내부의 스크롤에 맞는 physics 사용
        physics: const BouncingScrollPhysics(),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        itemCount: count,
        itemBuilder: (context, index) {
          developer.log('그리드 아이템 빌드: index=$index', name: '스크롤 디버그');
          return Container(
            color: color,
            child: Center(child: Text('아이템 $index')),
          );
        },
      );
    });
  }
}

class _TabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar tabBar;

  _TabBarDelegate(this.tabBar);

  @override
  double get minExtent => tabBar.preferredSize.height;

  @override
  double get maxExtent => tabBar.preferredSize.height;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      color: Theme.of(context).scaffoldBackgroundColor,
      child: tabBar,
    );
  }

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    return false;
  }
}

Threads 앱의 프로필 화면처럼 구현하려고 합니다.

현재 구현은

• 스크롤 내릴 때는 탭바만 상단에 고정되고 있고

• 스크롤 올릴 때는 앱바, 프로필 정보, 탭바가 원래 위치로 복귀되고 있습니다.

 

문제는

 

탭바 아래 컨텐츠가 적을 때 발생하는 문제:

스크롤 할 컨텐츠 양이 부족해도 스크롤이 불필요하게 탭바 고정 위치까지 이동하고 있습니다.

컨텐츠가 부족할 경우, 탭바 고정 위치까지 스크롤되지 않고, 컨텐츠 높이까지만 스크롤되도록 하고 싶은데 어떻게해야될까요?
(스레드앱처럼구현되길원합니다)

스크린샷 2025-03-23 18.30.01.png.webp


스크린샷 2025-03-23 18.29.55.png.webpIMG_6C7FD4AA51D3-1.jpeg.webp


(스레드앱사진인데 포스트가없으면 스크롤이 되지 않습니다.)

고수분들 의견 부탁드립니다.

답변

답변을 기다리고 있는 질문이에요
첫번째 답변을 남겨보세요!
코딩린이님의 프로필 이미지
코딩린이

작성한 질문수

질문하기