Commit 119ba920eb7b4b400e6d9141cbedeb26a2a692d1

Authored by liangchengyou
1 parent 624214d0

feat:视频播放器

android/app/src/main/AndroidManifest.xml
@@ -30,4 +30,5 @@ @@ -30,4 +30,5 @@
30 android:name="flutterEmbedding" 30 android:name="flutterEmbedding"
31 android:value="2" /> 31 android:value="2" />
32 </application> 32 </application>
  33 + <uses-permission android:name="android.permission.INTERNET"/>
33 </manifest> 34 </manifest>
ios/Runner/Info.plist
@@ -26,6 +26,11 @@ @@ -26,6 +26,11 @@
26 <string>$(FLUTTER_BUILD_NUMBER)</string> 26 <string>$(FLUTTER_BUILD_NUMBER)</string>
27 <key>LSRequiresIPhoneOS</key> 27 <key>LSRequiresIPhoneOS</key>
28 <true/> 28 <true/>
  29 + <key>NSAppTransportSecurity</key>
  30 + <dict>
  31 + <key>NSAllowsArbitraryLoads</key>
  32 + <true/>
  33 + </dict>
29 <key>UIApplicationSupportsIndirectInputEvents</key> 34 <key>UIApplicationSupportsIndirectInputEvents</key>
30 <true/> 35 <true/>
31 <key>UILaunchStoryboardName</key> 36 <key>UILaunchStoryboardName</key>
lib/common/extension/string_extension.dart
@@ -6,4 +6,4 @@ extension AssetExtension on String { @@ -6,4 +6,4 @@ extension AssetExtension on String {
6 String get assetImg => _assetImagePrefix + this; 6 String get assetImg => _assetImagePrefix + this;
7 String get assetPng => 'assets/images/$this.png'; 7 String get assetPng => 'assets/images/$this.png';
8 String get assetGif => 'assets/images/$this.gif'; 8 String get assetGif => 'assets/images/$this.gif';
9 -}  
10 \ No newline at end of file 9 \ No newline at end of file
  10 +}
lib/home/home_page.dart
@@ -34,7 +34,8 @@ class _HomePageView extends StatelessWidget { @@ -34,7 +34,8 @@ class _HomePageView extends StatelessWidget {
34 Navigator.of(AppRouter.context).pushNamed(AppRouteName.shop); 34 Navigator.of(AppRouter.context).pushNamed(AppRouteName.shop);
35 } else { 35 } else {
36 // Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicPic); 36 // Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicPic);
37 - Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicWord); 37 + // Navigator.of(AppRouter.context).pushNamed(AppRouteName.topicWord);
  38 + Navigator.of(AppRouter.context).pushNamed(AppRouteName.lookVideo);
38 } 39 }
39 } 40 }
40 41
lib/practice/chosetopic/topicword/topic_word_page.dart
@@ -11,7 +11,7 @@ class TopicWordPage extends StatelessWidget { @@ -11,7 +11,7 @@ class TopicWordPage extends StatelessWidget {
11 @override 11 @override
12 Widget build(BuildContext context) { 12 Widget build(BuildContext context) {
13 return BlocProvider( 13 return BlocProvider(
14 - create: (context) => TopicWordBloc(PageController(), 3), 14 + create: (context) => TopicWordBloc(PageController(), 4),
15 child: _TopicWordPage(), 15 child: _TopicWordPage(),
16 ); 16 );
17 } 17 }
lib/route/route.dart
@@ -15,6 +15,7 @@ import &#39;package:wow_english/shop/exchane/exchange_lesson_page.dart&#39;; @@ -15,6 +15,7 @@ import &#39;package:wow_english/shop/exchane/exchange_lesson_page.dart&#39;;
15 import 'package:wow_english/shop/exchangelist/exchange_lesson_list_page.dart'; 15 import 'package:wow_english/shop/exchangelist/exchange_lesson_list_page.dart';
16 import 'package:wow_english/shop/home/shop_home_page.dart'; 16 import 'package:wow_english/shop/home/shop_home_page.dart';
17 import 'package:wow_english/tab/tab_page.dart'; 17 import 'package:wow_english/tab/tab_page.dart';
  18 +import 'package:wow_english/video/lookvideo/look_video_page.dart';
18 19
19 20
20 class AppRouteName { 21 class AppRouteName {
@@ -32,6 +33,7 @@ class AppRouteName { @@ -32,6 +33,7 @@ class AppRouteName {
32 static const String reAfter = 'reAfter'; 33 static const String reAfter = 'reAfter';
33 static const String topicPic = 'topicPic'; 34 static const String topicPic = 'topicPic';
34 static const String topicWord = 'topicWord'; 35 static const String topicWord = 'topicWord';
  36 + static const String lookVideo = 'lookVideo';
35 static const String tab = '/'; 37 static const String tab = '/';
36 } 38 }
37 39
@@ -70,6 +72,8 @@ class AppRouter { @@ -70,6 +72,8 @@ class AppRouter {
70 return CupertinoPageRoute(builder: (_) => const TopicPicturePage()); 72 return CupertinoPageRoute(builder: (_) => const TopicPicturePage());
71 case AppRouteName.topicWord: 73 case AppRouteName.topicWord:
72 return CupertinoPageRoute(builder: (_) => const TopicWordPage()); 74 return CupertinoPageRoute(builder: (_) => const TopicWordPage());
  75 + case AppRouteName.lookVideo:
  76 + return CupertinoPageRoute(builder: (_) => const LookVideoPage());
73 case AppRouteName.setPwd: 77 case AppRouteName.setPwd:
74 final phoneNum = (settings.arguments as Map)['phoneNumber'] as String; 78 final phoneNum = (settings.arguments as Map)['phoneNumber'] as String;
75 return CupertinoPageRoute(builder: (_) => SetPassWordPage(phoneNum: phoneNum)); 79 return CupertinoPageRoute(builder: (_) => SetPassWordPage(phoneNum: phoneNum));
lib/video/lookvideo/bloc/look_video_bloc.dart 0 → 100644
  1 +import 'package:flutter/cupertino.dart';
  2 +import 'package:flutter_bloc/flutter_bloc.dart';
  3 +import 'package:video_player/video_player.dart';
  4 +
  5 +part 'look_video_event.dart';
  6 +part 'look_video_state.dart';
  7 +
  8 +class LookVideoBloc extends Bloc<LookVideoEvent, LookVideoState> {
  9 +
  10 + VideoPlayerController? _controller;
  11 +
  12 + LookVideoBloc() : super(LookVideoInitial()) {
  13 + on<LookVideoEvent>((event, emit) {
  14 + // TODO: implement event handler
  15 + });
  16 + }
  17 +}
lib/video/lookvideo/bloc/look_video_event.dart 0 → 100644
  1 +part of 'look_video_bloc.dart';
  2 +
  3 +@immutable
  4 +abstract class LookVideoEvent {}
lib/video/lookvideo/bloc/look_video_state.dart 0 → 100644
  1 +part of 'look_video_bloc.dart';
  2 +
  3 +@immutable
  4 +abstract class LookVideoState {}
  5 +
  6 +class LookVideoInitial extends LookVideoState {}
  7 +
  8 +class VideoStarState extends LookVideoState {}
lib/video/lookvideo/look_video_page.dart 0 → 100644
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter_bloc/flutter_bloc.dart';
  3 +import 'package:wow_english/video/lookvideo/bloc/look_video_bloc.dart';
  4 +import 'package:wow_english/video/lookvideo/widgets/video_widget.dart';
  5 +
  6 +class LookVideoPage extends StatelessWidget {
  7 + const LookVideoPage({super.key});
  8 +
  9 + @override
  10 + Widget build(BuildContext context) {
  11 + return BlocProvider(
  12 + create: (context) => LookVideoBloc(),
  13 + child: _LookVideoPage(),
  14 + );
  15 + }
  16 +}
  17 +
  18 +class _LookVideoPage extends StatelessWidget {
  19 + @override
  20 + Widget build(BuildContext context) {
  21 + return BlocListener<LookVideoBloc, LookVideoState>(
  22 + listener: (context,state){},
  23 + child: _lookVideoView(),
  24 + );
  25 + }
  26 +
  27 + Widget _lookVideoView() => BlocBuilder<LookVideoBloc, LookVideoState>(
  28 + builder: (context,state){
  29 + return const VideoWidget(
  30 + videoUrl: 'https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7194236f31b2e1e3da0fe06cfed4ba2b.mp4',
  31 + );
  32 + });
  33 +}
0 \ No newline at end of file 34 \ No newline at end of file
lib/video/lookvideo/widgets/video_opera_widget.dart 0 → 100644
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter_screenutil/flutter_screenutil.dart';
  3 +import 'package:wow_english/common/extension/string_extension.dart';
  4 +
  5 +class VideoOperaWidget extends StatefulWidget {
  6 + const VideoOperaWidget({super.key, this.currentTime = '00:00', this.totalTime = '00:00', this.degree = 0.0,});
  7 + //当前播放时间
  8 + final String currentTime;
  9 + //总时间
  10 + final String totalTime;
  11 + final double degree;
  12 +
  13 + @override
  14 + State<StatefulWidget> createState() {
  15 + return _VideoOperaWidgetState();
  16 + }
  17 +}
  18 +
  19 +class _VideoOperaWidgetState extends State<VideoOperaWidget> {
  20 + @override
  21 + Widget build(BuildContext context) {
  22 + return SafeArea(
  23 + child: SizedBox(
  24 + width: double.infinity,
  25 + height: double.infinity,
  26 + child: Column(
  27 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  28 + children: [
  29 + Padding(
  30 + padding: EdgeInsets.only(left: 8.5.w,right: 8.5.w,top: 11.h),
  31 + child: Row(
  32 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  33 + children: [
  34 + Row(
  35 + children: [
  36 + Image.asset(
  37 + 'back_around'.assetPng,
  38 + height: 40,
  39 + width: 40
  40 + ),
  41 + 18.horizontalSpace,
  42 + Container(
  43 + height: 40.h,
  44 + alignment: Alignment.center,
  45 + decoration: BoxDecoration(
  46 + color: Colors.white,
  47 + borderRadius: BorderRadius.circular(6.r),
  48 + border: Border.all(
  49 + width: 1.5,
  50 + color: const Color(0xFF140C10)
  51 + )
  52 + ),
  53 + padding: EdgeInsets.symmetric(horizontal: 10.w),
  54 + child: Text(
  55 + 'song',
  56 + textAlign: TextAlign.center,
  57 + style: TextStyle(
  58 + fontSize: 20.sp,
  59 + color: const Color(0xFF333333),
  60 + ),
  61 + ),
  62 + )
  63 + ],
  64 + ),
  65 + Container(
  66 + height: 40.h,
  67 + alignment: Alignment.center,
  68 + decoration: BoxDecoration(
  69 + color: Colors.white,
  70 + borderRadius: BorderRadius.circular(6.r),
  71 + border: Border.all(
  72 + width: 1.5,
  73 + color: const Color(0xFF140C10)
  74 + )
  75 + ),
  76 + padding: EdgeInsets.symmetric(horizontal: 10.w),
  77 + child: Text(
  78 + '中/英',
  79 + style: TextStyle(
  80 + fontSize: 20.sp,
  81 + color: const Color(0xFF333333),
  82 + ),
  83 + ),
  84 + )
  85 + ],
  86 + ),
  87 + )
  88 + ],
  89 + ),
  90 + ),
  91 + );
  92 + }
  93 +}
0 \ No newline at end of file 94 \ No newline at end of file
lib/video/lookvideo/widgets/video_widget.dart 0 → 100644
  1 +import 'package:common_utils/common_utils.dart';
  2 +import 'package:flutter/foundation.dart';
  3 +import 'package:flutter/material.dart';
  4 +import 'package:video_player/video_player.dart';
  5 +import 'package:wow_english/video/lookvideo/widgets/video_opera_widget.dart';
  6 +
  7 +class VideoWidget extends StatefulWidget {
  8 + const VideoWidget({super.key, this.videoUrl = ''});
  9 +
  10 + final String videoUrl;
  11 +
  12 + @override
  13 + State<StatefulWidget> createState() {
  14 + return _VideoWidgetState();
  15 + }
  16 +}
  17 +
  18 +class _VideoWidgetState extends State<VideoWidget> {
  19 + VideoPlayerController? _controller;
  20 + String _currentTime = '00:00';
  21 + String _totalTime = '00:00';
  22 + double _playDegree = 0.0;
  23 + bool _hiddenTipView = false;
  24 + TimerUtil? timerUtil;
  25 +
  26 + String formatDuration(Duration duration) {
  27 + String hours = duration.inHours.toString().padLeft(2, '0');
  28 + String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
  29 + String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
  30 + return "$hours:$minutes:$seconds";
  31 + }
  32 +
  33 + void _addListener() {
  34 + _controller!.addListener(() {
  35 + if(_controller!.value.isInitialized) {
  36 + if (_controller!.value.isPlaying) {
  37 + setState(() {
  38 + double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble();
  39 + int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60);
  40 + _currentTime = formatDuration(_controller!.value.position);
  41 + _playDegree = currentSecond/totalSecond;
  42 + });
  43 + }
  44 + }
  45 + });
  46 + }
  47 +
  48 + //开始倒计时
  49 + void startTimer() {
  50 + if(timerUtil == null) {
  51 + timerUtil = TimerUtil(mInterval: 1000,mTotalTime: 1000*10);
  52 + timerUtil!.setOnTimerTickCallback((int tick) {
  53 + double currentTick = tick / 1000;
  54 + if (kDebugMode) {
  55 + print(currentTick);
  56 + }
  57 + if (currentTick.toInt() == 0) {//倒计时结束
  58 + setState(() {
  59 + _hiddenTipView = true;
  60 + });
  61 + timerUtil!.cancel();
  62 + timerUtil = null;
  63 + }
  64 + });
  65 + timerUtil!.startCountDown();
  66 + }
  67 + }
  68 +
  69 + //取消倒计时
  70 + void cancelTimer() {
  71 + timerUtil!.cancel();
  72 + timerUtil = null;
  73 + }
  74 +
  75 + @override
  76 + void initState() {
  77 + super.initState();
  78 + _controller = VideoPlayerController.network(widget.videoUrl)
  79 + ..initialize().then((_){
  80 + startTimer();
  81 + setState(() {
  82 + _currentTime = formatDuration(_controller!.value.position);
  83 + _totalTime = formatDuration(_controller!.value.duration);
  84 + _controller!.setLooping(true);
  85 + _controller!.setVolume(100);
  86 + _controller!.play();
  87 + });
  88 + _addListener();
  89 + });
  90 + }
  91 +
  92 + @override
  93 + Widget build(BuildContext context) {
  94 + return GestureDetector(
  95 + onTap: () {
  96 + setState(() {
  97 + _hiddenTipView = !_hiddenTipView;
  98 + if(!_hiddenTipView) {
  99 + startTimer();
  100 + } else {
  101 + if (timerUtil!.isActive()) {
  102 + cancelTimer();
  103 + }
  104 + }
  105 + });
  106 + },
  107 + onDoubleTap: () {
  108 + if(_controller!.value.isInitialized) {
  109 + if (_controller!.value.isPlaying) {
  110 + _controller!.pause();
  111 + } else {
  112 + _controller!.play();
  113 + }
  114 + }
  115 + },
  116 + child: Center(
  117 + child: _controller!.value.isInitialized ? Stack(
  118 + alignment: Alignment.center,
  119 + children: [
  120 + SizedBox(
  121 + height: double.infinity,
  122 + width: double.infinity,
  123 + child: AspectRatio(
  124 + aspectRatio: _controller!.value.aspectRatio,
  125 + child: VideoPlayer(_controller!),
  126 + ),
  127 + ),
  128 + Offstage(
  129 + offstage: _hiddenTipView,
  130 + child: VideoOperaWidget(
  131 + currentTime: _currentTime,
  132 + totalTime: _totalTime,
  133 + degree: _playDegree,
  134 + ),
  135 + )
  136 + ],
  137 + ): Container(
  138 + color: Colors.black,
  139 + ),
  140 + ),
  141 + );
  142 + }
  143 +
  144 + @override
  145 + void dispose() {
  146 + _controller?.dispose();
  147 + _controller?.removeListener(() {});
  148 + if (timerUtil != null) {
  149 + timerUtil!.cancel();
  150 + timerUtil = null;
  151 + }
  152 + super.dispose();
  153 + }
  154 +}
pubspec.yaml
@@ -85,7 +85,11 @@ dependencies: @@ -85,7 +85,11 @@ dependencies:
85 limiting_direction_csx: ^0.2.0 85 limiting_direction_csx: ^0.2.0
86 # 富文本插件 https://pub.dev/packages/extended_text 86 # 富文本插件 https://pub.dev/packages/extended_text
87 extended_text: ^11.0.1 87 extended_text: ^11.0.1
  88 + # 视频播放 https://pub.dev/packages/video_player
  89 + video_player: ^2.6.1
  90 + # UI适配 https://pub.dev/packages/responsive_framework
88 responsive_framework: ^1.0.0 91 responsive_framework: ^1.0.0
  92 + auto_orientation: ^2.3.1
89 93
90 dev_dependencies: 94 dev_dependencies:
91 build_runner: ^2.4.4 95 build_runner: ^2.4.4