最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Implementing a Profile Screen Like Threads App in Flutter.. how to..? - Stack Overflow

programmeradmin3浏览0评论

I'm trying to implement a profile screen in Flutter similar to the Threads app.

  • When scrolling down, only the TabBar remains fixed at the top.
  • When scrolling up, the AppBar, profile info, and TabBar return to their original positions and are fully visible.

The issue occurs when there is little or no content below the TabBar

  • If there is no content, the screen should not scroll.
  • However, in my current code, the screen scrolls up to where the TabBar gets fixed, even when there is not enough content.

How can I make the screen scroll only as much as the content allows, just like in the Threads app?

Demo video for my app: result

Demo video for Thread app: result

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

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

  @override
  State<TestSearchScreen> createState() => _TestSearchScreenState();
}

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

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _scrollController.addListener(_scrollListener);
  }

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

  void _scrollListener() {
    dev.log('''
    [스크롤 정보]
    • 현재 스크롤 위치: ${_scrollController.position.pixels}
    • 최대 스크롤 범위: ${_scrollController.position.maxScrollExtent}
    • 최소 스크롤 범위: ${_scrollController.position.minScrollExtent}
    • 뷰포트 크기: ${_scrollController.position.viewportDimension}
    • 현재 방향: ${_scrollController.position.userScrollDirection}
    • 현재 활성화된 탭: ${_tabController.index}
    ''');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification) {
            return false;
          },
          child: NestedScrollView(
            controller: _scrollController,
            physics: const BouncingScrollPhysics(),
            headerSliverBuilder: (context, innerBoxIsScrolled) {
              return [
                // 앱바
                SliverAppBar(
                  title: const Text('Test Search'),
                  backgroundColor: Colors.white,
                  elevation: 0,
                  floating: true,
                  snap: true,
                ),
                // 프로필 정보
                SliverToBoxAdapter(
                  child: Container(
                    padding: const EdgeInsets.all(70),
                    color: Colors.grey[200],
                    child: const Text('Profile Info Here'),
                  ),
                ),
                // 탭바
                SliverPersistentHeader(
                  pinned: true,
                  delegate: _SliverAppBarDelegate(
                    TabBar(
                      controller: _tabController,
                      tabs: const [
                        Tab(text: 'Tab 1'),
                        Tab(text: 'Tab 2'),
                        Tab(text: 'Tab 3'),
                      ],
                    ),
                  ),
                ),
              ];
            },
            body: TabBarView(
              controller: _tabController,
              physics: const NeverScrollableScrollPhysics(),
              children: [
                _buildTabContent(7),
                _buildTabContent(5),
                _buildTabContent(1),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildTabContent(int itemCount) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final double itemHeight = 116.0; // 아이템 높이(100) + 마진(16)
        final double filterHeight = 48.0; // 필터 높이
        final double totalContentHeight =
            (itemHeight * itemCount) + filterHeight;
        final bool hasScrollableContent =
            totalContentHeight > constraints.maxHeight;

        return CustomScrollView(
          physics: const AlwaysScrollableScrollPhysics(
            parent: BouncingScrollPhysics(),
          ),
          slivers: [
            // 필터
            SliverToBoxAdapter(
              child: Container(
                padding: const EdgeInsets.all(16),
                color: Colors.blue[100],
                child: const Text('Filter Here'),
              ),
            ),
            // 아이템 리스트
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Container(
                    height: 100,
                    margin: const EdgeInsets.all(8),
                    color: Colors.primaries[index % Colors.primaries.length],
                    child: Center(child: Text('Item $index')),
                  );
                },
                childCount: itemCount,
              ),
            ),
            // 스크롤이 불가능한 경우에도 bounce 효과를 위한 추가 공간
            if (!hasScrollableContent)
              SliverFillRemaining(
                hasScrollBody: false,
                child: Container(),
              ),
          ],
        );
      },
    );
  }
}

// 탭바를 위한 SliverPersistentHeaderDelegate
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate(this._tabBar);

  final TabBar _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: Colors.white,
      child: _tabBar,
    );
  }

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

void main() {
  runApp(const MaterialApp(home: TestSearchScreen()));
}
发布评论

评论列表(0)

  1. 暂无评论