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

如何在Flutter中实现可让组件绕原点旋转的圆形ListView?

实现Flutter环形旋转ListView的两种实用方案

嘿,我之前刚好做过类似的环形列表效果,给你分享两种靠谱的实现思路,都能达到你要的「Widget绕原点旋转排列+滚动交互」的效果~


方案一:用Flow Widget实现可控旋转的环形布局

Flow是Flutter里专门用来做自定义子组件布局的widget,非常适合这种需要统一变换的场景。我们可以通过自定义FlowDelegate来计算每个子组件的环形位置,再结合手势拖拽来控制旋转角度。

完整可运行代码示例

import 'dart:math';
import 'package:flutter/material.dart';

class CircularListView extends StatefulWidget {
  final List<Widget> children;
  final double radius; // 环形半径

  const CircularListView({
    super.key,
    required this.children,
    this.radius = 150.0,
  });

  @override
  State<CircularListView> createState() => _CircularListViewState();
}

class _CircularListViewState extends State<CircularListView> {
  double _rotationAngle = 0.0; // 当前旋转角度(弧度)
  double _previousDragAngle = 0.0;

  // 计算拖拽位置对应的角度
  double _getAngleFromOffset(Offset offset) {
    final center = Offset(MediaQuery.of(context).size.width / 2, MediaQuery.of(context).size.height / 2);
    final dx = offset.dx - center.dx;
    final dy = offset.dy - center.dy;
    return atan2(dy, dx);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: (details) {
        _previousDragAngle = _getAngleFromOffset(details.globalPosition);
      },
      onPanUpdate: (details) {
        final currentAngle = _getAngleFromOffset(details.globalPosition);
        final delta = currentAngle - _previousDragAngle;
        setState(() {
          _rotationAngle += delta;
        });
        _previousDragAngle = currentAngle;
      },
      child: Flow(
        delegate: _CircularFlowDelegate(
          rotationAngle: _rotationAngle,
          radius: widget.radius,
          childCount: widget.children.length,
        ),
        children: widget.children,
      ),
    );
  }
}

class _CircularFlowDelegate extends FlowDelegate {
  final double rotationAngle;
  final double radius;
  final int childCount;

  _CircularFlowDelegate({
    required this.rotationAngle,
    required this.radius,
    required this.childCount,
  });

  @override
  void paintChildren(FlowPaintingContext context) {
    final centerX = context.size.width / 2;
    final centerY = context.size.height / 2;
    final angleStep = 2 * pi / childCount; // 每个子组件的间隔角度

    for (int i = 0; i < childCount; i++) {
      final currentAngle = angleStep * i + rotationAngle;
      // 计算子组件的环形位置
      final x = centerX + radius * cos(currentAngle) - context.getChildSize(i)!.width / 2;
      final y = centerY + radius * sin(currentAngle) - context.getChildSize(i)!.height / 2;
      // 绘制子组件,同时让组件朝向中心
      context.paintChild(
        i,
        transform: Matrix4.translationValues(x, y, 0)
          ..rotateZ(-currentAngle - pi / 2), // 调整组件朝向
      );
    }
  }

  @override
  bool shouldRepaint(covariant _CircularFlowDelegate oldDelegate) {
    return rotationAngle != oldDelegate.rotationAngle ||
        radius != oldDelegate.radius ||
        childCount != oldDelegate.childCount;
  }
}

// 使用示例
void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: CircularListView(
            radius: 180,
            children: List.generate(8, (index) {
              return Container(
                width: 60,
                height: 60,
                decoration: BoxDecoration(
                  color: Colors.primaries[index % Colors.primaries.length],
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Center(
                  child: Text(
                    '$index',
                    style: const TextStyle(color: Colors.white, fontSize: 18),
                  ),
                ),
              );
            }),
          ),
        ),
      ),
    );
  }
}

关键细节说明

  • FlowDelegate的paintChildren方法负责用三角函数计算每个子组件的环形坐标
  • 通过Matrix4同时实现平移和旋转,让组件既在环形上排列,又自动朝向中心
  • 手势拖拽时实时计算角度变化,更新旋转值实现流畅的滚动体验

方案二:用ListView.builder实现无限滚动环形列表

如果你需要类似普通ListView的无限滚动、下拉刷新等原生特性,可以用ListView.builder结合Transform,根据item索引和滚动偏移计算环形位置。

核心代码示例

import 'dart:math';
import 'package:flutter/material.dart';

class InfiniteCircularListView extends StatefulWidget {
  final int itemCount;
  final double radius;

  const InfiniteCircularListView({
    super.key,
    required this.itemCount,
    this.radius = 150.0,
  });

  @override
  State<InfiniteCircularListView> createState() => _InfiniteCircularListViewState();
}

class _InfiniteCircularListViewState extends State<InfiniteCircularListView> {
  final ScrollController _scrollController = ScrollController();
  double _scrollOffset = 0.0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      setState(() {
        _scrollOffset = _scrollController.offset;
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final center = Offset(MediaQuery.of(context).size.width / 2, MediaQuery.of(context).size.height / 2);
    final itemSize = 60.0; // 每个item的尺寸
    final angleStep = 2 * pi / widget.itemCount;

    return ListView.builder(
      controller: _scrollController,
      itemCount: widget.itemCount,
      itemBuilder: (context, index) {
        // 将滚动偏移转换为旋转角度
        final scrollAngle = (_scrollOffset / itemSize) * angleStep;
        final currentAngle = angleStep * index + scrollAngle;

        final x = center.dx + widget.radius * cos(currentAngle) - itemSize / 2;
        final y = center.dy + widget.radius * sin(currentAngle) - itemSize / 2;

        return Transform(
          transform: Matrix4.translationValues(x - center.dx, y - center.dy, 0)
            ..rotateZ(-currentAngle - pi / 2),
          origin: center,
          child: Container(
            width: itemSize,
            height: itemSize,
            decoration: BoxDecoration(
              color: Colors.primaries[index % Colors.primaries.length],
              borderRadius: BorderRadius.circular(8),
            ),
            child: Center(
              child: Text(
                '$index',
                style: const TextStyle(color: Colors.white, fontSize: 18),
              ),
            ),
          ),
        );
      },
    );
  }
}

关键细节说明

  • 通过ScrollController监听滚动偏移,将偏移量转换为环形旋转角度
  • 每个item的位置由索引和当前滚动角度共同计算,滚动时自动同步旋转
  • 完全兼容ListView的原生特性,比如无限滚动、下拉刷新、滚动物理效果

两种方案各有侧重:如果需要极致的自定义布局灵活性,选Flow方案;如果想复用ListView的原生滚动能力,选第二种方案。

内容的提问来源于stack exchange,提问作者Howard Tseng

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:01:44