You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现类YouTube的跨平台Picture in Picture功能?

实现Flutter双模式画中画(应用内悬浮+系统全局PIP)

要同时实现应用内可拖拽悬浮窗和系统级全局画中画,你需要把这两个功能拆成独立模块来做——系统原生PIP本来就是为应用后台场景设计的,没法直接实现应用内悬浮,得自己用Flutter组件搭应用内悬浮层,再和系统PIP逻辑做切换整合。

一、应用内悬浮窗口实现

不用依赖第三方包,直接用Flutter原生的Stack+Draggable就能快速实现可拖拽的悬浮Widget,完全可控:

class InAppFloatingWidget extends StatefulWidget {
  final Widget content;
  final bool isActive;

  const InAppFloatingWidget({
    super.key,
    required this.content,
    required this.isActive,
  });

  @override
  State<InAppFloatingWidget> createState() => _InAppFloatingWidgetState();
}

class _InAppFloatingWidgetState extends State<InAppFloatingWidget> {
  Offset _position = const Offset(20, 20);
  final _windowSize = const Size(300, 180);

  @override
  Widget build(BuildContext context) {
    if (!widget.isActive) return widget.content;

    return Stack(
      fit: StackFit.expand,
      children: [
        // 点击背景关闭悬浮窗(可选)
        GestureDetector(
          onTap: () => setState(() => widget.isActive = false),
          child: const ColoredBox(color: Colors.black38),
        ),
        // 可拖拽悬浮窗
        Positioned(
          left: _position.dx,
          top: _position.dy,
          child: Draggable(
            feedback: _buildFloatingWindow(),
            childWhenDragging: const SizedBox.shrink(),
            onDragEnd: (details) {
              setState(() {
                _position = Offset(
                  details.offset.dx.clamp(0, MediaQuery.of(context).size.width - _windowSize.width),
                  details.offset.dy.clamp(0, MediaQuery.of(context).size.height - _windowSize.height),
                );
              });
            },
            child: _buildFloatingWindow(),
          ),
        ),
      ],
    );
  }

  Widget _buildFloatingWindow() {
    return Container(
      width: _windowSize.width,
      height: _windowSize.height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        color: Colors.black,
        boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 8)],
      ),
      clipBehavior: Clip.antiAlias,
      child: widget.content,
    );
  }
}

使用时,在你的页面里用一个布尔值控制isActive,比如点击视频右上角的"悬浮"按钮,切换这个状态就能显示/隐藏应用内悬浮窗。

二、系统全局PIP整合(安卓+iOS)

系统级PIP需要原生代码配合,和应用内悬浮完全独立,当用户需要把应用最小化保留悬浮时,触发系统PIP逻辑:

安卓端配置与实现

  1. 修改AndroidManifest.xml的主Activity标签,添加PIP支持:
<activity
    android:name=".MainActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
  1. 在MainActivity.kt里添加进入PIP的方法:
class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.your.app/pip"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "enterSystemPIP") {
                enterSystemPIP()
                result.success(null)
            } else {
                result.notImplemented()
            }
        }
    }

    private fun enterSystemPIP() {
        val aspectRatio = Rational(16, 9)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            .build()
        enterPictureInPictureMode(params)
        // 手动让应用退到后台(可选,部分系统会自动处理)
        moveTaskToBack(true)
    }
}

iOS端配置与实现

iOS的系统PIP要求必须关联AVPlayer(非视频场景可以用空的AVPlayer占位):

  1. 修改Info.plist添加权限:
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
<key>UIPictureInPictureEnabled</key>
<true/>
  1. 在AppDelegate.swift里添加PIP触发方法:
import AVKit

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
    private let CHANNEL = "com.your.app/pip"

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
        channel.setMethodCallHandler { [weak self] call, result in
            if call.method == "enterSystemPIP" {
                self?.enterSystemPIP()
                result.success(nil)
            } else {
                result.notImplemented()
            }
        }
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    private func enterSystemPIP() {
        guard let vc = window?.rootViewController else { return }
        let player = AVPlayer()
        let playerLayer = AVPlayerLayer(player: player)
        let contentSource = AVPlayerLayerPictureInPictureControllerContentSource(
            playerLayer: playerLayer,
            playbackDelegate: self
        )
        if let pipController = AVPictureInPictureController(contentSource: contentSource) {
            pipController.startPictureInPicture()
        }
        // 退到后台
        UIApplication.shared.perform(#selector(NSApplication.terminate(_:)))
    }
}

extension AppDelegate: AVPictureInPictureControllerDelegate {}

三、双模式切换逻辑

在Flutter层用状态管理统一控制两个模式的切换,避免冲突:

class PiPController extends ChangeNotifier {
  bool _isInAppFloating = false;

  bool get isInAppFloating => _isInAppFloating;

  void toggleInAppFloating() {
    _isInAppFloating = !_isInAppFloating;
    notifyListeners();
  }

  Future<void> switchToSystemPIP() async {
    // 先关闭应用内悬浮
    if (_isInAppFloating) {
      _isInAppFloating = false;
      notifyListeners();
    }
    // 调用原生方法进入系统PIP
    const channel = MethodChannel('com.your.app/pip');
    try {
      await channel.invokeMethod('enterSystemPIP');
    } catch (e) {
      debugPrint("进入系统PIP失败: $e");
    }
  }
}

在你的页面里,用Consumer监听状态,渲染对应的UI:

Consumer<PiPController>(
  builder: (context, controller, child) {
    return InAppFloatingWidget(
      isActive: controller.isInAppFloating,
      content: YourVideoPlayerWidget(), // 你的目标Widget(比如视频播放器)
    );
  },
)

关键注意事项

  • 应用内悬浮和系统PIP是完全独立的两个机制,不能用同一个第三方包同时实现,必须分开处理
  • 系统PIP只有在应用退到后台时才会显示全局悬浮,所以调用系统PIP后需要让应用进入后台
  • iOS的系统PIP严格绑定AVPlayer,如果你的内容不是视频,要么用AVPlayer占位,要么申请UIBackgroundModes的picture-in-picture权限(部分场景需要审核)

内容的提问来源于stack exchange,提问作者Hassun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:04:56