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

Flutter挑战:实现ListView列表项形状切换动画

实现ListView项的形状切换动画

核心实现思路

通过监听ListView的滚动位置,计算每个列表项与视口中心的偏移距离,动态调整项的圆角值——距离视口中心越近,圆角越小(趋近矩形);距离越远,圆角越大(趋近圆形)。用AnimatedContainer实现平滑过渡,比手动使用AnimationBuilder更流畅、更省心。

完整代码示例

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: Scaffold(
        appBar: AppBar(title: const Text('列表项形状动画')),
        body: const ShapeAnimatedListView(),
      ),
    );
  }
}

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

  @override
  State<ShapeAnimatedListView> createState() => _ShapeAnimatedListViewState();
}

class _ShapeAnimatedListViewState extends State<ShapeAnimatedListView> {
  late final ScrollController _scrollController;
  // 存储每个item的圆角值,初始设为圆形(30为示例值,可根据item高度调整)
  final List<double> _borderRadii = List.generate(20, (_) => 30.0);
  final double _itemHeight = 80.0;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController()..addListener(_onScroll);
    // 页面渲染完成后初始化一次圆角状态
    WidgetsBinding.instance.addPostFrameCallback((_) => _updateBorderRadii());
  }

  void _onScroll() {
    _updateBorderRadii();
  }

  void _updateBorderRadii() {
    final viewportHeight = MediaQuery.of(context).size.height;
    final scrollOffset = _scrollController.offset;
    // 计算视口中心的Y坐标
    final viewportCenterY = scrollOffset + viewportHeight / 2;

    setState(() {
      for (int i = 0; i < _borderRadii.length; i++) {
        // 计算当前item中心的Y坐标
        final itemCenterY = scrollOffset + i * _itemHeight + _itemHeight / 2;
        // 计算item与视口中心的距离,做归一化处理
        final distance = (itemCenterY - viewportCenterY).abs();
        // 距离越大,圆角越大(范围0-30,0是矩形,30是圆形)
        _borderRadii[i] = clampDouble(distance / (viewportHeight / 2), 0, 30);
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: _borderRadii.length,
      itemBuilder: (context, index) {
        return AnimatedContainer(
          // 动画时长,可根据需求调整
          duration: const Duration(milliseconds: 200),
          height: _itemHeight,
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          decoration: BoxDecoration(
            color: Colors.blueAccent,
            borderRadius: BorderRadius.circular(_borderRadii[index]),
          ),
          alignment: Alignment.center,
          child: Text(
            '列表项 $index',
            style: const TextStyle(color: Colors.white, fontSize: 18),
          ),
        );
      },
    );
  }
}

关键细节说明

  1. 滚动监听与位置计算:通过ScrollController获取滚动偏移量,结合视口高度算出视口中心,再对比每个列表项的中心位置,得到距离值
  2. AnimatedContainer的优势:自带动画过渡,只需修改borderRadius参数就能自动完成平滑的形状切换,避免了手动管理动画控制器的繁琐,性能也更稳定
  3. 归一化处理:将距离值转化为0-30的圆角范围,保证形状变化的连贯性和视觉一致性

优化方向

  • 如果列表项数量庞大,务必保留ListView.builder的懒加载特性,避免一次性创建所有组件导致性能问题
  • 可以调整圆角最大值和动画时长,适配不同的视觉风格和交互节奏
  • 若需要更精准的"最可见项"判断,可以改为计算列表项的可见区域占比,替代中心距离的判断逻辑

内容的提问来源于stack exchange,提问作者Fahmi Barguellil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 04:05:33