发现之前设置的详情页全屏双击收藏动效还有待优化。图片没加载时双加能看到收藏效果,加载完成图片后双击就不显示了,这次针对这个问题进行优化下。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

详情页双击收藏动效优化

这次需要完善的是作品详情页「双击屏幕收藏」的动效优化:解决图片加载后动效被挡住的问题,并改为气泡变大 + 心形弹出的视觉效果。


在这里插入图片描述

一、目标与方案

  • 目标
    1. 修复:图片加载完成后,双击收藏的动效被图片挡住、看不见。
    2. 优化:将原心形放大淡出改为「气泡从中心扩散变大 + 心形弹出」的动效。
  • 方案
    • 将动效从整页 Stack 挪到 当前页内部:在 PageView 的每页用 Stack 包裹「图片 + 动效层」,动效层与图片同属一页且在上层,保证始终可见。
    • 新增私有组件 _FavoriteAnimationOverlay,用 TweenAnimationBuilder 实现:白色气泡圆从中心放大并变透明,心形在前段弹性放大、后段淡出。

二、涉及文件

文件 说明
lib/pages/work_detail_page.dart 详情页:PageView 每页内 Stack 结构、双击触发逻辑、_FavoriteAnimationOverlay 组件

无新增依赖,仅修改详情页。


三、问题原因与处理:动效必须在最顶层

原先动效放在 PageView 内部每页的 Stack 中(与图片同栈),在部分设备/图片加载完成后,图片的绘制层仍会盖住动效。
处理方式:将动效放在整页 Stack 的最后一个子节点,即绘制顺序为:

  1. PageView(图片)
  2. 顶部栏
  3. 底部栏
  4. 双击收藏动效层if (_showHeartOverlay)Positioned.fill

这样动效层始终在最上层,不会被图片或其它层挡住。动效层使用 IgnorePointer,点击会穿透到底下内容。


四、实现要点

4.1 动效层在整页 Stack 最顶层

  • 整页 Stackchildren 顺序:PageView → 顶部栏 → 底部栏 → 动效层(最后一项)。
  • 动效层仅在 _showHeartOverlay 为 true 时插入,使用 Positioned.fill 铺满 body,保证绘制在所有内容之上,不被图片或其它层挡住。
  • _FavoriteAnimationOverlay 外包一层 IgnorePointer,不拦截点击,手势可穿透到底下。

4.2 双击与单次动画触发

  • 手势:GestureDetector 同时使用 onTap(用于模拟双击间隔)与 onDoubleTap
  • 单击通过 _onImageTap 配合 _lastTapTime_doubleTapInterval(400ms)判断是否为「两次单击视为双击」,触发 _triggerFavoriteAndAnimationonDoubleTap 也直接调用 _triggerFavoriteAndAnimation
  • _triggerFavoriteAndAnimation:调用 _favoritesService.toggleFavorite,在 addPostFrameCallbacksetState 更新 _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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐