Search code examples
flutterdartflutter-layoutflutter-animation

How to horizontally scroll stacked positioned widgets in flutter?


I have a stack of positioned containers like this:

enter image description here

I would like to scroll them horizontally and bring that container to the front accordingly namely bring blue one to the front when scrolled right and yellow one when scrolled left.

Something like this but a scroll instead of clicks: enter image description here

I tried using SingleChildScrollView with horizontal scroll but unsurprisingly it didn't work. The code I tried was:

SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Stack(
                  clipBehavior: Clip.none,
                  children: [
                    Container(),
                    Positioned(
                      top: 145.h,
                      left: 100.w,
                      height: 300.h,
                      width: 250.w,
                      child: ElevatedButton(
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.blue,
                          elevation: 3.h,
                          shape: RoundedRectangleBorder(
                            borderRadius:
                                BorderRadius.circular(5.r), //// <-- Radius
                          ),
                        ),
                        child: Text("pp"),
                      ),
                    ),
                    Positioned(
                      top: 145.h,
                      left: 20.w,
                      height: 300.h,
                      width: 250.w,
                      child: ElevatedButton(
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.yellow,
                          elevation: 3.h,
                          shape: RoundedRectangleBorder(
                            borderRadius:
                                BorderRadius.circular(5.r), //// <-- Radius
                          ),
                        ),
                        child: Text("Demo 3"),
                      ),
                    ),
                    Positioned(
                      top: 145.h,
                      left: 60.w,
                      height: 300.h,
                      width: 250.w,
                      child: ElevatedButton(
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red,
                          elevation: 3.h,
                          shape: RoundedRectangleBorder(
                            borderRadius:
                                BorderRadius.circular(5.r), //// <-- Radius
                          ),
                        ),
                        child: Text(
                          "Demo 2",
                          style: TextStyle(
                            fontSize: 20.sp,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ),
                    ),
                    Positioned(
                      top: 145.h,
                      left: 60.w,
                      height: 300.h,
                      width: 250.w,
                      child: ElevatedButton(
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red,
                          elevation: 3.h,
                          shape: RoundedRectangleBorder(
                            borderRadius:
                                BorderRadius.circular(5.r), //// <-- Radius
                          ),
                        ),
                        child: Text(
                          "Demo",
                          style: TextStyle(
                            fontSize: 20.sp,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),

One solution I do know is to use InkWell and use onTap/ElevatedButton onPress to tweak the positioning and bring the corresponding container to the front but the user experience won't be as good as a horizontal scroll.

I also tried using ListWheelScrollView with RotatedBox to simulate horizontal scrolling but it was not good enough.

Any help on this would be greatly appreciated!


Solution

  • Well I found a solution with PageView.builder not with Stack, which is pretty close to what you are trying to make,

    home page where we call our carousel:

    import 'dart:math' as math;
    
    
    class HomeScreen extends StatefulWidget {
      const HomeScreen({Key? key}) : super(key: key);
    
      @override
      State<HomeScreen> createState() => _HomeScreenState();
    }
    
    class _HomeScreenState extends State<HomeScreen> {
      List items = List.generate(
          20,
          (index) => Color((math.Random().nextDouble() * 0xFFFFFF).toInt())
              .withOpacity(1.0)); // list for test
    
      late PageController _pageController; // controller of pageview
    
      @override
      void initState() {
        _pageController = PageController(initialPage: 0, viewportFraction: 0.8);
        super.initState();
      }
    
      @override
      void dispose() {
        _pageController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return ScrollConfiguration(
          behavior: CustomScrollBehavior(),
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: CardCarousel(pageController: _pageController, items: items),
          ),
        );
      }
    }
    

    this is Carousel:

    class CardCarousel extends StatelessWidget {
      final PageController pageController;
      final List items;
    
      const CardCarousel(
          {super.key, required this.pageController, required this.items});
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 400.0,
              width: 300,
              child: PageView.builder(
                controller: pageController,
                itemCount: items.length,
                itemBuilder: (BuildContext context, int index) {
                  return _cardBuilder(context, index);
                },
              ),
            ),
          ],
        );
      }
    
      _cardBuilder(BuildContext context, int index) {
        var item = items[index];
        return AnimatedBuilder(
          animation: pageController,
          builder: (context, child) {
            double value = 1;
            if (pageController.position.haveDimensions) {
              value = (pageController.page! - index);
              value = (1 - (value.abs() * 0.25)).clamp(0.0, 1.0);
              // you can change these values to change the size
              // transformation that i found it
            }
    
            return Center(
              child: Container(
                color: item,
                // change animation right here
                // the value for transform have got above
                height: Curves.easeInOut.transform(value) * 400.0,
                child: child,
              ),
            );
          },
        );
      }
    }
    

    I hope this was helpful, the result be like:

    result