You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现横向滚动服务栏与联动纵向分类列表方案咨询

Hey there! Let’s work through this linked scroll problem you’re facing in your Flutter project—this pattern (think tabbed category lists like you see in shopping apps) is totally doable with the right setup. Here’s a step-by-step breakdown of how to pull it off:

Core Implementation Approach

The key is to use ScrollController instances to sync the state between your horizontal service list and vertical category list, plus pre-calculate scroll offsets to map each service to its corresponding category section.

1. Define a Clear Data Structure

First, tie each service to its associated categories so you can easily map between them later:

class ServiceCategory {
  final String serviceName;
  final List<String> categories;

  ServiceCategory({required this.serviceName, required this.categories});
}

Use this to build your dataset, e.g.:

final List<ServiceCategory> serviceData = [
  ServiceCategory(serviceName: "Delivery", categories: ["Food", "Groceries", "Packages"]),
  ServiceCategory(serviceName: "Travel", categories: ["Flights", "Hotels", "Rides"]),
  ServiceCategory(serviceName: "Entertainment", categories: ["Movies", "Concerts", "Streaming"]),
];

2. Set Up Scroll Controllers & Offset Tracking

You’ll need two controllers: one for the horizontal service list, one for the vertical category list. You’ll also track the starting scroll offset of each service’s category section to jump between them:

final ScrollController _verticalController = ScrollController();
final ScrollController _horizontalController = ScrollController();
bool _isProgrammaticScroll = false; // Prevents looped scroll triggers
Map<String, double> _serviceOffsets = {};
final double _categoryItemHeight = 60.0; // Adjust to your item height

In initState(), calculate the initial offsets for each service (this assumes fixed-height category items; we’ll cover dynamic heights later):

@override
void initState() {
  super.initState();
  double currentOffset = 0.0;
  for (var service in serviceData) {
    _serviceOffsets[service.serviceName] = currentOffset;
    currentOffset += service.categories.length * _categoryItemHeight;
  }
  // Listen for vertical scroll events to update the horizontal tab
  _verticalController.addListener(_onVerticalScroll);
}

3. Handle Vertical → Horizontal Scroll Sync

When the user scrolls the vertical category list, detect which service’s section is currently in view and auto-scroll the horizontal list to the matching service tab:

void _onVerticalScroll() {
  if (_isProgrammaticScroll) return; // Skip if we triggered the scroll ourselves

  final currentOffset = _verticalController.offset;
  // Find the first service whose starting offset is <= current scroll position
  for (var i = serviceData.length - 1; i >= 0; i--) {
    final service = serviceData[i];
    if (currentOffset >= _serviceOffsets[service.serviceName]!) {
      _isProgrammaticScroll = true;
      _horizontalController.animateTo(
        i * 130.0, // Adjust based on your horizontal tab width + margins
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      ).then((_) => _isProgrammaticScroll = false);
      break;
    }
  }
}

4. Handle Horizontal → Vertical Scroll Sync

When the user taps a service tab in the horizontal list, jump the vertical list to the start of that service’s category section:

void _onServiceTabTap(int index) {
  _isProgrammaticScroll = true;
  _verticalController.animateTo(
    _serviceOffsets[serviceData[index].serviceName]!,
    duration: const Duration(milliseconds: 300),
    curve: Curves.easeInOut,
  ).then((_) => _isProgrammaticScroll = false);
}

5. Build the UI

Put it all together with a horizontal list for services and a vertical list for categories:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        // Horizontal Service Tabs
        SizedBox(
          height: 70,
          child: ListView.builder(
            controller: _horizontalController,
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.symmetric(horizontal: 8),
            itemCount: serviceData.length,
            itemBuilder: (context, index) {
              final service = serviceData[index];
              return GestureDetector(
                onTap: () => _onServiceTabTap(index),
                child: Container(
                  width: 120,
                  margin: const EdgeInsets.symmetric(horizontal: 4),
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: // Highlight active tab (track current service separately)
                        _activeService == service.serviceName 
                        ? Colors.blueAccent 
                        : Colors.grey[200],
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(service.serviceName, style: const TextStyle(fontSize: 16)),
                ),
              );
            },
          ),
        ),
        // Vertical Category List
        Expanded(
          child: ListView.builder(
            controller: _verticalController,
            itemCount: serviceData.expand((s) => s.categories).length,
            itemBuilder: (context, index) {
              // Map global index to service + category
              int serviceIndex = 0;
              int categoryIndex = index;
              while (categoryIndex >= serviceData[serviceIndex].categories.length) {
                categoryIndex -= serviceData[serviceIndex].categories.length;
                serviceIndex++;
              }
              final category = serviceData[serviceIndex].categories[categoryIndex];
              return Container(
                height: _categoryItemHeight,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                alignment: Alignment.centerLeft,
                child: Text(category, style: const TextStyle(fontSize: 18)),
              );
            },
          ),
        ),
      ],
    ),
  );
}
Key Edge Cases to Handle
  • Dynamic Category Item Heights: If your category items have variable heights, use GlobalKey to measure each service’s category group height after the widget renders. Use WidgetsBinding.instance.addPostFrameCallback to calculate offsets once the layout is complete.
  • Performance: Add a debounce to the vertical scroll listener to avoid excessive calculations during fast scrolling.
  • Active Tab Highlighting: Track the currently active service (update it in _onVerticalScroll and _onServiceTabTap) to highlight the selected tab.

Hope this gives you a solid starting point! Let me know if you run into issues with dynamic heights or other edge cases.

内容的提问来源于stack exchange,提问作者Tamil Arasu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 17:58:10