rendering

Step 1: Stack the cards

There are many ways to deploy sub-widgets, such as using Containers to calculate the margin of each card. C-21. Using Positioned space, count left, top, right, bottom. Using Transform, calculate Offset.

@override
Widget build(BuildContext context) {
  List<Widget> children = [];
  double offset = 8.0;
  for (int i = 0; i < 3; i++) {
    Widget child = Transform.translate(
      child: children[i],
      offset: Offset(offset * i, offset * i),
    );
    children.add(child);
  }
  return Stack(children: children.reversed.toList());
}
Copy the code

Step 2: Implement drag effects through GestureDetector

The first layer of cards needs to recognize the user’s drag gestures. The Offset of the first layer card is updated according to the distance of the drag.

For gesture recognition, see Gestures deep into Flutter.

_onPanUpdate(DragUpdateDetails details) {
  if(! _isDragging) { _isDragging =true;
    return;
  }
  _offsetDx += details.delta.dx;
  _offsetDy += details.delta.dy;
  setState(() {});
}
Copy the code

Step 3: Implement the remove animation

When the user raises the gesture, the animation is activated to remove the card from the first layer to the last layer.

Toggles two elements of the List: available

list.insert(a, list.removeAt(b));
Copy the code

When you remove an animation, you need to calculate the Offset of the animation to be removed. For example, if the user drags the animation from the upper left corner, it will be removed from the upper left corner. If the user drags a small distance, the user can choose to restore the original position and Offset. Zero.

To start the Animation, we use Tween to move from one Offset to another within the specified time, and add value listener to the Animation to refresh UI. Add status listening, remove cards or switch cards when the animation is complete.

For more information about animation, see Flutter animations and examples

DroppableWidget code

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

/// Its child widget should have a fixed width and height
class DroppableWidget extends StatefulWidget {
  /// Children cards
  final List<Widget> children;

  const DroppableWidget({Key key, this.children}) : super(key: key);

  @override
  _DroppableWidgetState createState() => _DroppableWidgetState();
}

class _DroppableWidgetState extends State<DroppableWidget>
    with TickerProviderStateMixin {
  /// Offset
  double _offsetDx;
  double _offsetDy;

  /// Mark if dragging
  bool _isDragging;

  /// Animation Controller
  AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    // Init
    _offsetDx = _offsetDy = 0;
    _isDragging = false; } bool get _isAnimating => _animationController? .isAnimating ??false;

  /// Update offset value
  _onPanUpdate(DragUpdateDetails details) {
    if (_isAnimating) return;
    if(! _isDragging) { _isDragging =true;
      return;
    }
    _offsetDx += details.delta.dx;
    _offsetDy += details.delta.dy;
    setState(() {});
  }

  /// Start animation when PanEnd
  _onPanEnd(DragEndDetails details) {
    if (_isAnimating) return;
    _isDragging = false; Bool change = _offsetDx. Abs () > = context. The size, width * 0.1 | | _offsetDy. The abs () > = context. The size, height * 0.1;if (change) {
      double endX, endY;
      if (_offsetDx.abs() > _offsetDy.abs()) {
        endX = context.size.width * _offsetDx.sign;
        endY = _offsetDy.sign *
            context.size.width *
            _offsetDy.abs() /
            _offsetDx.abs();
      } else {
        endY = context.size.height * _offsetDy.sign;
        endX = _offsetDx.sign *
            context.size.height *
            _offsetDx.abs() /
            _offsetDy.abs();
      }
      _startAnimation(Offset(_offsetDx, _offsetDy), Offset(endX, endY), true);
    } else {
      _startAnimation(Offset(_offsetDx, _offsetDy), Offset.zero, false);
    }
  }

  /// Start animation
  /// [change] if change child, when animation complete
  _startAnimation(Offset begin, Offset end, bool change) {
    _animationController = AnimationController(
      duration: Duration(milliseconds: 200),
      vsync: this,
    );
    var _animation = Tween(begin: begin, end: end).animate(
      _animationController,
    );
    _animationController.addListener(() {
      setState(() {
        _offsetDx = _animation.value.dx;
        _offsetDy = _animation.value.dy;
      });
    });
    _animationController.addStatusListener((status) {
      if(status ! = AnimationStatus.completed)return;
      _offsetDx = 0;
      _offsetDy = 0;
      if (change) {
        widget.children.insert(
          widget.children.length - 1,
          widget.children.removeAt(0),
        );
      }
      setState(() {}); }); _animationController.forward(); } @override Widget build(BuildContext context) { List<Widget> children = []; int length = widget.children? .length ?? 0; Double offset = 8.0;for(int i = 0; i < length; i++) { double dx = i == 0 ? _offsetDx: 0.0; double dy = i == 0 ? _offsetDy: 0.0; Widget child = Transform.translate( child: widget.children[i], offset: Offset(dx + (offset * i), dy + (offset * i)), );if (i == 0) {
        child = GestureDetector(
          child: child,
          onPanEnd: _onPanEnd,
          onPanUpdate: _onPanUpdate,
        );
      }
      children.add(child);
    }
    return Stack(children: children.reversed.toList());
  }

  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }
}

Copy the code

The project address

Github.com/smiling1990…