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 | 26 | <string>$(FLUTTER_BUILD_NUMBER)</string> |
27 | 27 | <key>LSRequiresIPhoneOS</key> |
28 | 28 | <true/> |
29 | + <key>NSAppTransportSecurity</key> | |
30 | + <dict> | |
31 | + <key>NSAllowsArbitraryLoads</key> | |
32 | + <true/> | |
33 | + </dict> | |
29 | 34 | <key>UIApplicationSupportsIndirectInputEvents</key> |
30 | 35 | <true/> |
31 | 36 | <key>UILaunchStoryboardName</key> | ... | ... |
lib/common/extension/string_extension.dart
lib/home/home_page.dart
... | ... | @@ -34,7 +34,8 @@ class _HomePageView extends StatelessWidget { |
34 | 34 | Navigator.of(AppRouter.context).pushNamed(AppRouteName.shop); |
35 | 35 | } else { |
36 | 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 | 11 | @override |
12 | 12 | Widget build(BuildContext context) { |
13 | 13 | return BlocProvider( |
14 | - create: (context) => TopicWordBloc(PageController(), 3), | |
14 | + create: (context) => TopicWordBloc(PageController(), 4), | |
15 | 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 | 15 | import 'package:wow_english/shop/exchangelist/exchange_lesson_list_page.dart'; |
16 | 16 | import 'package:wow_english/shop/home/shop_home_page.dart'; |
17 | 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 | 21 | class AppRouteName { |
... | ... | @@ -32,6 +33,7 @@ class AppRouteName { |
32 | 33 | static const String reAfter = 'reAfter'; |
33 | 34 | static const String topicPic = 'topicPic'; |
34 | 35 | static const String topicWord = 'topicWord'; |
36 | + static const String lookVideo = 'lookVideo'; | |
35 | 37 | static const String tab = '/'; |
36 | 38 | } |
37 | 39 | |
... | ... | @@ -70,6 +72,8 @@ class AppRouter { |
70 | 72 | return CupertinoPageRoute(builder: (_) => const TopicPicturePage()); |
71 | 73 | case AppRouteName.topicWord: |
72 | 74 | return CupertinoPageRoute(builder: (_) => const TopicWordPage()); |
75 | + case AppRouteName.lookVideo: | |
76 | + return CupertinoPageRoute(builder: (_) => const LookVideoPage()); | |
73 | 77 | case AppRouteName.setPwd: |
74 | 78 | final phoneNum = (settings.arguments as Map)['phoneNumber'] as String; |
75 | 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 | 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 | 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 | 85 | limiting_direction_csx: ^0.2.0 |
86 | 86 | # 富文本插件 https://pub.dev/packages/extended_text |
87 | 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 | 91 | responsive_framework: ^1.0.0 |
92 | + auto_orientation: ^2.3.1 | |
89 | 93 | |
90 | 94 | dev_dependencies: |
91 | 95 | build_runner: ^2.4.4 | ... | ... |