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

求助:在Flutter中实现水平列表元素图片的缩放切换动画

Implementing Linked Background-List Animation in Flutter

Hey there! I’ve built a similar animation for a project before, so let me walk you through how to pull this off. The core idea is to layer the animated background and horizontal list with a Stack, then trigger size/position transitions based on which list item is active.

Core Implementation Approach

  • Layer with Stack: Place the animated background image at the bottom of a Stack, and the horizontal list (I’ll use PageView for smooth swipe transitions) on top.
  • Track Active Item: Use a state variable to keep tabs on the currently selected list item index.
  • Animate with AnimatedContainer: Let AnimatedContainer handle smooth transitions between the "fullscreen background" and "list item size" states.
  • Sync Background and List: When the first list item is active, its image expands to fullscreen, and the item itself hides (since it overlaps with the background). When switching to other items, the previous background shrinks back to the first list item’s position and size, while the new active item’s image takes over as the fullscreen background.

Full Working Code Example

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const BackgroundListAnimation(),
      theme: ThemeData(primarySwatch: Colors.blue),
    );
  }
}

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

  @override
  State<BackgroundListAnimation> createState() => _BackgroundListAnimationState();
}

class _BackgroundListAnimationState extends State<BackgroundListAnimation> {
  // Sample items with image URLs and titles
  final List<Map<String, dynamic>> _items = [
    {'image': 'https://picsum.photos/id/1015/800/600', 'title': 'Mountain'},
    {'image': 'https://picsum.photos/id/1016/800/600', 'title': 'Ocean'},
    {'image': 'https://picsum.photos/id/1018/800/600', 'title': 'Forest'},
    {'image': 'https://picsum.photos/id/1019/800/600', 'title': 'Desert'},
  ];

  int _currentActiveIndex = 0;
  final PageController _pageController = PageController(initialPage: 0);

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

  @override
  Widget build(BuildContext context) {
    final screenSize = MediaQuery.of(context).size;
    // Define fixed size for list items
    final listItemWidth = screenSize.width * 0.66;
    final listItemHeight = screenSize.height * 0.3;

    return Scaffold(
      body: Stack(
        children: [
          // Animated Background Image
          AnimatedContainer(
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOut,
            // Expand to fullscreen if first item is active, else match list item size
            width: _currentActiveIndex == 0 ? screenSize.width : listItemWidth,
            height: _currentActiveIndex == 0 ? screenSize.height : listItemHeight,
            // Position at top-left corner when fullscreen, else align with first list item
            margin: _currentActiveIndex == 0
                ? EdgeInsets.zero
                : EdgeInsets.only(
                    top: screenSize.height * 0.1,
                    left: (screenSize.width - listItemWidth) / 2,
                  ),
            decoration: BoxDecoration(
              // Remove border radius when fullscreen
              borderRadius: _currentActiveIndex == 0
                  ? BorderRadius.zero
                  : BorderRadius.circular(16),
              image: DecorationImage(
                image: NetworkImage(_items[_currentActiveIndex]['image']),
                fit: BoxFit.cover,
              ),
            ),
          ),
          // Horizontal List (PageView)
          Positioned(
            top: screenSize.height * 0.1,
            left: 0,
            right: 0,
            child: SizedBox(
              height: listItemHeight,
              child: PageView.builder(
                controller: _pageController,
                onPageChanged: (newIndex) {
                  setState(() {
                    _currentActiveIndex = newIndex;
                  });
                },
                itemCount: _items.length,
                itemBuilder: (context, index) {
                  final currentItem = _items[index];
                  // Hide first item when it's active (background covers it)
                  return Opacity(
                    opacity: index == 0 && _currentActiveIndex == 0 ? 0 : 1,
                    child: Container(
                      width: listItemWidth,
                      margin: const EdgeInsets.symmetric(horizontal: 8),
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(16),
                        image: DecorationImage(
                          image: NetworkImage(currentItem['image']),
                          fit: BoxFit.cover,
                        ),
                      ),
                      child: Align(
                        alignment: Alignment.bottomLeft,
                        child: Padding(
                          padding: const EdgeInsets.all(16.0),
                          child: Text(
                            currentItem['title'],
                            style: const TextStyle(
                              color: Colors.white,
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Key Code Breakdown

  1. State Management: _currentActiveIndex tracks which list item is in focus, and PageController handles smooth swiping between items.
  2. Animated Background: The AnimatedContainer adjusts its size, position, and border radius automatically when _currentActiveIndex changes. This creates the expand/shrink effect without needing a separate AnimationController.
  3. List Item Visibility: When the first item is active, we set its opacity to 0 so it doesn’t overlap with the fullscreen background. For all other states, all list items are visible normally.
  4. Smooth Transitions: The 500ms duration and Curves.easeInOut curve ensure the animation feels natural and responsive.

Quick Optimization Tips

  • Local Assets: If using local images instead of network URLs, swap NetworkImage with AssetImage and add your assets to pubspec.yaml.
  • Custom Curves: Adjust the curve property in AnimatedContainer to match your app’s vibe (e.g., Curves.fastOutSlowIn for a snappier feel).
  • ListView Alternative: If you prefer ListView over PageView, use a ScrollController to detect when the first item enters the viewport and trigger the animation.
  • Performance: For large lists, keep using PageView.builder (or ListView.builder) to only render items that are visible on screen.

内容的提问来源于stack exchange,提问作者Anas Ezzat

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 16:43:13