本文主要是使用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();
}
}
实现效果如下: