Commit 119ba920eb7b4b400e6d9141cbedeb26a2a692d1
1 parent
624214d0
feat:视频播放器
Showing
13 changed files
with
327 additions
and
3 deletions
android/app/src/main/AndroidManifest.xml
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 'package:wow_english/shop/exchane/exchange_lesson_page.dart'; | @@ -15,6 +15,7 @@ import 'package:wow_english/shop/exchane/exchange_lesson_page.dart'; | ||
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
lib/video/lookvideo/bloc/look_video_state.dart
0 → 100644
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 |