在 Flutter Android 应用中实现画中画功能
画中画(Picture-in-Picture, PiP)模式允许您的应用在一个固定在屏幕角落的小窗口中运行,同时用户可以与其他应用进行交互。本指南将介绍如何在 Flutter Android 应用中实现画中画功能,包括其局限性和解决方案。
项目地址
前提条件
-
需要 Android 8.0 (API level 26) 或更高版本才能完全支持画中画功能
-
基本的 Flutter 插件开发知识
-
基本的 Android 开发知识
实现概述
实现包含两个主要组件:
-
FlutterPipController
: 处理画中画功能和状态管理 -
FlutterPipPlugin
: 桥接 Flutter 和原生 Android 代码
主要特性
-
画中画模式支持检测
-
自定义宽高比配置
-
平滑过渡的源矩形提示
-
画中画状态监控和回调
-
非视频内容的交叉淡入淡出动画
核心实现
1. 检查画中画支持
在使用画中画之前,我们需要检查设备是否支持:
java
public boolean isSupported() {
Activity activity = mActivity.get();
if (activity == null) {
return false;
}
// Requires Android 8.0 (API 26) or higher
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
final PackageManager pm = activity.getApplicationContext().getPackageManager();
return pm != null && pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
2. 配置画中画参数
画中画模式可以自定义几个参数:
java
public boolean setup(@Nullable Rational aspectRatio,
@Nullable Boolean autoEnterEnabled,
@Nullable Rect sourceRectHint) {
// ... version checks and null checks ...
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
if (aspectRatio != null) {
builder.setAspectRatio(aspectRatio);
}
if (sourceRectHint != null) {
builder.setSourceRectHint(sourceRectHint);
}
// Disable seamless resize for non-video content
builder.setSeamlessResizeEnabled(false);
activity.setPictureInPictureParams(builder.build());
}
Flutter 集成限制和解决方案
1. 自动进入画中画模式限制
Flutter 不正确地委托 Android 生命周期事件,如 onPause
和 onPiPModeChanged
。这给实现自动进入画中画模式带来了挑战。
限制:
java
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
public boolean isAutoEnterSupported() {
// We could support this on Android 12+, but Flutter limitations prevent it
return false;
}
解决方案:
我们不依赖自动进入画中画模式,而是提供显式的方法来进入和退出画中画模式,这些方法可以从 Flutter 代码中调用:
java
public boolean start() {
if (!isSupported() || isActived() || !isPipEnabled()) {
return false;
}
Activity activity = mActivity.get();
if (activity == null) {
return false;
}
activity.enterPictureInPictureMode(mParamsBuilder.build());
return true;
}
2. 画中画状态变化检测
由于 Flutter 不提供画中画状态变化事件,我们实现了一个轮询机制来检测状态变化。
解决方案:
java
private void startStateMonitoring() {
// Poll every 100ms to check PiP state
mCheckStateTask = new Runnable() {
@Override
public void run() {
checkPipState();
mHandler.postDelayed(this, CHECK_INTERVAL_MS);
}
};
mHandler.post(mCheckStateTask);
}
3. 画中画退出处理
Android 不提供直接退出画中画模式的方法。
解决方案:
java
public void stop() {
if (!isSupported() || !isActived()) {
return;
}
Activity activity = mActivity.get();
if (activity == null) {
return;
}
// Move the activity to background instead of truly stopping PiP
activity.moveTaskToBack(false);
}
最佳实践
- 资源管理: 始终正确释放资源:
java
public void dispose() {
stopStateMonitoring();
mPipParams = null;
mParamsBuilder = null;
mHandler = null;
mLastPipState = false;
mCheckStateTask = null;
}
- 状态监控: 跟踪画中画状态变化并通知 Flutter:
java
private void checkPipState() {
boolean currentState = isActived();
if (currentState != mLastPipState) {
mLastPipState = currentState;
notifyPipStateChanged(currentState ? PipState.Started : PipState.Stopped);
}
}
- 交叉淡入淡出动画: 对于非视频内容,禁用无缝调整大小:
java
mParamsBuilder.setSeamlessResizeEnabled(false);
结论
虽然在 Flutter Android 应用中实现画中画功能受到 Flutter 处理 Android 生命周期事件的一些限制,但我们可以通过轮询状态检测和显式控制方法来解决这些问题。这里提供的解决方案提供了一个可靠且稳定的实现,同时保持良好的用户体验。
请记住要在不同的 Android 版本和设备配置上进行全面测试,因为画中画行为在不同的 Android 实现中可能会有所不同。
参考
PS
这个项目会持续维护下去,而且已经在准备发布pub.dev, 目前上面的文档是AI帮助生成的,有些不太准确和完善,但基本路线是对的,后续会持续补充完善。