Flutter 12 实现双击屏幕显示点赞爱心多种动画(AnimationIcon)效果

本文主要是使用Flutter封装一个双击屏幕显示点赞爱心UI效果,并实现了爱心Icon 透明度、缩放、旋转、渐变等动画效果。

实现效果:

实现逻辑:

1、封装FavoriteGesture(爱心手势)实现双击屏幕显示爱心Icon;

2、封装FavoriteAnimationIcon(爱心Icon)实现双击屏幕显示爱心Icon,快速双击时同时显示多个爱心Icon;

3、给FavoriteAnimationIcon增加透明度淡入淡出动画效果;

4、给FavoriteAnimationIcon增加缩放动画效果;

5、给FavoriteAnimationIcon增加旋转动画效果;

7、给FavoriteAnimationIcon增加渐变动画效果;

一、封装FavoriteGesture爱心手势

1)Stack实现多界面堆叠

实现爱心Icon显示在视频界面上层,视频界面由上层child传入,使用Stack实现多界面堆叠;

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)
      ],
    );
  }
}

2)GestureDetector监听双击事件

使用GestureDetector监听屏幕双击事件和点击的坐标,使用Positioned将Icon显示到点击坐标的位置;当用户双击屏幕时,显示爱心Icon,延迟600毫秒,爱心Icon消失。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  bool inFavorite = false;

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = details.globalPosition;
        },
        onDoubleTap: () {
          setState(() {
            inFavorite = true;
          });
          Future.delayed(const Duration(milliseconds: 600), () {
            setState(() {
              inFavorite = false;
            });
          });
        },
        child: Stack(
          children: [
            widget.child,
            if (inFavorite)
              Positioned(
                  top: temp.dy - widget.size / 2,
                  left: temp.dx - widget.size / 2,
                  child: Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)),
          ],
        ));
  }
}

3)RenderBox 实现屏幕坐标转换本地坐标

onDoubleTapDown: (details) {temp = details.globalPosition;} 获取到的点击坐标是屏幕的坐标,需要转换成Icon的父布局Stack的坐标,通过RenderBox 来实现屏幕坐标转换本地坐标。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  bool inFavorite = false;

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  Offset _globalToLocal(Offset global) {
    RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
    return renderBox.globalToLocal(global);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = _globalToLocal(details.globalPosition);
        },
        onDoubleTap: () {
          setState(() {
            inFavorite = true;
          });
          Future.delayed(const Duration(milliseconds: 600), () {
            setState(() {
              inFavorite = false;
            });
          });
        },
        child: Stack(
          children: [
            widget.child,
            if (inFavorite)
              Positioned(
                  top: temp.dy - widget.size / 2,
                  left: temp.dx - widget.size / 2,
                  child: Icon(Icons.favorite, size: widget.size, color: Colors.redAccent)),
          ],
        ));
  }
}

实现效果如下:

二、封装FavoriteAnimationIcon 爱心Icon

1)封装FavoriteAnimationIcon

封装FavoriteAnimationIcon,将Icon坐标、大小、动效等统一封装起来,并暴露动效结束的回调接口给上层,便于清除过期的爱心Icon。

Dart 复制代码
/*年轻人,只管向前看,不要管自暴自弃者的话*/

import 'package:flutter/material.dart';

///create by itz on 2024-10-24 14:19
///desc :
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    Future.delayed(const Duration(milliseconds: 600), () {
      widget.onAnimationComplete?.call();
    });
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: content);
  }
}

2)快速双击时,同时显示多个爱心Icon。

使用list记录双击的坐标位置,实现快速双击屏幕时,同时显示多个爱心Icon;多个爱心Icon,使用Stack实现多爱心Icon堆叠效果。

Dart 复制代码
class FavoriteGesture extends StatefulWidget {
  static const double defaultSize = 100;

  final Widget child;
  final double size;

  const FavoriteGesture({super.key, this.size = defaultSize, required this.child});

  @override
  State<FavoriteGesture> createState() => _FavoriteGestureState();
}

class _FavoriteGestureState extends State<FavoriteGesture> {
  final GlobalKey _key = GlobalKey();

  // 保存当前需要展示的icon
  List<Offset> iconOffsets = [];

  // temp表示最近的一次双击坐标
  Offset temp = Offset.zero;

  Offset _globalToLocal(Offset global) {
    RenderBox renderBox = _key.currentContext?.findRenderObject() as RenderBox;
    return renderBox.globalToLocal(global);
  }

  @override
  Widget build(BuildContext context) {
    var iconStack = Stack(
        children: iconOffsets
            .map((e) => FavoriteAnimationIcon(
                  position: e,
                  size: widget.size,
                  onAnimationComplete: () {
                    setState(() => iconOffsets.remove(e));
                  },
                ))
            .toList());

    return GestureDetector(
        key: _key,
        onDoubleTapDown: (details) {
          temp = _globalToLocal(details.globalPosition);
        },
        onDoubleTap: () {
          // 添加坐标到集合中,触发一次重绘制。根据坐标集合来在不同的坐标上渲染出icon
          setState(() => iconOffsets.add(temp));
        },
        child: Stack(
          children: [
            widget.child,
            iconStack,
          ],
        ));
  }
}

实现效果如下:

三、透明度淡入淡出动画效果

1)Animation 动画

本节使用Animation forward 正向播放来实现淡入淡出动画效果。

2)Opacity Widget

在 Flutter 中,Opacity Widget 用于控制其子部件(child widget)的不透明度。通过设置 Opacity Widget 的 opacity 属性为一个介于 0.0(完全透明)和 1.0(完全不透明)之间的值,可以调整子部件的可见度。

3)淡入淡出动画设计

我们将爱心淡入、显示、淡出时间占比分别设计为:淡入10%、显示70%、淡出20%;这个可以自由设置。

以Icon显示但消失时间1S来计算:

淡入10% = 100ms内 实现 opacity 属性从 0.0(完全透明)和 1.0(完全不透明)的值。

显示70% = 700ms内 实现 opacity 属性值 一直等于1(完全不透明)。

淡入20% = 200ms内 实现 opacity 属性从 0.0(完全透明)和 1.0(完全不透明)的值。

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    return Positioned(
        top: widget.position.dy - widget.size / 2,
        left: widget.position.dx - widget.size / 2,
        child: Opacity(opacity: opacity, child: content));
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

四、缩放动画效果

Transform.scale Widget

在 Flutter 中,Transform.scale Widget 用于按比例缩放其子部件(child widget)。通过设置 Transform.scale 的 scale 属性为一个倍数,可以调整子部件的缩放比例。

Icon缩放时间同上面的 淡入淡出动画设计

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    var child = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: content);

    return Positioned(
        top: widget.position.dy - widget.size / 2,
        left: widget.position.dx - widget.size / 2,
        child: Opacity(opacity: opacity, child: child));
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

五、旋转动画效果

Transform.rotate Widget

在 Flutter 中,Transform.rotate Widget 用于旋转其子部件(child widget)。通过设置 Transform.rotate 的 angle 属性为一个角度值,可以使子部件绕中心旋转指定的角度。

Dart 复制代码
import 'dart:math';

import 'package:flutter/material.dart';

///create by itz on 2024-10-24 14:19
///desc :
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  final double angle = pi / 10 * (2 * Random().nextDouble() - 1);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    // 旋转
    var childRotate = Transform.rotate(angle: angle, child: content);

    // 缩放
    var childScale = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: childRotate);

    // 透明度
    var childOpacity = Opacity(opacity: opacity, child: childScale);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: childOpacity);
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

六、渐变动画效果

ShaderMask Widget

在 Flutter 中,ShaderMask Widget 用于将子部件应用一个着色器效果,可以用来创建各种视觉效果,比如颜色遮罩、渐变遮罩等。

Dart 复制代码
class FavoriteAnimationIcon extends StatefulWidget {
  // 爱心显示位置坐标
  final Offset position;

  // 爱心大小
  final double size;

  // 动效完成,Icon消失回调
  final Function? onAnimationComplete;

  const FavoriteAnimationIcon({super.key, required this.position, required this.size, this.onAnimationComplete});

  @override
  State<FavoriteAnimationIcon> createState() => _FavoriteAnimationIconState();
}

class _FavoriteAnimationIconState extends State<FavoriteAnimationIcon> with TickerProviderStateMixin {
  // 展示的进度值为0.1
  static const double appearValue = 0.1;

  // 消失的进度值为0.8
  static const double dismissValue = 0.8;

  static const int _duration = 600;
  late AnimationController _animationController;

  final double angle = pi / 10 * (2 * Random().nextDouble() - 1);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: _duration),
      vsync: this,
    );

    // 监听动画进度变化,刷新Icon
    _animationController.addListener(() {
      setState(() {});
    });

    startAnimation();
  }

  @override
  void dispose() {
    super.dispose();
    // 释放动画资源
    _animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var content = Icon(Icons.favorite, size: widget.size, color: Colors.redAccent);

    // 缩放
    var childScale = Transform.scale(scale: scale, alignment: Alignment.bottomCenter, child: content);

    // 旋转
    var childRotate = Transform.rotate(angle: angle, child: childScale);

    // 渐变
    var childShaderMask = ShaderMask(
        blendMode: BlendMode.srcATop,
        shaderCallback: (Rect bounds) => RadialGradient(
                colors: const [Color(0xFFEE6E6E), Color(0xFFF03F3F)],
                center: Alignment.topLeft.add(const Alignment(0.66, 0.66)))
            .createShader(bounds),
        child: childRotate);

    // 透明度
    var childOpacity = Opacity(opacity: opacity, child: childShaderMask);

    return Positioned(
        top: widget.position.dy - widget.size / 2, left: widget.position.dx - widget.size / 2, child: childOpacity);
  }

  // 需要得到的结果是透明度的进度值的百分比
  double get opacity {
    if (value < appearValue) {
      // 处于渐进阶段,播放透明度动画
      return value / appearValue;
    }
    if (value < dismissValue) {
      // 处于展示阶段,不需要动画
      return 1;
    }
    // 处于渐隐阶段,播放器透明度动画
    return (1 - value) / (1 - dismissValue);
  }

  // 需要计算缩放尺寸的占比
  double get scale {
    if (value < appearValue) {
      // 处于出现阶段(从1.1到1 缩小的过程)
      return 1 + appearValue - value;
    }

    if (value < dismissValue) {
      // 处于正常展示阶段
      return 1;
    }

    // 处于消失放大阶段
    return 1 + (value - dismissValue) / (1 - dismissValue);
  }

  double get value {
    return _animationController.value;
  }

  Future<void> startAnimation() async {
    await _animationController.forward();
    widget.onAnimationComplete?.call();
  }
}

实现效果如下:

相关推荐
20岁30年经验的码农9 分钟前
爬虫基础
1024程序员节
licy__28 分钟前
计算机网络IP地址分类,子网掩码,子网划分复习资料
1024程序员节
Chris-zz1 小时前
Linux:磁盘深潜:探索文件系统、连接之道与库的奥秘
linux·网络·c++·1024程序员节
JasonYin~1 小时前
《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 —— 模块化基础篇》
1024程序员节
Teamol20202 小时前
求助帖:ubuntu22.10 auto install user-data配置了为何还需要选择语言键盘(如何全自动)
linux·ubuntu·1024程序员节
尘佑不尘2 小时前
shodan5,参数使用,批量查找Mongodb未授权登录,jenkins批量挖掘
数据库·笔记·mongodb·web安全·jenkins·1024程序员节
SeniorMao0073 小时前
结合Intel RealSense深度相机和OpenCV来实现语义SLAM系统
1024程序员节
网安_秋刀鱼3 小时前
CSRF防范及绕过
前端·安全·web安全·网络安全·csrf·1024程序员节
WW、forever3 小时前
【ArcGIS Pro实操第4期】绘制三维地图
1024程序员节