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

Flutter 3.16.0实现3D堆叠轮播组件的需求及问题咨询

Flutter 3.16.0实现3D堆叠轮播(Stacked Carousel)

需求回顾

  • 支持水平滚动
  • 可设置为无限滚动
  • 支持点击事件(onTap/onPressed)
  • 能显示当前、上一个和下一个索引
  • 具备响应式特性

你之前尝试用carousel_slider包实现,但效果未达预期,你的代码如下:

CarouselSlider.builder(
            itemCount: widget.allCards.length,
            options: CarouselOptions(
              viewportFraction: 0.25,
              initialPage: 0,
              enableInfiniteScroll: true,
              enlargeCenterPage: true,
              enlargeFactor: 0.25,
              onPageChanged: (int index, CarouselPageChangedReason reason) {},
              scrollDirection: Axis.horizontal,
              enlargeStrategy: CenterPageEnlargeStrategy.height,
            ),
            itemBuilder: (BuildContext context, int index, int realIndex) {
              final DeckCardModel model = widget.allCards[index];
              return InkWell(
                onTap: () {
                  autoInsertValue(model);
                  setState(() {});
                },
                child: SizedBox(
                  height: Get.width / 2.5,
                  width: Get.width / 4,
                  child: ClipRRect(
                    borderRadius: const BorderRadius.all(
                      Radius.circular(10.0),
                    ),
                    child: CustomImageWidget(
                      imageUrl: model.image ?? "",
                      rounded: false,
                      typeEnum: TypeEnum.user,
                    ),
                  ),
                ),
              );
            },
          ),

解决方案:自定义PageView实现3D堆叠效果

下面是完全满足需求的自定义实现,通过PageView结合Transform组件实现堆叠3D效果,同时支持无限滚动、点击事件和响应式适配:

import 'package:flutter/material.dart';

class Stacked3DCarousel extends StatefulWidget {
  final List<DeckCardModel> items;
  final bool enableInfiniteScroll;
  final Function(DeckCardModel model) onTap;

  const Stacked3DCarousel({
    super.key,
    required this.items,
    this.enableInfiniteScroll = true,
    required this.onTap,
  });

  @override
  State<Stacked3DCarousel> createState() => _Stacked3DCarouselState();
}

class _Stacked3DCarouselState extends State<Stacked3DCarousel> {
  final PageController _pageController = PageController(viewportFraction: 0.7);
  int _currentIndex = 0;

  @override
  void initState() {
    super.initState();
    _pageController.addListener(() {
      setState(() {
        _currentIndex = _pageController.page?.round() ?? 0;
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final cardWidth = screenWidth * 0.7;
    final cardHeight = cardWidth * 1.4; // 保持卡片宽高比

    return Column(
      children: [
        // 索引显示区域
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('上一个: ${_getPrevIndex()}'),
              const SizedBox(width: 20),
              Text('当前: ${_getCurrentIndex()}'),
              const SizedBox(width: 20),
              Text('下一个: ${_getNextIndex()}'),
            ],
          ),
        ),
        // 3D堆叠轮播主体
        SizedBox(
          height: cardHeight + 40, // 预留堆叠间距
          child: PageView.builder(
            controller: _pageController,
            scrollDirection: Axis.horizontal,
            itemCount: widget.enableInfiniteScroll ? null : widget.items.length,
            onPageChanged: (index) {
              setState(() => _currentIndex = index);
            },
            itemBuilder: (context, index) {
              // 处理无限滚动的循环索引
              final realIndex = index % widget.items.length;
              final model = widget.items[realIndex];

              // 计算当前卡片相对于中心的位置,生成3D效果参数
              final pageValue = _pageController.page ?? 0;
              final delta = index - pageValue;
              final scale = 1 - (delta.abs() * 0.2);
              final opacity = 1 - (delta.abs() * 0.3);
              final translateY = delta.abs() * 20;

              return Transform(
                transform: Matrix4.identity()
                  ..translate(0, translateY)
                  ..scale(scale),
                alignment: Alignment.center,
                child: Opacity(
                  opacity: opacity.clamp(0.5, 1.0),
                  child: InkWell(
                    onTap: () => widget.onTap(model),
                    child: Container(
                      width: cardWidth,
                      margin: const EdgeInsets.symmetric(horizontal: 8),
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(12),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black.withOpacity(0.2),
                            blurRadius: 10,
                            offset: const Offset(0, 5),
                          )
                        ],
                      ),
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(12),
                        child: CustomImageWidget(
                          imageUrl: model.image ?? "",
                          rounded: false,
                          typeEnum: TypeEnum.user,
                          height: cardHeight,
                          fit: BoxFit.cover,
                        ),
                      ),
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  // 获取当前真实索引
  int _getCurrentIndex() {
    return _currentIndex % widget.items.length;
  }

  // 获取上一个索引
  int _getPrevIndex() {
    final prev = _currentIndex - 1;
    return prev < 0 ? widget.items.length - 1 : prev % widget.items.length;
  }

  // 获取下一个索引
  int _getNextIndex() {
    return (_currentIndex + 1) % widget.items.length;
  }
}

// 使用示例
class CarouselDemo extends StatelessWidget {
  final List<DeckCardModel> allCards = []; // 替换为你的数据源

  CarouselDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('3D堆叠轮播')),
      body: Stacked3DCarousel(
        items: allCards,
        enableInfiniteScroll: true,
        onTap: (model) {
          autoInsertValue(model);
          // 这里如果需要更新状态,可将此组件改为StatefulWidget
        },
      ),
    );
  }

  void autoInsertValue(DeckCardModel model) {
    // 你的原有逻辑
  }
}

关键特性说明

  1. 无限滚动:通过index % widget.items.length实现循环索引,PageView的itemCount设为null即可开启无限滚动
  2. 3D堆叠效果:利用Transform.scale和Transform.translate根据卡片在PageView中的位置动态调整缩放、位移和透明度,模拟堆叠层级
  3. 点击事件:每个卡片包裹InkWell,点击时直接返回对应的DeckCardModel实例
  4. 索引显示:通过三个工具方法计算并展示当前、上一个、下一个的真实索引
  5. 响应式适配:使用MediaQuery获取屏幕宽度,动态计算卡片尺寸,适配不同设备

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 15:51:04