feat: 视频、直播pip Android端
This commit is contained in:
@ -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<LiveRoomPage> {
|
||||
|
||||
bool isShowCover = true;
|
||||
bool isPlay = true;
|
||||
Floating? floating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -32,19 +36,24 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
}
|
||||
},
|
||||
);
|
||||
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<LiveRoomPage> {
|
||||
bottomControl: BottomControl(
|
||||
controller: plPlayerController,
|
||||
liveRoomCtr: _liveRoomController,
|
||||
floating: floating,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
@ -123,5 +133,25 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<BottomControl> {
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
@ -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<VideoDetailPage>
|
||||
late Future _futureBuilderFuture;
|
||||
// 自动退出全屏
|
||||
late bool autoExitFullcreen;
|
||||
Floating? floating;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -60,6 +64,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
|
||||
videoSourceInit();
|
||||
appbarStreamListen();
|
||||
if (Platform.isAndroid) {
|
||||
floating = Floating();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取视频资源,初始化播放器
|
||||
@ -83,7 +90,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
void playerListener(PlayerStatus? status) {
|
||||
void playerListener(PlayerStatus? status) async {
|
||||
playerStatus = status!;
|
||||
if (status == PlayerStatus.completed) {
|
||||
// 结束播放退出全屏
|
||||
@ -91,7 +98,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
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<VideoDetailPage>
|
||||
plPlayerController!.removeStatusLister(playerListener);
|
||||
plPlayerController!.dispose();
|
||||
}
|
||||
if (floating != null) {
|
||||
floating!.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -162,7 +175,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
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<VideoDetailPage>
|
||||
plPlayerController,
|
||||
videoDetailCtr:
|
||||
videoDetailController,
|
||||
floating: floating,
|
||||
),
|
||||
danmuWidget: Obx(
|
||||
() => PlDanmaku(
|
||||
@ -320,13 +334,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
),
|
||||
];
|
||||
},
|
||||
// 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<VideoDetailPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// 重新进入会刷新
|
||||
// 播放完成/暂停播放
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<HeaderControl> {
|
||||
),
|
||||
),
|
||||
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,
|
||||
|
@ -752,6 +752,10 @@ class PlPlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
void hiddenControls(bool val) {
|
||||
showControls.value = val;
|
||||
}
|
||||
|
||||
/// 设置长按倍速状态 live模式下禁用
|
||||
void setDoubleSpeedStatus(bool val) {
|
||||
if (videoType.value == 'live') {
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user