From 119ba920eb7b4b400e6d9141cbedeb26a2a692d1 Mon Sep 17 00:00:00 2001 From: lcy <2503978335@qq.com> Date: Mon, 19 Jun 2023 17:08:34 +0800 Subject: [PATCH] feat:视频播放器 --- android/app/src/main/AndroidManifest.xml | 1 + ios/Runner/Info.plist | 5 +++++ lib/common/extension/string_extension.dart | 2 +- lib/home/home_page.dart | 3 ++- lib/practice/chosetopic/topicword/topic_word_page.dart | 2 +- lib/route/route.dart | 4 ++++ lib/video/lookvideo/bloc/look_video_bloc.dart | 17 +++++++++++++++++ lib/video/lookvideo/bloc/look_video_event.dart | 4 ++++ lib/video/lookvideo/bloc/look_video_state.dart | 8 ++++++++ lib/video/lookvideo/look_video_page.dart | 33 +++++++++++++++++++++++++++++++++ lib/video/lookvideo/widgets/video_opera_widget.dart | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/video/lookvideo/widgets/video_widget.dart | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 4 ++++ 13 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 lib/video/lookvideo/bloc/look_video_bloc.dart create mode 100644 lib/video/lookvideo/bloc/look_video_event.dart create mode 100644 lib/video/lookvideo/bloc/look_video_state.dart create mode 100644 lib/video/lookvideo/look_video_page.dart create mode 100644 lib/video/lookvideo/widgets/video_opera_widget.dart create mode 100644 lib/video/lookvideo/widgets/video_widget.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7ffad04..9d2a629 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,4 +30,5 @@ android:name="flutterEmbedding" android:value="2" /> + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 36d79e4..6cf0f9e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -26,6 +26,11 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/lib/common/extension/string_extension.dart b/lib/common/extension/string_extension.dart index d42297a..2536904 100644 --- a/lib/common/extension/string_extension.dart +++ b/lib/common/extension/string_extension.dart @@ -6,4 +6,4 @@ extension AssetExtension on String { String get assetImg => _assetImagePrefix + this; String get assetPng => 'assets/images/$this.png'; String get assetGif => 'assets/images/$this.gif'; -} \ No newline at end of file +} diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 1e12179..01d2516 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -34,7 +34,8 @@ class _HomePageView extends StatelessWidget { Navigator.of(AppRouter.context).pushNamed(AppRouteName.shop); } else { // Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicPic); - Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicWord); + // Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicWord); + Navigator.of(AppRouter.context).pushNamed(AppRouteName.lookVideo); } } diff --git a/lib/practice/chosetopic/topicword/topic_word_page.dart b/lib/practice/chosetopic/topicword/topic_word_page.dart index 90f6f97..b8f7c8c 100644 --- a/lib/practice/chosetopic/topicword/topic_word_page.dart +++ b/lib/practice/chosetopic/topicword/topic_word_page.dart @@ -11,7 +11,7 @@ class TopicWordPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TopicWordBloc(PageController(), 3), + create: (context) => TopicWordBloc(PageController(), 4), child: _TopicWordPage(), ); } diff --git a/lib/route/route.dart b/lib/route/route.dart index 2b3fee6..a48edce 100644 --- a/lib/route/route.dart +++ b/lib/route/route.dart @@ -15,6 +15,7 @@ import 'package:wow_english/shop/exchane/exchange_lesson_page.dart'; import 'package:wow_english/shop/exchangelist/exchange_lesson_list_page.dart'; import 'package:wow_english/shop/home/shop_home_page.dart'; import 'package:wow_english/tab/tab_page.dart'; +import 'package:wow_english/video/lookvideo/look_video_page.dart'; class AppRouteName { @@ -32,6 +33,7 @@ class AppRouteName { static const String reAfter = 'reAfter'; static const String topicPic = 'topicPic'; static const String topicWord = 'topicWord'; + static const String lookVideo = 'lookVideo'; static const String tab = '/'; } @@ -70,6 +72,8 @@ class AppRouter { return CupertinoPageRoute(builder: (_) => const TopicPicturePage()); case AppRouteName.topicWord: return CupertinoPageRoute(builder: (_) => const TopicWordPage()); + case AppRouteName.lookVideo: + return CupertinoPageRoute(builder: (_) => const LookVideoPage()); case AppRouteName.setPwd: final phoneNum = (settings.arguments as Map)['phoneNumber'] as String; return CupertinoPageRoute(builder: (_) => SetPassWordPage(phoneNum: phoneNum)); diff --git a/lib/video/lookvideo/bloc/look_video_bloc.dart b/lib/video/lookvideo/bloc/look_video_bloc.dart new file mode 100644 index 0000000..9c10502 --- /dev/null +++ b/lib/video/lookvideo/bloc/look_video_bloc.dart @@ -0,0 +1,17 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:video_player/video_player.dart'; + +part 'look_video_event.dart'; +part 'look_video_state.dart'; + +class LookVideoBloc extends Bloc { + + VideoPlayerController? _controller; + + LookVideoBloc() : super(LookVideoInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/video/lookvideo/bloc/look_video_event.dart b/lib/video/lookvideo/bloc/look_video_event.dart new file mode 100644 index 0000000..95bd813 --- /dev/null +++ b/lib/video/lookvideo/bloc/look_video_event.dart @@ -0,0 +1,4 @@ +part of 'look_video_bloc.dart'; + +@immutable +abstract class LookVideoEvent {} diff --git a/lib/video/lookvideo/bloc/look_video_state.dart b/lib/video/lookvideo/bloc/look_video_state.dart new file mode 100644 index 0000000..9d7102f --- /dev/null +++ b/lib/video/lookvideo/bloc/look_video_state.dart @@ -0,0 +1,8 @@ +part of 'look_video_bloc.dart'; + +@immutable +abstract class LookVideoState {} + +class LookVideoInitial extends LookVideoState {} + +class VideoStarState extends LookVideoState {} diff --git a/lib/video/lookvideo/look_video_page.dart b/lib/video/lookvideo/look_video_page.dart new file mode 100644 index 0000000..b9c3a5c --- /dev/null +++ b/lib/video/lookvideo/look_video_page.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wow_english/video/lookvideo/bloc/look_video_bloc.dart'; +import 'package:wow_english/video/lookvideo/widgets/video_widget.dart'; + +class LookVideoPage extends StatelessWidget { + const LookVideoPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LookVideoBloc(), + child: _LookVideoPage(), + ); + } +} + +class _LookVideoPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context,state){}, + child: _lookVideoView(), + ); + } + + Widget _lookVideoView() => BlocBuilder( + builder: (context,state){ + return const VideoWidget( + videoUrl: 'https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7194236f31b2e1e3da0fe06cfed4ba2b.mp4', + ); + }); +} \ No newline at end of file diff --git a/lib/video/lookvideo/widgets/video_opera_widget.dart b/lib/video/lookvideo/widgets/video_opera_widget.dart new file mode 100644 index 0000000..9633e33 --- /dev/null +++ b/lib/video/lookvideo/widgets/video_opera_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:wow_english/common/extension/string_extension.dart'; + +class VideoOperaWidget extends StatefulWidget { + const VideoOperaWidget({super.key, this.currentTime = '00:00', this.totalTime = '00:00', this.degree = 0.0,}); + //当前播放时间 + final String currentTime; + //总时间 + final String totalTime; + final double degree; + + @override + State createState() { + return _VideoOperaWidgetState(); + } +} + +class _VideoOperaWidgetState extends State { + @override + Widget build(BuildContext context) { + return SafeArea( + child: SizedBox( + width: double.infinity, + height: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(left: 8.5.w,right: 8.5.w,top: 11.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Image.asset( + 'back_around'.assetPng, + height: 40, + width: 40 + ), + 18.horizontalSpace, + Container( + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6.r), + border: Border.all( + width: 1.5, + color: const Color(0xFF140C10) + ) + ), + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Text( + 'song', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.sp, + color: const Color(0xFF333333), + ), + ), + ) + ], + ), + Container( + height: 40.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6.r), + border: Border.all( + width: 1.5, + color: const Color(0xFF140C10) + ) + ), + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Text( + '中/英', + style: TextStyle( + fontSize: 20.sp, + color: const Color(0xFF333333), + ), + ), + ) + ], + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/video/lookvideo/widgets/video_widget.dart b/lib/video/lookvideo/widgets/video_widget.dart new file mode 100644 index 0000000..7284f57 --- /dev/null +++ b/lib/video/lookvideo/widgets/video_widget.dart @@ -0,0 +1,154 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:wow_english/video/lookvideo/widgets/video_opera_widget.dart'; + +class VideoWidget extends StatefulWidget { + const VideoWidget({super.key, this.videoUrl = ''}); + + final String videoUrl; + + @override + State createState() { + return _VideoWidgetState(); + } +} + +class _VideoWidgetState extends State { + VideoPlayerController? _controller; + String _currentTime = '00:00'; + String _totalTime = '00:00'; + double _playDegree = 0.0; + bool _hiddenTipView = false; + TimerUtil? timerUtil; + + String formatDuration(Duration duration) { + String hours = duration.inHours.toString().padLeft(2, '0'); + String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return "$hours:$minutes:$seconds"; + } + + void _addListener() { + _controller!.addListener(() { + if(_controller!.value.isInitialized) { + if (_controller!.value.isPlaying) { + setState(() { + double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble(); + int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60); + _currentTime = formatDuration(_controller!.value.position); + _playDegree = currentSecond/totalSecond; + }); + } + } + }); + } + + //开始倒计时 + void startTimer() { + if(timerUtil == null) { + timerUtil = TimerUtil(mInterval: 1000,mTotalTime: 1000*10); + timerUtil!.setOnTimerTickCallback((int tick) { + double currentTick = tick / 1000; + if (kDebugMode) { + print(currentTick); + } + if (currentTick.toInt() == 0) {//倒计时结束 + setState(() { + _hiddenTipView = true; + }); + timerUtil!.cancel(); + timerUtil = null; + } + }); + timerUtil!.startCountDown(); + } + } + + //取消倒计时 + void cancelTimer() { + timerUtil!.cancel(); + timerUtil = null; + } + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.videoUrl) + ..initialize().then((_){ + startTimer(); + setState(() { + _currentTime = formatDuration(_controller!.value.position); + _totalTime = formatDuration(_controller!.value.duration); + _controller!.setLooping(true); + _controller!.setVolume(100); + _controller!.play(); + }); + _addListener(); + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _hiddenTipView = !_hiddenTipView; + if(!_hiddenTipView) { + startTimer(); + } else { + if (timerUtil!.isActive()) { + cancelTimer(); + } + } + }); + }, + onDoubleTap: () { + if(_controller!.value.isInitialized) { + if (_controller!.value.isPlaying) { + _controller!.pause(); + } else { + _controller!.play(); + } + } + }, + child: Center( + child: _controller!.value.isInitialized ? Stack( + alignment: Alignment.center, + children: [ + SizedBox( + height: double.infinity, + width: double.infinity, + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, + child: VideoPlayer(_controller!), + ), + ), + Offstage( + offstage: _hiddenTipView, + child: VideoOperaWidget( + currentTime: _currentTime, + totalTime: _totalTime, + degree: _playDegree, + ), + ) + ], + ): Container( + color: Colors.black, + ), + ), + ); + } + + @override + void dispose() { + _controller?.dispose(); + _controller?.removeListener(() {}); + if (timerUtil != null) { + timerUtil!.cancel(); + timerUtil = null; + } + super.dispose(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 841286f..91a4aa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,7 +85,11 @@ dependencies: limiting_direction_csx: ^0.2.0 # 富文本插件 https://pub.dev/packages/extended_text extended_text: ^11.0.1 + # 视频播放 https://pub.dev/packages/video_player + video_player: ^2.6.1 + # UI适配 https://pub.dev/packages/responsive_framework responsive_framework: ^1.0.0 + auto_orientation: ^2.3.1 dev_dependencies: build_runner: ^2.4.4 -- libgit2 0.22.2