import 'dart:async'; import 'dart:ui'; import 'package:extended_image/extended_image.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter_meedu_media_kit/meedu_player.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:wakelock/wakelock.dart'; class VideoDetailPage extends StatefulWidget { const VideoDetailPage({Key? key}) : super(key: key); @override State createState() => _VideoDetailPageState(); static final RouteObserver routeObserver = RouteObserver(); } class _VideoDetailPageState extends State with TickerProviderStateMixin, RouteAware { final VideoDetailController videoDetailController = Get.put(VideoDetailController(), tag: Get.arguments['heroTag']); MeeduPlayerController? _meeduPlayerController; final ScrollController _extendNestCtr = ScrollController(); late AnimationController animationController; StreamSubscription? _playerEventSubs; bool isPlay = false; PlayerStatus playerStatus = PlayerStatus.paused; bool isShowCover = true; double doubleOffset = 0; @override void initState() { super.initState(); _meeduPlayerController = videoDetailController.meeduPlayerController; _playerEventSubs = _meeduPlayerController!.onPlayerStatusChanged.listen( (PlayerStatus status) { videoDetailController.markHeartBeat(); playerStatus = status; if (status == PlayerStatus.playing) { Wakelock.enable(); isPlay = false; isShowCover = false; setState(() {}); videoDetailController.loopHeartBeat(); } else { videoDetailController.timer!.cancel(); isPlay = true; setState(() {}); Wakelock.disable(); // 播放完成停止 or 切换下一个 if (status == PlayerStatus.completed) {} } }, ); animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600)); _extendNestCtr.addListener( () { double offset = _extendNestCtr.position.pixels; if (offset > doubleOffset) { animationController.forward(); } else { animationController.reverse(); } doubleOffset = offset; setState(() {}); }, ); } Future _meeduDispose() async { if (_meeduPlayerController != null) { _playerEventSubs?.cancel(); await _meeduPlayerController!.dispose(); _meeduPlayerController = null; // The next line disables the wakelock again. await Wakelock.disable(); } } void continuePlay() async { await _extendNestCtr.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); _meeduPlayerController!.play(); } @override void dispose() { videoDetailController.meeduPlayerController.dispose(); videoDetailController.timer!.cancel(); super.dispose(); } @override // 离开当前页面时 void didPushNext() async { if (!_meeduPlayerController!.pipEnabled) { _meeduPlayerController!.pause(); } if (videoDetailController.timer!.isActive) { videoDetailController.timer!.cancel(); } super.didPushNext(); } @override // 返回当前页面时 void didPopNext() async { if (_extendNestCtr.position.pixels == 0) { await Future.delayed(const Duration(milliseconds: 300)); _meeduPlayerController!.play(); } if (!videoDetailController.timer!.isActive) { videoDetailController.loopHeartBeat(); } super.didPopNext(); } @override void didChangeDependencies() { super.didChangeDependencies(); VideoDetailPage.routeObserver .subscribe(this, ModalRoute.of(context) as PageRoute); } @override Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final double pinnedHeaderHeight = statusBarHeight + kToolbarHeight + videoHeight; return SafeArea( top: false, bottom: false, child: Stack( children: [ Positioned( top: 0, left: 0, right: 0, child: Obx( () => NetworkImgLayer( type: 'emote', src: videoDetailController.bgCover.value, width: Get.size.width, height: videoHeight, ), ), ), Positioned.fill( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100), //可以看源码 child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background.withOpacity(0.1), ), ), ), ), Scaffold( resizeToAvoidBottomInset: false, key: videoDetailController.scaffoldKey, backgroundColor: Colors.transparent, body: ExtendedNestedScrollView( controller: _extendNestCtr, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( automaticallyImplyLeading: false, pinned: false, elevation: 0, scrolledUnderElevation: 0, forceElevated: innerBoxIsScrolled, expandedHeight: videoHeight, backgroundColor: Colors.transparent, // backgroundColor: Theme.of(context).colorScheme.background, flexibleSpace: FlexibleSpaceBar( background: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top), child: LayoutBuilder( builder: (context, boxConstraints) { double maxWidth = boxConstraints.maxWidth; double maxHeight = boxConstraints.maxHeight; return Hero( tag: videoDetailController.heroTag, child: Stack( children: [ AspectRatio( aspectRatio: 16 / 9, child: MeeduVideoPlayer( controller: _meeduPlayerController!, header: (BuildContext context, MeeduPlayerController _meeduPlayerController, Responsive) { return AppBar( toolbarHeight: 40, backgroundColor: Colors.transparent, primary: false, elevation: 0, scrolledUnderElevation: 0, foregroundColor: Colors.white, leading: IconButton( onPressed: () { Get.back(); }, icon: const Icon( Icons.arrow_back_ios, size: 19, ), ), ); }, ), ), Visibility( visible: isShowCover, child: Positioned( top: 0, left: 0, right: 0, child: NetworkImgLayer( type: 'emote', src: videoDetailController .videoItem['pic'], width: maxWidth, height: maxHeight, ), ), ), ], ), ); }, ), ), ), ), ]; }, pinnedHeaderSliverHeightBuilder: () { return playerStatus != PlayerStatus.playing ? statusBarHeight + kToolbarHeight : pinnedHeaderHeight; }, onlyOneScrollInBody: true, body: Container( color: Theme.of(context).colorScheme.background, child: Column( children: [ Container( width: double.infinity, height: 0, decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor.withOpacity(0.1), ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ Container( width: 280, margin: const EdgeInsets.only(left: 20), child: Obx( () => TabBar( controller: videoDetailController.tabCtr, dividerColor: Colors.transparent, indicatorColor: Theme.of(context).colorScheme.background, tabs: videoDetailController.tabs .map((String name) => Tab(text: name)) .toList(), ), ), ), ], ), ), Expanded( child: TabBarView( controller: videoDetailController.tabCtr, children: [ Builder( builder: (context) { return const CustomScrollView( key: PageStorageKey('简介'), slivers: [ VideoIntroPanel(), RelatedVideoPanel(), ], ); }, ), VideoReplyPanel() ], ), ), ], ), ), ), ), // 播放完成/暂停播放 Positioned( top: -statusBarHeight + (doubleOffset / (videoHeight - kToolbarHeight)) * (kToolbarHeight - 9), left: 0, right: 0, child: Opacity( opacity: doubleOffset / (videoHeight - kToolbarHeight), child: Container( height: statusBarHeight + kToolbarHeight, color: Theme.of(context).colorScheme.background, padding: EdgeInsets.only(top: statusBarHeight), child: AppBar( primary: false, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, title: TextButton( onPressed: () => continuePlay(), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.play_arrow_rounded), Text( playerStatus == PlayerStatus.paused ? '继续播放' : playerStatus == PlayerStatus.completed ? '重新播放' : '播放中', ) ], ), ), actions: [ IconButton( onPressed: () {}, icon: const Icon( Icons.share, size: 20, )), const SizedBox(width: 12) ], ), ), ), ), ], ), ); } }