这篇文章解决什么问题
目标不是"讲原理",而是给你一套以后能直接复用的方案,用来快速实现:
- Flutter 页面点击触发原生能力。
- iOS 启动隐藏视频播放器并进入 PiP。
- PiP 启动后主动退到桌面。
- App 回前台时,立即关闭 PiP,而不是只暂停。
这套方案适合沉淀成团队模板,也适合直接喂给 AI,让它一次产出高质量代码。
适用场景
- 引导用户添加小组件的教程悬浮窗。
- 退到桌面后继续播放短视频教学。
- 仅 iOS 需要
最小实现蓝图
调用链路固定为:
Flutter 点击事件
-> NativeWidgetUtil
-> Pigeon HostApi
-> iOS PiP Manager(AVPlayer + AVPictureInPictureController)
-> suspend 退桌面
建议文件分层:
pigeon/native_widget_service.dart定义接口。lib/pigeon/native_widget_service.dart/native_widget_util.dartDart 调用封装。ios/Runner/pigeon/NativeWidgetAPI.swift放完整 PiP 管理器。- Android/OHOS 侧实现 no-op。
关键实现步骤
1) Pigeon 新增统一入口
dart
@HostApi()
abstract class NativeWidgetService {
void startWidgetTutorialPipAndExitDebug(String assetPath, bool loop, bool muted);
}
执行生成(按你项目脚本):
./pigeon.sh
2) Flutter 页面入口
页面标题点击触发:
dart
void onTapTitle() {
if (!GetPlatform.isIOS) {
MyToastUtil.showToast('该测试入口仅 iOS 可用');
return;
}
unawaited(_startWidgetTutorialPipAndExitDebug());
}
Future<void> _startWidgetTutorialPipAndExitDebug() async {
try {
await NativeWidgetUtil.startWidgetTutorialPipAndExitDebug(
assetPath: 'assets/audios/widget_add_tutorial.mp4',
loop: true,
muted: false,
);
} catch (e) {
printLog('启动小组件教程画中画失败: $e');
MyToastUtil.showToast('画中画启动失败,请稍后重试');
}
}
3) iOS PiP Manager 必做项
一个类统一管理以下对象,避免散点逻辑:
AVPlayerAVPlayerLayerAVPictureInPictureController- 前台通知监听
- cleanup 生命周期
启动流程关键点:
- 校验
AVPictureInPictureController.isPictureInPictureSupported()。 - 通过
FlutterDartProject.lookupKey(forAsset:)找到 MP4。 - 用隐藏容器挂
AVPlayerLayer。 startPictureInPicture(),并处理首帧isPictureInPicturePossible为 false 的延迟兜底。
4) "边退边出"时序
在 delegate 的 willStartPictureInPicture 就触发:
swift
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
在 didStartPictureInPicture 再兜底一次,外加防重复标记 hasRequestedSuspend。
这样用户体感最好,不会出现"PiP 出来后等一会才退桌面"。
5) 前台关闭 PiP 的稳定方案(核心)
这部分决定成败。推荐固定策略:
- 同时监听
willEnterForeground和didBecomeActive。 - 前台阶段持续调用
stopPictureInPicture(),不要只pause()。 - 做短间隔重试(例如 0.12s * 12 次)。
- 多次 stop 仍失败时,断开
playerLayer.player = nil后再 stop。 - 只在
didStop回调或确认isPictureInPictureActive == false后 cleanup。
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler 返回 true,可减少灰底等待态。
6) 必加日志(排障效率翻倍)
建议统一前缀,例如 小组件教程画中画,打印:
- 启动成功/失败。
active / possible / retry。willStart / didStart / willStop / didStop。- 前台兜底断链是否触发。
你会快速判断是:
- 系统没收到 stop。
- 回调没回来。
- cleanup 过早导致状态机断裂。
7) AI 一次生成高质量代码的提示词模板
把下面这段直接给 AI,成功率会高很多:
text
请在 Flutter 项目中实现"教程视频 PiP + 退桌面"能力,要求:
1) 页面入口:历史页标题点击触发;
2) Flutter 仅调用 NativeWidgetUtil,不在页面内渲染视频;
3) 用 Pigeon 新增 HostApi:startWidgetTutorialPipAndExitDebug(String assetPath, bool loop, bool muted);
4) iOS 原生在 NativeWidgetAPI.swift 内新增独立管理器,使用 AVPlayer + AVPictureInPictureController;
5) 启动时序:willStartPictureInPicture 即触发 suspend,didStart 再兜底;
6) 回前台必须关闭 PiP,不是仅暂停:
- 监听 willEnterForeground + didBecomeActive;
- stopPictureInPicture 重试;
- 必要时断开 playerLayer.player 再 stop;
- 仅 didStop 或 inactive 后 cleanup;
7) Android/OHOS 实现 no-op;
8) 代码要包含中文注释,抽常量,提升可维护性;
9) 输出修改文件清单、关键 diff、验收步骤。
8) 验收清单(复制即测)
- iOS 真机点击入口后,出现 PiP 并退桌面。
- 回到前台时,PiP 在短时间内关闭,不只暂停。
- 不支持 PiP 的机型不崩溃,有错误提示。
- Android/OHOS 不受影响。
flutter analyze与 iOS 构建通过。
按这个模板实现这个功能,下次你或 AI 都能少走很多弯路。
感谢您的阅读和参与,HH思无邪愿与您一起在技术的道路上不断探索。如果您喜欢这篇文章,不妨留下您宝贵的赞!如果您对文章有任何疑问或建议,欢迎在评论区留言,我会第一时间处理,您的支持是我前行的动力,愿我们都能成为更好的自己!