【开源鸿蒙跨平台开发训练营】Flutter框架鸿蒙应用的全屏双击收藏动画效果优化
,},// ... 顶部栏、底部栏 ...),),
·
发现之前设置的详情页全屏双击收藏动效还有待优化。图片没加载时双加能看到收藏效果,加载完成图片后双击就不显示了,这次针对这个问题进行优化下。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
文章目录
详情页双击收藏动效优化
这次需要完善的是作品详情页「双击屏幕收藏」的动效优化:解决图片加载后动效被挡住的问题,并改为气泡变大 + 心形弹出的视觉效果。

一、目标与方案
- 目标:
- 修复:图片加载完成后,双击收藏的动效被图片挡住、看不见。
- 优化:将原心形放大淡出改为「气泡从中心扩散变大 + 心形弹出」的动效。
- 方案:
- 将动效从整页 Stack 挪到 当前页内部:在 PageView 的每页用
Stack包裹「图片 + 动效层」,动效层与图片同属一页且在上层,保证始终可见。 - 新增私有组件
_FavoriteAnimationOverlay,用TweenAnimationBuilder实现:白色气泡圆从中心放大并变透明,心形在前段弹性放大、后段淡出。
- 将动效从整页 Stack 挪到 当前页内部:在 PageView 的每页用
二、涉及文件
| 文件 | 说明 |
|---|---|
lib/pages/work_detail_page.dart |
详情页:PageView 每页内 Stack 结构、双击触发逻辑、_FavoriteAnimationOverlay 组件 |
无新增依赖,仅修改详情页。
三、问题原因与处理:动效必须在最顶层
原先动效放在 PageView 内部每页的 Stack 中(与图片同栈),在部分设备/图片加载完成后,图片的绘制层仍会盖住动效。
处理方式:将动效放在整页 Stack 的最后一个子节点,即绘制顺序为:
- PageView(图片)
- 顶部栏
- 底部栏
- 双击收藏动效层(
if (_showHeartOverlay)的Positioned.fill)
这样动效层始终在最上层,不会被图片或其它层挡住。动效层使用 IgnorePointer,点击会穿透到底下内容。
四、实现要点
4.1 动效层在整页 Stack 最顶层
- 整页
Stack的 children 顺序:PageView → 顶部栏 → 底部栏 → 动效层(最后一项)。 - 动效层仅在
_showHeartOverlay为 true 时插入,使用Positioned.fill铺满 body,保证绘制在所有内容之上,不被图片或其它层挡住。 _FavoriteAnimationOverlay外包一层IgnorePointer,不拦截点击,手势可穿透到底下。
4.2 双击与单次动画触发
- 手势:
GestureDetector同时使用onTap(用于模拟双击间隔)与onDoubleTap。 - 单击通过
_onImageTap配合_lastTapTime、_doubleTapInterval(400ms)判断是否为「两次单击视为双击」,触发_triggerFavoriteAndAnimation;onDoubleTap也直接调用_triggerFavoriteAndAnimation。 _triggerFavoriteAndAnimation:调用_favoritesService.toggleFavorite,在addPostFrameCallback里setState更新_isFavorite、_heartAnimationKey++、_showHeartOverlay = true,并在_favoriteAnimationDuration(800ms)后置_showHeartOverlay = false。
4.3 气泡变大动效
- 动画时长:
_favoriteAnimationDuration = 800ms,与 overlay 隐藏延迟一致。 - 气泡:白色圆从 scale 0 放大到约 2.2 倍(
Curves.easeOut),透明度从约 0.45 线性降为 0,形成从中心扩散变大的效果。 - 心形:前约 35% 时间用
Curves.elasticOut从 0 放大到约 1.15 倍;之后保持并随整体时间线淡出(约 25% 时间淡入,剩余时间淡出)。
五、关键代码
5.1 状态与常量(_WorkDetailPageState)
int _heartAnimationKey = 0;
bool _showHeartOverlay = false;
DateTime? _lastTapTime;
static const _doubleTapInterval = Duration(milliseconds: 400);
static const _favoriteAnimationDuration = Duration(milliseconds: 800);
5.2 双击触发与动画控制
void _triggerFavoriteAndAnimation() async {
await _favoritesService.toggleFavorite(widget.work);
if (!mounted) return;
final nowFavorite = await _favoritesService.isFavoriteWork(widget.work);
if (!mounted) return;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
_isFavorite = nowFavorite;
_heartAnimationKey++;
_showHeartOverlay = true;
});
Future.delayed(_favoriteAnimationDuration, () {
if (mounted) setState(() => _showHeartOverlay = false);
});
});
}
void _onImageTap() {
final now = DateTime.now();
final isDoubleTap = _lastTapTime != null &&
now.difference(_lastTapTime!) <= _doubleTapInterval;
_lastTapTime = now;
if (isDoubleTap) {
_triggerFavoriteAndAnimation();
}
}
5.3 PageView 每页仅图片;动效在整页 Stack 最后
itemBuilder: (context, index) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _onImageTap,
onDoubleTap: _triggerFavoriteAndAnimation,
child: NetworkImageWidget(
imageUrl: images[index],
fit: BoxFit.cover,
),
);
},
// ... 顶部栏、底部栏 ...
// 双击收藏动效:放在 Stack 最顶层,确保不被图片或其它层挡住
if (_showHeartOverlay)
Positioned.fill(
child: _FavoriteAnimationOverlay(
key: ValueKey<int>(_heartAnimationKey),
duration: _favoriteAnimationDuration,
),
),
5.4 气泡 + 心形动效组件(_FavoriteAnimationOverlay)
/// 双击收藏时的气泡变大 + 心形动效,叠在图片之上
class _FavoriteAnimationOverlay extends StatelessWidget {
final Duration duration;
const _FavoriteAnimationOverlay({
super.key,
required this.duration,
});
Widget build(BuildContext context) {
return IgnorePointer(
child: TweenAnimationBuilder<double>(
duration: duration,
tween: Tween<double>(begin: 0, end: 1),
curve: Curves.easeOut,
builder: (context, t, _) {
final bubbleScale = Curves.easeOut.transform(t) * 2.2;
final bubbleOpacity = (1.0 - t) * 0.45;
final heartScale = t < 0.35
? Curves.elasticOut.transform(t / 0.35) * 1.15
: 1.15 * (1.0 - (t - 0.35) / 0.65);
final heartOpacity = t < 0.25 ? t / 0.25 : (1.0 - (t - 0.25) / 0.75).clamp(0.0, 1.0);
return Center(
child: SizedBox(
width: 200,
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
Transform.scale(
scale: bubbleScale,
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: bubbleOpacity),
),
),
),
Opacity(
opacity: heartOpacity,
child: Transform.scale(
scale: heartScale,
child: appHeartIcon(
size: 80,
color: Colors.white,
interactive: false,
),
),
),
],
),
),
);
},
),
);
}
}
IgnorePointer:动效层不拦截点击。ValueKey(_heartAnimationKey):每次双击重建 overlay,动画从 0 重新播放。
六、使用方式与检查
- 使用:在作品详情页全屏图片上双击,会切换收藏状态并在屏幕中央播放一次「气泡扩散 + 心形弹出」的 800ms 动效;图片加载前后动效均可见。
- 检查:
- 动效层为整页 Stack 的最后一个子节点,绘制在最上层,不被图片或其它层挡住。
- 气泡从中心放大并变透明,心形弹性放大后淡出。
- 单击不触发收藏,双击或 400ms 内两次单击触发收藏与动效。
结束语
感谢阅读本帖,如对贴中内容有意见和建议的,欢迎与我联系交流,也欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)