IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 跟我学习Flutter实现SwipeCard效果 -> 正文阅读

[移动开发]跟我学习Flutter实现SwipeCard效果

写在前头

记得很久以前曾经看到过一个交友App的选择好友功能,当时觉得特别炫酷。后来在探探、tinder、九九之恋上也看到了类似的效果。正好最近也在复习flutter,所以就写了一个flutter版本的SwipeCard效果。独乐乐不如众乐乐,写篇文章留个纪念,也送给有需要的同学。

实现效果

效果图
实现效果

思路

要实现这个效果,需要按照步骤进行如下操作:

  1. 按照效果布局。
  2. 调整布局参数,实现布局可根据一个或者几个参数动态变化。
  3. 添加手势动作,通过手势动作修改参数,驱动页面展现。
  4. 为了效果流畅,添加动画效果。手势执行完成之后根据状态执行动画。

实现(完整代码在最后)

1、按照效果布局

如果要实现这个布局效果,第一步就需要知道当前Widget尺寸,故我们选择使用LayoutBuilder来做根布局。使用LayoutBuild的好处是,在创建布局之前我们就能获得控件尺寸。知道尺寸之后就可以动态计算出每个Page组件尺寸和摆放位置。

2、布局参数化?

如下:

Opacity(
          opacity: 0.8 + 0.2 * _scale,
          child: Transform.scale(
            scale: layer1Scale + (1 - layer1Scale) * _scale,
            alignment: Alignment.topLeft,
            child: Transform.translate(
              offset: Offset(layer1X + (0 - layer1X) * _scale,
                  layer1Y + (0 - layer1Y) * _scale),
              child: SizedBox(
                child: widget.children[1],
                width: constraints.maxWidth,
                height: constraints.maxHeight - 2 * bottom,
              ),
            ),
          ),
        )

例如:scale属性就需要根据_scale来动态改变。后续只需要驱动_scale改变即可改变UI效果。

3、添加手势

手势实现起来就相对比较简单,通过GestureDetector来监控手势变化,故根布局是要使用GestureDetector,然后监听onPanUpdate、onPanEnd、onPanCancel事件。通过这几个事件来改变参数。

如下:

onPanUpdate: _onPanUpdate,
onPanEnd: (DragEndDetails? details) {
setState(() {
    _upX = _moveX;
    _upY = _moveY;
    _upScale = _scale;

    if (_isToRight()) {
    _direction = Direction.right;
    } else if (_isToLeft()) {
    _direction = Direction.left;
    } else {
    if (details != null) {
        if (details.velocity.pixelsPerSecond.dx > 1000) {
        _direction = Direction.right;
        } else if (details.velocity.pixelsPerSecond.dx < -1000) {
        _direction = Direction.left;
        } else {
        _direction = Direction.stay;
        }
    } else {
        _direction = Direction.stay;
    }
    }

    if(widget.children.length == 1) {
    _direction = Direction.stay;
    }
    _startAnim();
});
},
onPanCancel: () {
setState(() {
    _moveX = 0;
    _moveY = 0;
    _scale = 0;
});
}

?代码中改变的就是_scale、_moveX、moveY参数。

4、添加动画

动画实现是基于AnimationController来实现,通过这个动画改变的仍然是参数化的数据_scale、_moveX、moveY。

如下:

_controller = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 300),
    lowerBound: 0,
    upperBound: 1.0);
_controller.addListener(() {
    setState(() {
    double width = context.size!.width;
    if (_direction == Direction.right) {
        _scale = _upScale + (1 - _upScale) * _controller.value;
        _moveX = _upX + (width - _upX) * _controller.value;
    } else if (_direction == Direction.left) {
        _scale = _upScale + (1 - _upScale) * _controller.value;
        _moveX = _upX + (-width - _upX) * _controller.value;
    } else {
        _scale = _upScale + (0 - _upScale) * _controller.value;
        _moveX = _upX + (0 - _upX) * _controller.value;
        _moveY = _upY + (0 - _upY) * _controller.value;
    }
    });
});
_controller.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
    if (_direction == Direction.left) {
        widget.cardSelected(false);
    } else if (_direction == Direction.right) {
        widget.cardSelected(true);
    }
    setState(() {
        _moveX = 0;
        _moveY = 0;
        _scale = 0;
    });
    }
});

决定动画如何执行,是根据手势结果来区分的。代码中是通过_direction来区别如何执行动画效果。

完成上述步骤基本上就完成了一个自定义SwipeCard效果。

完整代码

// Copyright (C) 2021 The swipe_card Project.
// @auth: yangzc
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

// Examples:
// class _UIHomeState extends State<UIHome> {
//
//   List<Widget> list = [];
//   int _index = 0;
//
//   @override
//   void initState() {
//     super.initState();
//     list.add(Card(
//       child: Container(
//         child: Center(
//           child: Text(
//             "Card 3 - $_index",
//             style: TextStyle(fontSize: 30),
//           ),
//         ),
//         height: double.infinity,
//         width: double.infinity,
//         color: Colors.blue,
//       ),
//     ));
//     list.add(Card(
//       child: Container(
//         child: Center(
//           child: Text(
//             "Card 2 - $_index",
//             style: TextStyle(fontSize: 30),
//           ),
//         ),
//         height: double.infinity,
//         width: double.infinity,
//         color: Colors.yellow,
//       ),
//     ));
//     list.add(Card(
//       child: Container(
//         child: Center(
//           child: Text(
//             "Card 1 - $_index",
//             style: TextStyle(fontSize: 30),
//           ),
//         ),
//         height: double.infinity,
//         width: double.infinity,
//         color: Colors.red,
//       ),
//     ));
//   }
//
//   @override
//   Widget build(BuildContext context) {
//     return Scaffold(
//       body: Container(
//         child: SwipeCard((bool right) {
//           setState(() {
//             list.removeAt(0);
//           });
//         }, children: list),
//         width: double.infinity,
//         height: double.infinity,
//       ),
//       appBar: AppBar(
//         title: Text("推荐"),
//         centerTitle: true,
//       ),
//     );
//
//   }
// }

/// Card左右选择回调
typedef SwipeCardSelected = Function(bool right);

/// 实现SwipeCard效果
class SwipeCard extends StatefulWidget {
  final List<Widget> children;
  final SwipeCardSelected cardSelected;
  SwipeCard(this.cardSelected, {this.children = const <Widget>[]});

  @override
  State<StatefulWidget> createState() {
    return _SwipeCardState();
  }
}

enum Direction { left,//向左滑动
  right,//向右滑动
  stay //无需滑动
}

class _SwipeCardState extends State<SwipeCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _moveX = 0, _moveY = 0;
  double _scale = 0;
  double _upX = 0, _upY = 0, _upScale = 0;
  Direction _direction = Direction.stay;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this,
        duration: Duration(milliseconds: 300),
        lowerBound: 0,
        upperBound: 1.0);
    _controller.addListener(() {
      setState(() {
        double width = context.size!.width;
        if (_direction == Direction.right) {
          _scale = _upScale + (1 - _upScale) * _controller.value;
          _moveX = _upX + (width - _upX) * _controller.value;
        } else if (_direction == Direction.left) {
          _scale = _upScale + (1 - _upScale) * _controller.value;
          _moveX = _upX + (-width - _upX) * _controller.value;
        } else {
          _scale = _upScale + (0 - _upScale) * _controller.value;
          _moveX = _upX + (0 - _upX) * _controller.value;
          _moveY = _upY + (0 - _upY) * _controller.value;
        }
      });
    });
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        if (_direction == Direction.left) {
          widget.cardSelected(false);
        } else if (_direction == Direction.right) {
          widget.cardSelected(true);
        }
        setState(() {
          _moveX = 0;
          _moveY = 0;
          _scale = 0;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
      final double bottom = constraints.maxHeight * 0.025;

      final layer1X = constraints.maxWidth * 0.05 / 2;
      final layer1Y = (constraints.maxHeight * 0.05) + bottom;
      final layer1Scale = 0.95;

      final layer2X = constraints.maxWidth * (1 - 0.95 * 0.95) / 2;
      final layer2Y = (constraints.maxHeight * (1 - 0.95 * 0.95)) + bottom * 2;
      final layer2Scale = layer1Scale * layer1Scale;

      if (_scale > 1) {
        _scale = 1;
      }
      List<Widget> children = [];
      if (widget.children.length > 2) {
        children.add(Opacity(
          opacity: 0.8 * 0.8 + 0.8 * 0.2 * _scale,
          child: Transform.scale(
            scale: layer2Scale + (layer1Scale - layer2Scale) * _scale,
            alignment: Alignment.topLeft,
            child: Transform.translate(
              offset: Offset(layer2X + (layer1X - layer2X) * _scale,
                  layer2Y + (layer1Y - layer2Y) * _scale),
              child: SizedBox(
                child: widget.children[2],
                width: constraints.maxWidth,
                height: constraints.maxHeight - 2 * bottom,
              ),
            ),
          ),
        ));
      }

      if (widget.children.length > 1) {
        children.add(Opacity(
          opacity: 0.8 + 0.2 * _scale,
          child: Transform.scale(
            scale: layer1Scale + (1 - layer1Scale) * _scale,
            alignment: Alignment.topLeft,
            child: Transform.translate(
              offset: Offset(layer1X + (0 - layer1X) * _scale,
                  layer1Y + (0 - layer1Y) * _scale),
              child: SizedBox(
                child: widget.children[1],
                width: constraints.maxWidth,
                height: constraints.maxHeight - 2 * bottom,
              ),
            ),
          ),
        ));
      }

      if (widget.children.length > 0) {
        children.add(Opacity(
          opacity: 1,
          child: Transform.scale(
            scale: 1,
            child: Transform.translate(
              offset: Offset(_moveX, _moveY),
              child: SizedBox(
                child: widget.children[0],
                width: constraints.maxWidth,
                height: constraints.maxHeight - 2 * bottom,
              ),
            ),
          ),
        ));
      }

      return GestureDetector(
          child: Stack(
            children: children,
          ),
          onPanUpdate: _onPanUpdate,
          onPanEnd: (DragEndDetails? details) {
            setState(() {
              _upX = _moveX;
              _upY = _moveY;
              _upScale = _scale;

              if (_isToRight()) {
                _direction = Direction.right;
              } else if (_isToLeft()) {
                _direction = Direction.left;
              } else {
                if (details != null) {
                  if (details.velocity.pixelsPerSecond.dx > 1000) {
                    _direction = Direction.right;
                  } else if (details.velocity.pixelsPerSecond.dx < -1000) {
                    _direction = Direction.left;
                  } else {
                    _direction = Direction.stay;
                  }
                } else {
                  _direction = Direction.stay;
                }
              }

              if(widget.children.length == 1) {
                _direction = Direction.stay;
              }
              _startAnim();
            });
          },
          onPanCancel: () {
            setState(() {
              _moveX = 0;
              _moveY = 0;
              _scale = 0;
            });
          });
    });
  }

  void _onPanUpdate(DragUpdateDetails details) {
    _moveX += details.delta.dx;
    _moveY += details.delta.dy;

    var size = context.size;
    // var offset = details.localPosition;
    var distance = _moveX / size!.width;

    setState(() {
      _scale = distance < 0 ? -distance : distance;
    });
  }

  bool _isToRight() {
    double width = context.size!.width;
    return _upX > width / 4;
  }

  bool _isToLeft() {
    double width = context.size!.width;
    return _upX < -width / 4;
  }

  void _startAnim() {
    _controller.reset();
    _controller.forward(from: 0);
  }
}

欢迎留言

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-14 11:02:31  更:2021-07-14 11:03:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/1 21:18:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码