Flutter 可拖拽的層疊卡片

藍色微笑ing發表於2019-12-18

效果圖

Flutter 可拖拽的層疊卡片

第一步:通過Stack實現層疊卡片

通過Stack實現層疊效果,它的子Widget的部署實現方式還是比較多的,比如使用Container,計算每個卡片的margin. 使用Positioned,計算left,top,right,bottom. 使用Transform,計算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());
}
複製程式碼

第二步:通過GestureDetector實現拖拽效果

第一層的卡片,需要識別使用者的拖拽手勢。並根據拖拽的距離更新第一層卡片的Offset.

關於手勢識別,可以檢視 [譯] 深入 Flutter 之手勢

_onPanUpdate(DragUpdateDetails details) {
  if (!_isDragging) {
    _isDragging = true;
    return;
  }
  _offsetDx += details.delta.dx;
  _offsetDy += details.delta.dy;
  setState(() {});
}
複製程式碼

第三步:實現移除動畫

當使用者抬起手勢的時候,啟動移除的動畫,也可以把第一層卡片移到最後一層。

切換List的兩個元素:可以使用

list.insert(a, list.removeAt(b));
複製程式碼

移除動畫的時候,需要計算移除的終止點Offset,比如使用者偏左上角拖拽,就從左上角移除。如果使用者拖拽的距離較時,可以選擇恢復原始位置,及Offset.zero。

啟動動畫,其實就是利用Tween,在規定時間內,由一個Offset到另一個Offset,並給Animation新增value監聽,重新整理UI. 新增status監聽,動畫完成時,移除卡片或切換卡片。

關於動畫的瞭解可以參考 Flutter 動畫及示例

DroppableWidget程式碼

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.size.width * 0.1 ||
        _offsetDy.abs() >= context.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();
  }
}

複製程式碼

專案地址

github.com/smiling1990…

相關文章