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 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter