前言
在 Yam TV 的 Flutter 端中,用户希望在播放视频时能够切换到画中画模式——视频以悬浮小窗继续播放,同时可以浏览其他内容。本文分享 Android PiP 在 Flutter 中的完整实现方案。
问题背景
画中画(Picture-in-Picture,PiP)是 Android 8.0+(API 26)提供的系统级功能,允许 Activity 以悬浮小窗形式继续运行。在 Flutter 中实现 PiP 需要解决几个关键问题:
1. **仅显示视频区域**:Android PiP 捕获的是整个 Activity,而非某个 Widget。如果不做处理,AppBar、按钮等 UI 元素也会出现在小窗中
2. **双端通信**:进入/退出 PiP 时,Flutter 需要响应状态变化来隐藏/恢复 UI
3. **视频播放不中断**:进入 PiP 后视频需继续播放,退出后无缝恢复
4. **多页面适配**:普通播放页和短视频页都需要支持
实现方案
1. Android 端配置
首先在 AndroidManifest.xml 中声明 PiP 支持:
<activity
android:supportsPictureInPicture="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|...">
</activity>
2. MethodChannel:Flutter ↔ Android 通信
建立 yamtv/pip 通道,Flutter 通过它调用 Android PiP API,Android 通过它回调 PiP 状态变化:
// MainActivity.kt
pipChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "yamtv/pip").apply {
setMethodCallHandler { call, result ->
when (call.method) {
"isAvailable" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE))
}
"enter" -> {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
enterPictureInPictureMode(params)
result.success(true)
}
}
}
}
// 监听 PiP 模式变化
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pipChannel?.invokeMethod("onPipModeChanged", isInPictureInPictureMode)
}
3. Flutter 端封装
Dart 端封装为工具类,自动初始化通道回调:
class PictureInPicture {
static PipModeCallback? _onPipModeChanged;
static void setOnPipModeChanged(PipModeCallback? callback) {
_ensureInitialized();
_onPipModeChanged = callback;
}
static void _ensureInitialized() {
if (_initialized) return;
_channel.setMethodCallHandler((call) async {
if (call.method == 'onPipModeChanged' && call.arguments is bool) {
_onPipModeChanged?.call(call.arguments as bool);
}
});
}
static Future<void> enter() async {
_ensureInitialized();
await _channel.invokeMethod('enter');
}
}
4. UI 适配:隐藏非视频元素
PiP 进入时最核心的问题是:Android 捕获的是整个 Activity 的内容。必须让 Flutter 页面「只剩播放器」。
在 initState 中注册 PiP 回调:
PictureInPicture.setOnPipModeChanged((isPip) {
if (!mounted) return;
setState(() => _pipActive = isPip);
});
在 build 方法中根据 _pipActive 控制 UI:
appBar: _pipActive ? null : glassAppBar(context, title: Text(title)),
对于短视频页,还需要将底部信息区推出屏幕:
final infoLift = active && chromeVisible
? kYamEmbeddedPlayerBottomChromeHeight
: pipActive ? -200.0 : 0.0;
5. 播放器工具栏按钮
在 YamVideoPlayer 的底部控制栏添加 PiP 按钮,用户可直接在播放器中触发 PiP:
IconButton(
tooltip: '画中画',
icon: const Icon(Icons.picture_in_picture_alt, color: Colors.white),
onPressed: () {
unawaited(PictureInPicture.enter());
},
),
6. 退出 PiP 时恢复 UI
退出 PiP 时,Android 回调 onPictureInPictureModeChanged 的 isInPictureInPictureMode = false,Flutter 端收到回调后将 _pipActive 设回 false,所有隐藏的 UI 自动恢复。
对于短视频页,还需要记住 PiP 前的 chrome 状态:
if (isPip) {
_chromeBeforePip = _chromeVisible;
_chromeVisible = false;
} else {
_chromeVisible = _chromeBeforePip;
}
总结
Android PiP 在 Flutter 中的实现要点:
1. **PiP 捕获的是整个 Activity**,必须主动隐藏非视频元素
2. 通过 MethodChannel 实现 Flutter ↔ Android 的双向状态同步
3. PictureInPictureParams.setAspectRatio 控制小窗比例
4. 短视频和普通播放页的适配逻辑相似,核心都是 _pipActive 状态驱动 UI 显隐
这套方案在 Yam TV 中已落地,支持普通播放和短视频两种场景的 PiP 切换。
讨论
还没有留言,来留下第一条评论吧!