flutterdynamicflutter-swiper

How to get this simple DynamicStackCardSwiper code working?


I am trying the package dynamic_stack_card_swiper from pub.dev because of the following requirements:

Initially, I tried the builtin CarouselView but I couldn't figure out a way to adjust the height dynamically as per the active card. So, I searched the internet for help, and found suggestion to this dynamic_stack_card_swiper package.

The Problem: The example given on the packages page in pub.dev is too complicated for me to understand. The Usage section in the Readme tab shows a simple example, but I think that's incomplete because it does not work. I've tried a couple of things but couldn't understand how this works.

Question: How can I fix the code given below? The DynamicStackCardSwiper widget does not render anything and also there are no error messages. Note: I've wrapped DynamicStackCardSwiper with SizedBox only to see if this would render something, but that didn't happen either. Ideally, I wouldn't want to wrap within SizedBox because of the requirement I've stated above.

The minimal code:

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: MyHomePage());
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<String> _myListOfStrings = ["one", "two", "three"];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Dynamic Stack Card Swiper"),
        backgroundColor: Colors.blue.shade700,
      ),
      backgroundColor: Colors.amber,
      body: SingleChildScrollView(
        child: Padding(
          padding: EdgeInsetsGeometry.all(10.0),
          child: SizedBox(
            height: 300.0,
            child: DynamicStackCardSwiper<List<String>>(
              cardBuilder: (BuildContext context, List<String> item) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.blueAccent,
                  child: Center(
                    child: Text(
                      // item[0],
                      "Hello world",
                      style: TextStyle(color: Colors.black),
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

Solution

  • 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: MyHomePage());
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key});
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      List<String> _myListOfStrings = ["one", "two", "three", "four"];
    
      refresh() {
        setState(() {
          _myListOfStrings = ["one", "two", "three", "four"];
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("Swipe Card Queue"),
            backgroundColor: Colors.blue.shade700,
          ),
          backgroundColor: Colors.amber,
          body: Column(
            children: [
              Expanded(
                child: Center(
                  child:
                      _myListOfStrings.isEmpty
                          ? InkWell(
                            onTap: refresh,
                            child: const Text(
                              "No more cards!",
                              style: TextStyle(fontSize: 28),
                            ),
                          )
                          : Stack(
                            alignment: Alignment.center,
                            children:
                                _myListOfStrings
                                    .asMap()
                                    .entries
                                    .toList()
                                    .reversed
                                    .map((entry) {
                                      final index = entry.key;
                                      final value = entry.value;
    
                                      return DisappearOnExitWidget(
                                        onDisappear: () {
                                          setState(() {
                                            _myListOfStrings.removeAt(index);
                                          });
                                        },
                                        child: AnimatedPadding(
                                          duration: const Duration(
                                            milliseconds: 300,
                                          ),
                                          padding: EdgeInsets.only(
                                            right: 20.0,
                                            left: 20.0,
                                            top: 40.0,
                                            bottom: 40.0 - (index * (_myListOfStrings.length * 2)),
                                          ),
                                          child: Card(
                                            elevation: 8,
                                            shape: RoundedRectangleBorder(
                                              borderRadius:
                                                  BorderRadius.circular(20),
                                            ),
                                            color: Colors.blue.shade700,
                                            child: Center(
                                              child: Text(
                                                value,
                                                style: const TextStyle(
                                                  fontSize: 32,
                                                  color: Colors.white,
                                                ),
                                              ),
                                            ),
                                          ),
                                        ),
                                      );
                                    })
                                    .toList(),
                          ),
                ),
              ),
            ],
          ),
        );
      }
    }
    
    class DisappearOnExitWidget extends StatefulWidget {
      final Widget child;
      final VoidCallback? onDisappear;
      final Duration animationDuration;
      final Curve animationCurve;
      final double boundaryThreshold;
    
      const DisappearOnExitWidget({
        super.key,
        required this.child,
        this.onDisappear,
        this.animationDuration = const Duration(milliseconds: 300),
        this.animationCurve = Curves.elasticOut,
        this.boundaryThreshold = 0.0, // How far outside before disappearing
      });
    
      @override
      State<DisappearOnExitWidget> createState() =>
          _DisappearOnExitWidgetState();
    }
    
    class _DisappearOnExitWidgetState extends State<DisappearOnExitWidget>
        with TickerProviderStateMixin {
      late AnimationController _springController;
      late AnimationController _fadeController;
      late Animation<Offset> _springAnimation;
      late Animation<double> _scaleAnimation;
    
      Offset _dragOffset = Offset.zero;
      Offset _startPosition = Offset.zero;
      bool _isDragging = false;
      bool _isDisappearing = false;
      double _opacity = 1.0;
      Size _screenSize = Size.zero;
      final GlobalKey _widgetKey = GlobalKey();
    
      @override
      void initState() {
        super.initState();
    
        _springController = AnimationController(
          duration: widget.animationDuration,
          vsync: this,
        );
    
        _fadeController = AnimationController(
          duration: Duration(milliseconds: 400),
          vsync: this,
        );
    
        _springAnimation = Tween<Offset>(
          begin: Offset.zero,
          end: Offset.zero,
        ).animate(
          CurvedAnimation(
            parent: _springController,
            curve: widget.animationCurve,
          ),
        );
    
        _scaleAnimation = Tween<double>(
          begin: 1.0,
          end: 0.5,
        ).animate(
          CurvedAnimation(
            parent: _fadeController,
            curve: Curves.easeOut,
          ),
        );
      }
    
      @override
      void dispose() {
        _springController.dispose();
        _fadeController.dispose();
        super.dispose();
      }
    
      void _onPanStart(DragStartDetails details) {
        setState(() {
          _isDragging = true;
          _startPosition = details.localPosition;
        });
        _springController.stop();
      }
    
      void _onPanUpdate(DragUpdateDetails details) {
        if (_isDisappearing) return;
        setState(() {
          _dragOffset = details.localPosition - _startPosition;
          _opacity = _calculateOpacity();
        });
      }
    
      double _calculateOpacity() {
        if (_screenSize == Size.zero) return 1.0;
    
        final RenderBox? renderBox =
            _widgetKey.currentContext?.findRenderObject() as RenderBox?;
        if (renderBox == null) return 1.0;
    
        final widgetSize = renderBox.size;
        final screenWidth = _screenSize.width;
        final screenHeight = _screenSize.height;
    
        // Get widget's top-left position relative to screen
        final globalPosition = renderBox.localToGlobal(Offset.zero);
        final widgetLeft = globalPosition.dx;
        final widgetTop = globalPosition.dy;
        final widgetRight = widgetLeft + widgetSize.width;
        final widgetBottom = widgetTop + widgetSize.height;
    
        final isOutOfBounds =
            widgetLeft <= 0 ||
            widgetTop <= kToolbarHeight + 20 ||
            widgetRight >= screenWidth ||
            widgetBottom >= screenHeight;
    
        if (isOutOfBounds) {
          return 0.0;
        }
    
        return 1.0;
      }
    
      void _onPanEnd(DragEndDetails details) {
        setState(() {
          _isDragging = false;
        });
        if (_opacity == 0.0) {
          _disappear();
        } else {
          _animateBack();
        }
      }
    
      void _animateBack() {
        setState(() {
          _opacity = 1.0;
        });
    
        _springAnimation = Tween<Offset>(
          begin: _dragOffset,
          end: Offset.zero,
        ).animate(
          CurvedAnimation(
            parent: _springController,
            curve: widget.animationCurve,
          ),
        );
    
        _springController.forward(from: 0).then((_) {
          if (mounted) {
            setState(() {
              _dragOffset = Offset.zero;
            });
          }
        });
      }
    
      void _disappear() {
        if (_isDisappearing) return;
    
        setState(() {
          _isDisappearing = true;
        });
    
        _fadeController.forward(from: 0).then((_) {
          widget.onDisappear?.call();
    
          if (mounted) {
            setState(() {
              _dragOffset = Offset.zero;
              _opacity = 1.0;
              _isDisappearing = false;
            });
            _fadeController.reset();
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        _screenSize = MediaQuery.of(context).size;
    
        return GestureDetector(
          onPanStart: _onPanStart,
          onPanUpdate: _onPanUpdate,
          onPanEnd: _onPanEnd,
          child: AnimatedBuilder(
            animation: Listenable.merge([_springController, _fadeController]),
            builder: (context, child) {
              Offset currentOffset =
                  _isDragging
                      ? _dragOffset
                      : (_isDisappearing
                          ? _dragOffset
                          : _springAnimation.value);
              double currentScale =
                  _isDisappearing ? _scaleAnimation.value : 1.0;
    
              return Transform.translate(
                offset: currentOffset,
                child: Transform.scale(
                  scale: currentScale,
                  child: Container(
                    key: _widgetKey,
                    child: widget.child,
                  ),
                ),
              );
            },
          ),
        );
      }
    }
    

    i think that DisappearOnExitWidget will does what you ask without any external package