diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart index 01718bd2..36b1f979 100644 --- a/lib/pages/liveRoom/view.dart +++ b/lib/pages/liveRoom/view.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -19,6 +22,7 @@ class _LiveRoomPageState extends State { bool isShowCover = true; bool isPlay = true; + Floating? floating; @override void initState() { @@ -32,19 +36,24 @@ class _LiveRoomPageState extends State { } }, ); + if (Platform.isAndroid) { + floating = Floating(); + } } @override void dispose() { plPlayerController!.dispose(); + if (floating != null) { + floating!.dispose(); + } super.dispose(); } @override Widget build(BuildContext context) { final videoHeight = MediaQuery.of(context).size.width * 9 / 16; - - return Scaffold( + Widget childWhenDisabled = Scaffold( primary: true, appBar: AppBar( centerTitle: false, @@ -98,6 +107,7 @@ class _LiveRoomPageState extends State { bottomControl: BottomControl( controller: plPlayerController, liveRoomCtr: _liveRoomController, + floating: floating, ), ) : const SizedBox(), @@ -123,5 +133,25 @@ class _LiveRoomPageState extends State { ], ), ); + Widget childWhenEnabled = AspectRatio( + aspectRatio: 16 / 9, + child: plPlayerController!.videoPlayerController != null + ? PLVideoPlayer( + controller: plPlayerController!, + bottomControl: BottomControl( + controller: plPlayerController, + liveRoomCtr: _liveRoomController, + ), + ) + : const SizedBox(), + ); + if (Platform.isAndroid) { + return PiPSwitcher( + childWhenDisabled: childWhenDisabled, + childWhenEnabled: childWhenEnabled, + ); + } else { + return childWhenDisabled; + } } } diff --git a/lib/pages/liveRoom/widgets/bottom_control.dart b/lib/pages/liveRoom/widgets/bottom_control.dart index f538acad..7bc8f8ab 100644 --- a/lib/pages/liveRoom/widgets/bottom_control.dart +++ b/lib/pages/liveRoom/widgets/bottom_control.dart @@ -1,4 +1,8 @@ +import 'dart:io'; + +import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/video/play/url.dart'; @@ -9,9 +13,11 @@ import 'package:pilipala/utils/storage.dart'; class BottomControl extends StatefulWidget implements PreferredSizeWidget { final PlPlayerController? controller; final LiveRoomController? liveRoomCtr; + final Floating? floating; const BottomControl({ this.controller, this.liveRoomCtr, + this.floating, Key? key, }) : super(key: key); @@ -85,6 +91,35 @@ class _BottomControlState extends State { ), ), const SizedBox(width: 4), + if (Platform.isAndroid) ...[ + SizedBox( + width: 34, + height: 34, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + bool canUsePiP = false; + widget.controller!.hiddenControls(false); + try { + canUsePiP = await widget.floating!.isPipAvailable; + } on PlatformException catch (_) { + canUsePiP = false; + } + if (canUsePiP) { + await widget.floating!.enable(); + } else {} + }, + icon: const Icon( + Icons.picture_in_picture_outlined, + size: 18, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 4), + ], ComBtn( icon: const Icon( Icons.fullscreen, diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 1c71e578..14adea22 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:floating/floating.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; @@ -51,6 +54,7 @@ class _VideoDetailPageState extends State late Future _futureBuilderFuture; // 自动退出全屏 late bool autoExitFullcreen; + Floating? floating; @override void initState() { @@ -60,6 +64,9 @@ class _VideoDetailPageState extends State setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); videoSourceInit(); appbarStreamListen(); + if (Platform.isAndroid) { + floating = Floating(); + } } // 获取视频资源,初始化播放器 @@ -83,7 +90,7 @@ class _VideoDetailPageState extends State } // 播放器状态监听 - void playerListener(PlayerStatus? status) { + void playerListener(PlayerStatus? status) async { playerStatus = status!; if (status == PlayerStatus.completed) { // 结束播放退出全屏 @@ -91,7 +98,10 @@ class _VideoDetailPageState extends State plPlayerController!.triggerFullScreen(status: false); } // 播放完展示控制栏 - plPlayerController!.onLockControl(false); + PiPStatus currentStatus = await floating!.pipStatus; + if (currentStatus == PiPStatus.disabled) { + plPlayerController!.onLockControl(false); + } } } @@ -116,6 +126,9 @@ class _VideoDetailPageState extends State plPlayerController!.removeStatusLister(playerListener); plPlayerController!.dispose(); } + if (floating != null) { + floating!.dispose(); + } super.dispose(); } @@ -162,7 +175,7 @@ class _VideoDetailPageState extends State final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final double pinnedHeaderHeight = statusBarHeight + kToolbarHeight + videoHeight; - return SafeArea( + Widget childWhenDisabled = SafeArea( top: false, bottom: false, child: Stack( @@ -209,6 +222,7 @@ class _VideoDetailPageState extends State plPlayerController, videoDetailCtr: videoDetailController, + floating: floating, ), danmuWidget: Obx( () => PlDanmaku( @@ -320,13 +334,18 @@ class _VideoDetailPageState extends State ), ]; }, + // pinnedHeaderSliverHeightBuilder: () { + // return playerStatus != PlayerStatus.playing + // ? statusBarHeight + kToolbarHeight + // : pinnedHeaderHeight; + // }, + /// 不收回 pinnedHeaderSliverHeightBuilder: () { - return playerStatus != PlayerStatus.playing - ? statusBarHeight + kToolbarHeight - : pinnedHeaderHeight; + return pinnedHeaderHeight; }, onlyOneScrollInBody: true, body: Container( + key: Key(Get.arguments['heroTag']), color: Theme.of(context).colorScheme.background, child: Column( children: [ @@ -402,21 +421,60 @@ class _VideoDetailPageState extends State ), ), ), + + /// 重新进入会刷新 // 播放完成/暂停播放 - StreamBuilder( - stream: appbarStream.stream, - initialData: 0, - builder: ((context, snapshot) { - return ScrollAppBar( - snapshot.data!.toDouble(), - () => continuePlay(), - playerStatus, - null, - ); - }), - ) + // StreamBuilder( + // stream: appbarStream.stream, + // initialData: 0, + // builder: ((context, snapshot) { + // return ScrollAppBar( + // snapshot.data!.toDouble(), + // () => continuePlay(), + // playerStatus, + // null, + // ); + // }), + // ) ], ), ); + Widget childWhenEnabled = FutureBuilder( + key: Key(Get.arguments['heroTag']), + future: _futureBuilderFuture, + builder: ((context, snapshot) { + if (snapshot.hasData && snapshot.data['status']) { + return Obx( + () => !videoDetailController.autoPlay.value + ? const SizedBox() + : PLVideoPlayer( + controller: plPlayerController!, + headerControl: HeaderControl( + controller: plPlayerController, + videoDetailCtr: videoDetailController, + ), + danmuWidget: Obx( + () => PlDanmaku( + key: Key( + videoDetailController.danmakuCid.value.toString()), + cid: videoDetailController.danmakuCid.value, + playerController: plPlayerController!, + ), + ), + ), + ); + } else { + return const SizedBox(); + } + }), + ); + if (Platform.isAndroid) { + return PiPSwitcher( + childWhenDisabled: childWhenDisabled, + childWhenEnabled: childWhenEnabled, + ); + } else { + return childWhenDisabled; + } } } diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 95c3052d..1697b4d4 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -1,4 +1,8 @@ +import 'dart:io'; + +import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -14,9 +18,11 @@ import 'package:pilipala/utils/storage.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { final PlPlayerController? controller; final VideoDetailController? videoDetailCtr; + final Floating? floating; const HeaderControl({ this.controller, this.videoDetailCtr, + this.floating, Key? key, }) : super(key: key); @@ -770,6 +776,39 @@ class _HeaderControlState extends State { ), ), const SizedBox(width: 4), + if (Platform.isAndroid) ...[ + SizedBox( + width: 34, + height: 34, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + bool canUsePiP = false; + widget.controller!.hiddenControls(false); + try { + canUsePiP = await widget.floating!.isPipAvailable; + } on PlatformException catch (_) { + canUsePiP = false; + } + if (canUsePiP) { + final aspectRatio = Rational( + widget.videoDetailCtr!.data.dash!.video!.first.width!, + widget.videoDetailCtr!.data.dash!.video!.first.height!, + ); + await widget.floating!.enable(aspectRatio: aspectRatio); + } else {} + }, + icon: const Icon( + Icons.picture_in_picture_outlined, + size: 19, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 4), + ], Obx( () => SizedBox( width: 45, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 6645f621..c688bf4c 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -752,6 +752,10 @@ class PlPlayerController { } } + void hiddenControls(bool val) { + showControls.value = val; + } + /// 设置长按倍速状态 live模式下禁用 void setDoubleSpeedStatus(bool val) { if (videoType.value == 'live') { diff --git a/pubspec.lock b/pubspec.lock index 43fb389a..d30fa01b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,6 +417,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + floating: + dependency: "direct main" + description: + name: floating + sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991 + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7e642aab..e32da85c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -117,6 +117,8 @@ dependencies: status_bar_control: ^3.2.1 # 代理 system_proxy: ^0.1.0 + # pip + floating: ^2.0.1 dev_dependencies: flutter_test: