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

Flutter透视PageView实现无限循环的问题求助

实现带透视效果的无限循环PageView问题

我实现了一个带透视效果的PageView,已适配新版Flutter且运行正常。现在需要让这个PageView实现无限循环效果,尝试过多种常规无限循环方案,但都会破坏透视效果。确认该功能可实现,求可行方案!


原代码:

Scaffold部分

class ProjectsViewState extends State<ProjectsView>
    with TickerProviderStateMixin {
  late PageViewHolder holder;

  late PageController _controller;
  double fraction = 0.50;

  @override
  void initState() {
    super.initState();
    holder = PageViewHolder(value: 2.0);
    _controller = PageController(initialPage: 2, viewportFraction: fraction);
    _controller.addListener(() {
      holder.setValue(_controller.page);
    });
  }

  int currentPage = 2;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ChangeNotifierProvider<PageViewHolder>.value(
          value: holder,
          child: Flexible(
            child: PageView.builder(
              controller: _controller,
              onPageChanged: (value) {
                setState(() {
                  currentPage = value;
                });
              },
              itemCount: projectsCounter,
              physics: const BouncingScrollPhysics(),
              itemBuilder: (context, index) {
                return ProjectsGalleryView(
                  number: index.toDouble(),
                  fraction: fraction,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

ProjectsGalleryView部分

class ProjectsGalleryView extends StatefulWidget {
  final double number;
  final double fraction;

  const ProjectsGalleryView(
      {super.key, required this.number, required this.fraction});

  @override
  State<ProjectsGalleryView> createState() => _ProjectsGalleryViewState();
}

class _ProjectsGalleryViewState extends State<ProjectsGalleryView>
    with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    double value = Provider.of<PageViewHolder>(context).value;
    double diff = (widget.number - value);

    //Matrix for Elements
    final Matrix4 pvMatrix = Matrix4.identity()
      ..setEntry(3, 3, 1 / 0.8) // Increasing Scale by 80%
      // ..setEntry(1, 1, widget.fraction) // Changing Scale Along Y Axis
      ..setEntry(3, 0, 0.003 * -diff); // Changing Perspective Along X Axis

    return Transform(
      transform: pvMatrix,
      alignment: FractionalOffset.center,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 0),
            child: SizedBox(
              height: 200,
              width: 200,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.redAccent,
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ),
          if (diff <= 1.0 && diff >= -1.0) ...[
            AnimatedOpacity(
              duration: const Duration(milliseconds: 100),
              opacity: 1 - diff.abs(),
              child: const Padding(
                padding: EdgeInsets.symmetric(
                  horizontal: 15,
                  vertical: 20,
                ),
                child: Text(
                  'A Text with animated opacity',
                ),
              ),
            ),
          ]
        ],
      ),
    );
  }
}

解决方案

常规无限循环方案破坏透视的核心原因:透视效果依赖controller.page与页面索引的差值计算,而无限循环时controller.page会无限增大/减小,导致差值超出合理范围,打乱透视矩阵的计算逻辑。

以下是修正方案,核心是对page值和索引做循环归一化处理:

1. 修改ProjectsViewState

class ProjectsViewState extends State<ProjectsView>
    with TickerProviderStateMixin {
  late PageViewHolder holder;
  late PageController _controller;
  double fraction = 0.50;
  // 设足够大的初始页,避免快速滚动触达边界
  final int initialPage = 1000;

  @override
  void initState() {
    super.initState();
    holder = PageViewHolder(value: initialPage.toDouble());
    _controller = PageController(initialPage: initialPage, viewportFraction: fraction);
    _controller.addListener(() {
      holder.setValue(_controller.page ?? initialPage.toDouble());
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ChangeNotifierProvider<PageViewHolder>.value(
          value: holder,
          child: Flexible(
            child: PageView.builder(
              controller: _controller,
              // 设极大值模拟无限滚动
              itemCount: 10000,
              physics: const BouncingScrollPhysics(),
              itemBuilder: (context, index) {
                // 取模得到真实页面索引,实现循环复用
                final realIndex = index % projectsCounter;
                return ProjectsGalleryView(
                  number: realIndex.toDouble(),
                  fraction: fraction,
                  totalCount: projectsCounter.toDouble(),
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

2. 修改ProjectsGalleryView

class ProjectsGalleryView extends StatefulWidget {
  final double number;
  final double fraction;
  // 新增总页数参数,用于差值归一化
  final double totalCount;

  const ProjectsGalleryView({
    super.key,
    required this.number,
    required this.fraction,
    required this.totalCount,
  });

  @override
  State<ProjectsGalleryView> createState() => _ProjectsGalleryViewState();
}

class _ProjectsGalleryViewState extends State<ProjectsGalleryView>
    with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    double value = Provider.of<PageViewHolder>(context).value;
    // 将无限增长的page值映射到[0, totalCount)区间
    double normalizedValue = value % widget.totalCount;
    if (normalizedValue < 0) normalizedValue += widget.totalCount;
    
    // 调整差值为循环范围内的合理值,确保透视计算正确
    double diff = widget.number - normalizedValue;
    if (diff > widget.totalCount / 2) {
      diff -= widget.totalCount;
    } else if (diff < -widget.totalCount / 2) {
      diff += widget.totalCount;
    }

    //Matrix for Elements
    final Matrix4 pvMatrix = Matrix4.identity()
      ..setEntry(3, 3, 1 / 0.8)
      ..setEntry(3, 0, 0.003 * -diff);

    return Transform(
      transform: pvMatrix,
      alignment: FractionalOffset.center,
      child: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 0),
            child: SizedBox(
              height: 200,
              width: 200,
              child: Container(
                decoration: BoxDecoration(
                  color: Colors.redAccent,
                  borderRadius: BorderRadius.circular(10),
                ),
              ),
            ),
          ),
          if (diff <= 1.0 && diff >= -1.0) ...[
            AnimatedOpacity(
              duration: const Duration(milliseconds: 100),
              opacity: 1 - diff.abs(),
              child: const Padding(
                padding: EdgeInsets.symmetric(
                  horizontal: 15,
                  vertical: 20,
                ),
                child: Text(
                  'A Text with animated opacity',
                ),
              ),
            ),
          ]
        ],
      ),
    );
  }
}

关键说明

  • 初始页设为大数:避免用户快速滚动到边界,保证无限循环的流畅性。
  • 索引取模:index % projectsCounter让PageView循环复用真实页面内容。
  • 差值归一化:将无限增长的page值映射到真实页面区间,并调整差值为循环范围内的合理值,确保透视矩阵计算始终正确,不会因page值过大/过小导致透视错乱。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 01:05:23