初始化
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
// ignore_for_file: use_build_context_synchronously, file_names
|
||||
|
||||
import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/video/video_logic.dart';
|
||||
import 'package:hgdj/hj_page/video/view/video_ad_widget.dart';
|
||||
import 'package:hgdj/hj_page/video/view/video_mask_buy_view.dart';
|
||||
import 'package:hgdj/hj_page/video/view/video_menu_view.dart';
|
||||
import 'package:hgdj/hj_page/video/view/video_status_view.dart';
|
||||
import 'package:hgdj/hj_page/video/view/video_tabbar_menu_widget.dart';
|
||||
import 'package:hgdj/hj_page/video/view/vip_promo_banner.dart';
|
||||
import 'package:hgdj/hj_utils/codec_support.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../hj_model/video_model.dart';
|
||||
import '../../tools_base/widget/net_image_widget.dart';
|
||||
import '../comment/comment_views.dart';
|
||||
import 'cartoon/video_detail_cartoon_view.dart';
|
||||
import 'view/video_detail_view.dart';
|
||||
|
||||
class VideoPage extends StatefulWidget {
|
||||
final bool isCartoon;
|
||||
final VideoModel? model;
|
||||
final String? id;
|
||||
final String? videoCover;
|
||||
|
||||
VideoPage({
|
||||
super.key,
|
||||
this.model,
|
||||
this.isCartoon = false,
|
||||
this.id,
|
||||
this.videoCover,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoPage> createState() => _VideoPageState();
|
||||
}
|
||||
|
||||
class _VideoPageState extends State<VideoPage> with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<VideoLogic>(
|
||||
init: VideoLogic(
|
||||
vid: widget.id ?? widget.model?.id,
|
||||
isCartoon: widget.isCartoon,
|
||||
),
|
||||
tag: uniqueTag,
|
||||
builder: (logic) => PopScope(
|
||||
// 场景3:长视频返回挽留。iOS 侧滑在 canPop=false 时会被整体禁用且不回调,没法用来弹窗,
|
||||
// 故 iOS 恒可退、仅靠返回箭头(onBackPressed)弹挽留;Android 用 canPop 拦硬件返回键。
|
||||
canPop: GetPlatform.isIOS || logic.canPop,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
logic.showBackGuide();
|
||||
},
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: logic.normalRatio,
|
||||
child: _buildPlayContent(logic),
|
||||
),
|
||||
if (logic.videoModel != null) ...[
|
||||
VipPromoBanner(
|
||||
playCtr: logic.playerCtr,
|
||||
videoModel: logic.videoModel,
|
||||
),
|
||||
VideoTabbarMenuWidget(
|
||||
logic.tabCtr,
|
||||
model: logic.videoModel,
|
||||
onSwitchLine: logic.onSwitchLine,
|
||||
),
|
||||
Expanded(child: _buildTabDetailInfo(logic)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 底部 TabBarView:第 0 页详情/推荐,第 1 页评论(与 VideoTabbarMenuWidget 共用 logic.tabCtr)
|
||||
/// 注:选哪个详情视图、切视频走哪条接口,都以当前数据的 videoType 为准,别用页面入参 widget.isCartoon
|
||||
Widget _buildTabDetailInfo(VideoLogic logic) {
|
||||
final model = logic.videoModel;
|
||||
final isCartoonVideo = model?.videoType == 1;
|
||||
return TabBarView(
|
||||
controller: logic.tabCtr,
|
||||
children: [
|
||||
if (isCartoonVideo)
|
||||
VideoDetailCartoonView(
|
||||
model: model,
|
||||
currentEpisode: logic.currentEpisode,
|
||||
playCtr: logic.playerCtr,
|
||||
vmCallback: logic.switchVideo,
|
||||
episoCallback: logic.switchEpisode,
|
||||
).keepAlive
|
||||
else
|
||||
VideoDetailView(
|
||||
model: model,
|
||||
playCtr: logic.playerCtr,
|
||||
vmCallback: logic.switchVideo,
|
||||
).keepAlive,
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: CommentView(
|
||||
// key 跟着 id 走:页内切内容后强制重建,否则 GetBuilder 只在 initState 注册一次 controller,
|
||||
// objId 变了评论区还是上一部的(推荐区靠 ValueKey('rec_$id') 解决的是同一个问题)
|
||||
key: ValueKey('cmt_${model?.id}'),
|
||||
model?.id ?? '',
|
||||
objType: isCartoonVideo ? "cartoon" : "video",
|
||||
),
|
||||
).keepAlive,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放器外层:监听 videoModel.id 变化,切换视频时触发 fade + scale 动画
|
||||
Widget _buildPlayContent(VideoLogic logic) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
transitionBuilder: (child, anim) => FadeTransition(
|
||||
opacity: anim,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.9, end: 1.0).animate(anim),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
// KeyedSubtree 的 key 由 videoId 决定:id 变化 → AnimatedSwitcher 触发动画
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey(logic.videoModel?.id ?? 'empty'),
|
||||
child: _buildPlayBody(logic),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 播放器主体(视频画面 + 控制层 + 蒙层 + 贴片广告 + 返回按钮)
|
||||
Widget _buildPlayBody(VideoLogic logic) {
|
||||
return Container(
|
||||
color: AppColors.primaryColor,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _togglePlayPause(logic),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: AspectRatio(
|
||||
aspectRatio: logic.videoRatio,
|
||||
child: logic.playerCtr != null
|
||||
? VideoPlayer(logic.playerCtr!)
|
||||
: SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (logic.playerCtr?.value.isInitialized != true)
|
||||
NetworkImageLoader(
|
||||
imageUrl: logic.videoModel?.cover ?? widget.videoCover ?? "",
|
||||
borderRadius: 0),
|
||||
if (logic.playerCtr?.value.isInitialized == true) ...[
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 16,
|
||||
child: VipFreeTipView(
|
||||
key: ValueKey("status_top_${logic.videoModel?.id}"),
|
||||
videoModel: logic.videoModel,
|
||||
),
|
||||
),
|
||||
VideoMenuView(
|
||||
key: ValueKey("VideoMenuView_${logic.videoModel?.id}"),
|
||||
playCtr: logic.playerCtr!,
|
||||
videoModel: logic.videoModel,
|
||||
onBuyEvent: logic.showBuyMask,
|
||||
onFullScreen: logic.toFullPage,
|
||||
),
|
||||
],
|
||||
Positioned.fill(child: _bufferLoading(logic)),
|
||||
Positioned.fill(
|
||||
child: Obx(
|
||||
() => Visibility(
|
||||
visible: logic.isShowBuy.value,
|
||||
child: VideoMaskBuyView(
|
||||
logic.videoModel,
|
||||
playCtr: logic.playerCtr,
|
||||
// 购买成功要重建播放器换成正片,否则画面停在预览片(要退出重进才正常)
|
||||
onBuySucc: () => logic.onBuySuccess(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (logic.isShowAd && logic.adsList.isNotEmpty)
|
||||
VideoAdWidget(
|
||||
adsInfos: logic.adsList,
|
||||
showBackArrow: false,
|
||||
onFinish: () {
|
||||
logic.setAdShowing(false);
|
||||
logic.update();
|
||||
logic.play();
|
||||
},
|
||||
onToVip: () async {
|
||||
logic.clickAdToVip();
|
||||
},
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.onBackPressed(),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.centerLeft,
|
||||
margin: EdgeInsets.only(top: 10, left: 18.w),
|
||||
child: Image.asset(
|
||||
"back_circle.png".commonImgPath,
|
||||
width: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (kDebugMode) _buildDebugCodecTag(logic),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 缓冲中的居中转圈。直接监听 playCtr(本身是 ValueNotifier),
|
||||
/// 切视频/切线路换了控制器时 ValueListenableBuilder 会自动改订阅,无需手动 add/removeListener
|
||||
Widget _bufferLoading(VideoLogic logic) {
|
||||
final ctr = logic.playerCtr;
|
||||
if (ctr == null) return const SizedBox();
|
||||
return ValueListenableBuilder<VideoPlayerValue>(
|
||||
valueListenable: ctr,
|
||||
builder: (_, value, __) => value.isInitialized && value.isBuffering
|
||||
? const Center(
|
||||
child: CupertinoActivityIndicator(
|
||||
color: AppColors.actionRed, radius: 15))
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 【仅 debug】左下角编码排查标签:显示当前走 H265/H264,点击复制播放链接
|
||||
Widget _buildDebugCodecTag(VideoLogic logic) {
|
||||
final isH265 = logic.isPlayingH265;
|
||||
final deviceSupport = CodecSupport.useH265; // 设备硬解 265 探测结果
|
||||
return Positioned(
|
||||
left: 10,
|
||||
bottom: 35,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final url = logic.videoUrl ?? '';
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
showToast('已复制播放链接');
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'${isH265 ? "H265" : "H264"} 设备265:${deviceSupport ? "✓" : "✗"}',
|
||||
style: TextStyle(
|
||||
color: isH265 ? const Color(0xff4CAF50) : const Color(0xffFFC107),
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 点击播放器:播放中则暂停,否则播放(ctr 为 null 时不操作)
|
||||
void _togglePlayPause(VideoLogic logic) {
|
||||
final ctr = logic.playerCtr;
|
||||
if (ctr == null) return;
|
||||
if (ctr.value.isPlaying) {
|
||||
ctr.pause();
|
||||
} else {
|
||||
ctr.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user