Commit aa0d236034621df896e478b34841135d7f11c341

Authored by 吴启风
1 parent 46675a89

feat:过渡页-视频环节(再来一次)

lib/pages/section/subsection/base_section/bloc.dart
@@ -12,8 +12,9 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent, @@ -12,8 +12,9 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent,
12 12
13 bool isCompleteDialogShow = false; 13 bool isCompleteDialogShow = false;
14 14
15 - // 这里可以定义一些通用的逻辑  
16 - void completeSection(final VoidCallback? nextSectionTap) { 15 + ///这里可以定义一些通用的逻辑
  16 + void sectionComplete(final VoidCallback? nextSectionTap,
  17 + {BuildContext? context}) {
17 // 逻辑来标记步骤为已完成 18 // 逻辑来标记步骤为已完成
18 // 比如更新状态 19 // 比如更新状态
19 if (isCompleteDialogShow) { 20 if (isCompleteDialogShow) {
@@ -21,7 +22,7 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent, @@ -21,7 +22,7 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent,
21 } 22 }
22 isCompleteDialogShow = true; 23 isCompleteDialogShow = true;
23 showDialog( 24 showDialog(
24 - context: AppRouter.context, 25 + context: context ?? AppRouter.context,
25 barrierDismissible: false, 26 barrierDismissible: false,
26 barrierColor: Colors.black54, 27 barrierColor: Colors.black54,
27 builder: (BuildContext context) { 28 builder: (BuildContext context) {
@@ -33,30 +34,24 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent, @@ -33,30 +34,24 @@ abstract class BaseSectionBloc<E extends BaseSectionEvent,
33 child: Row( 34 child: Row(
34 mainAxisAlignment: MainAxisAlignment.spaceBetween, // 图片之间分配空间 35 mainAxisAlignment: MainAxisAlignment.spaceBetween, // 图片之间分配空间
35 children: <Widget>[ 36 children: <Widget>[
36 - // 左侧可点击的图片  
37 Expanded( 37 Expanded(
38 flex: 1, 38 flex: 1,
39 child: GestureDetector( 39 child: GestureDetector(
40 onTap: () { 40 onTap: () {
41 - isCompleteDialogShow = false;  
42 - popPage();  
43 add(SectionAgainEvent() as E); 41 add(SectionAgainEvent() as E);
  42 + popPage();
44 }, 43 },
45 child: Image.asset('section_finish_again'.assetPng), 44 child: Image.asset('section_finish_again'.assetPng),
46 ), 45 ),
47 ), 46 ),
48 - // 中间的图片  
49 Expanded( 47 Expanded(
50 flex: 2, 48 flex: 2,
51 child: Image.asset('section_finish_steve'.assetPng), 49 child: Image.asset('section_finish_steve'.assetPng),
52 ), 50 ),
53 - // 右侧可点击的图片  
54 Expanded( 51 Expanded(
55 flex: 1, 52 flex: 1,
56 child: GestureDetector( 53 child: GestureDetector(
57 onTap: () { 54 onTap: () {
58 - // 处理右侧图片的点击事件  
59 - isCompleteDialogShow = false;  
60 popPage(); 55 popPage();
61 nextSectionTap!(); 56 nextSectionTap!();
62 }, 57 },
@@ -68,6 +63,6 @@ abstract class BaseSectionBloc&lt;E extends BaseSectionEvent, @@ -68,6 +63,6 @@ abstract class BaseSectionBloc&lt;E extends BaseSectionEvent,
68 ), 63 ),
69 ); 64 );
70 }, 65 },
71 - ); 66 + ).then((value) => isCompleteDialogShow = false);
72 } 67 }
73 } 68 }
lib/pages/section/subsection/base_section/state.dart
1 abstract class BaseSectionState {} 1 abstract class BaseSectionState {}
2 2
3 -class SectionCompleted extends BaseSectionState {} 3 +///环节完成事件
  4 +class SectionCompletedState extends BaseSectionState {}
  5 +
  6 +///环节再来一次事件
  7 +class SectionAgainState extends BaseSectionState {}
lib/pages/video/lookvideo/bloc/look_video_bloc.dart
@@ -8,7 +8,7 @@ import &#39;package:wow_english/pages/section/subsection/base_section/state.dart&#39;; @@ -8,7 +8,7 @@ import &#39;package:wow_english/pages/section/subsection/base_section/state.dart&#39;;
8 part 'look_video_event.dart'; 8 part 'look_video_event.dart';
9 part 'look_video_state.dart'; 9 part 'look_video_state.dart';
10 10
11 -class LookVideoBloc extends BaseSectionBloc<LookVideoEvent, LookVideoState> { 11 +class LookVideoBloc extends BaseSectionBloc<BaseSectionEvent, BaseSectionState> {
12 12
13 VideoPlayerController? _controller; 13 VideoPlayerController? _controller;
14 14
@@ -25,5 +25,8 @@ class LookVideoBloc extends BaseSectionBloc&lt;LookVideoEvent, LookVideoState&gt; { @@ -25,5 +25,8 @@ class LookVideoBloc extends BaseSectionBloc&lt;LookVideoEvent, LookVideoState&gt; {
25 on<LookVideoEvent>((event, emit) { 25 on<LookVideoEvent>((event, emit) {
26 // TODO: implement event handler 26 // TODO: implement event handler
27 }); 27 });
  28 + on<SectionAgainEvent>((event, emit) {
  29 + emit(SectionAgainState());
  30 + });
28 } 31 }
29 } 32 }
lib/pages/video/lookvideo/bloc/look_video_event.dart
@@ -2,3 +2,5 @@ part of &#39;look_video_bloc.dart&#39;; @@ -2,3 +2,5 @@ part of &#39;look_video_bloc.dart&#39;;
2 2
3 @immutable 3 @immutable
4 abstract class LookVideoEvent extends BaseSectionEvent {} 4 abstract class LookVideoEvent extends BaseSectionEvent {}
  5 +
  6 +class RestartVideoEvent extends LookVideoEvent {}
lib/pages/video/lookvideo/look_video_page.dart
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:flutter_bloc/flutter_bloc.dart'; 2 import 'package:flutter_bloc/flutter_bloc.dart';
  3 +import 'package:wow_english/pages/section/subsection/base_section/state.dart';
3 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart'; 4 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
4 import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart'; 5 import 'package:wow_english/pages/video/lookvideo/widgets/video_widget.dart';
5 6
@@ -22,7 +23,7 @@ class LookVideoPage extends StatelessWidget { @@ -22,7 +23,7 @@ class LookVideoPage extends StatelessWidget {
22 } 23 }
23 24
24 Widget _buildPage(BuildContext context) { 25 Widget _buildPage(BuildContext context) {
25 - return BlocBuilder<LookVideoBloc, LookVideoState>(builder: (context, state) { 26 + return BlocBuilder<LookVideoBloc, BaseSectionState>(builder: (context, state) {
26 final bloc = BlocProvider.of<LookVideoBloc>(context); 27 final bloc = BlocProvider.of<LookVideoBloc>(context);
27 return Container( 28 return Container(
28 color: Colors.white, 29 color: Colors.white,
lib/pages/video/lookvideo/widgets/video_widget.dart
@@ -7,10 +7,16 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;; @@ -7,10 +7,16 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;;
7 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart'; 7 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
8 import 'package:wow_english/route/route.dart'; 8 import 'package:wow_english/route/route.dart';
9 9
  10 +import '../../../section/subsection/base_section/event.dart';
  11 +import '../../../section/subsection/base_section/state.dart';
10 import 'video_opera_widget.dart'; 12 import 'video_opera_widget.dart';
11 13
12 class VideoWidget extends StatefulWidget { 14 class VideoWidget extends StatefulWidget {
13 - const VideoWidget({super.key, this.videoUrl = '',this.typeTitle, this.courseLessonId = '', this.isTopic = false}); 15 + const VideoWidget({super.key,
  16 + this.videoUrl = '',
  17 + this.typeTitle,
  18 + this.courseLessonId = '',
  19 + this.isTopic = false});
14 20
15 final String videoUrl; 21 final String videoUrl;
16 final String? typeTitle; 22 final String? typeTitle;
@@ -33,45 +39,68 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -33,45 +39,68 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
33 39
34 String formatDuration(Duration duration) { 40 String formatDuration(Duration duration) {
35 String hours = duration.inHours.toString().padLeft(2, '0'); 41 String hours = duration.inHours.toString().padLeft(2, '0');
36 - String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');  
37 - String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); 42 + String minutes = duration.inMinutes.remainder(60).toString().padLeft(
  43 + 2, '0');
  44 + String seconds = duration.inSeconds.remainder(60).toString().padLeft(
  45 + 2, '0');
38 return "$hours:$minutes:$seconds"; 46 return "$hours:$minutes:$seconds";
39 } 47 }
40 48
41 void _addListener() { 49 void _addListener() {
42 _controller!.addListener(() { 50 _controller!.addListener(() {
43 - if(_controller!.value.isInitialized) { 51 + if (_controller!.value.isInitialized) {
44 if (_controller!.value.isPlaying) { 52 if (_controller!.value.isPlaying) {
45 setState(() { 53 setState(() {
46 - double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble();  
47 - int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60); 54 + double currentSecond =
  55 + (_controller!.value.position.inMinutes.remainder(60) * 60 +
  56 + _controller!.value.position.inSeconds.remainder(60))
  57 + .toDouble();
  58 + int totalSecond =
  59 + _controller!.value.duration.inMinutes.remainder(60) * 60 +
  60 + _controller!.value.duration.inSeconds.remainder(60);
48 _currentTime = formatDuration(_controller!.value.position); 61 _currentTime = formatDuration(_controller!.value.position);
49 - _playDegree = currentSecond/totalSecond; 62 + _playDegree = currentSecond / totalSecond;
50 if (_playDegree > 1.0) { 63 if (_playDegree > 1.0) {
51 _playDegree = 1.0; 64 _playDegree = 1.0;
52 } 65 }
53 - if(_playDegree < 0) { 66 + if (_playDegree < 0) {
54 _playDegree = 0.0; 67 _playDegree = 0.0;
55 } 68 }
56 }); 69 });
57 - } else if (_controller!.value.isCompleted) {  
58 - context.read<LookVideoBloc>().completeSection((){  
59 - String currentTime = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toString();  
60 - popPage(data:{'courseLessonId':widget.courseLessonId,'currentTime':currentTime,  
61 - 'nextSection':widget.isTopic});  
62 - } as VoidCallback); 70 + } else if (widget.isTopic &&
  71 + //受限于video_player库没有唯一的播放完成状态标识,发现isBuffering=false的时候暂时是安全唯一的
  72 + _controller?.value.isBuffering == false &&
  73 + _controller!.value.isCompleted &&
  74 + _controller!.value.position.inSeconds ==
  75 + _controller!.value.duration.inSeconds) {
  76 + final lookVideoBloc = context.read<LookVideoBloc>();
  77 + lookVideoBloc.sectionComplete(() {
  78 + String currentTime =
  79 + (_controller!.value.position.inMinutes.remainder(60) * 60 +
  80 + _controller!.value.position.inSeconds.remainder(60))
  81 + .toString();
  82 + popPage(data: {
  83 + 'courseLessonId': widget.courseLessonId,
  84 + 'currentTime': currentTime,
  85 + 'nextSection': widget.isTopic
  86 + });
  87 + } as VoidCallback,
  88 + againSectionTap: (() {
  89 + lookVideoBloc.add(SectionAgainEvent());
  90 + }), context: context);
63 } 91 }
64 } 92 }
65 }); 93 });
66 } 94 }
67 95
68 //开始倒计时 96 //开始倒计时
69 - void startTimer() {  
70 - if(timerUtil == null) {  
71 - timerUtil = TimerUtil(mInterval: 1000,mTotalTime: 1000*10); 97 + void startTimer() {
  98 + if (timerUtil == null) {
  99 + timerUtil = TimerUtil(mInterval: 1000, mTotalTime: 1000 * 10);
72 timerUtil!.setOnTimerTickCallback((int tick) { 100 timerUtil!.setOnTimerTickCallback((int tick) {
73 double currentTick = tick / 1000; 101 double currentTick = tick / 1000;
74 - if (currentTick.toInt() == 0) {//倒计时结束 102 + if (currentTick.toInt() == 0) {
  103 + //倒计时结束
75 setState(() { 104 setState(() {
76 _hiddenTipView = true; 105 _hiddenTipView = true;
77 }); 106 });
@@ -98,8 +127,14 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -98,8 +127,14 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
98 popPage(); 127 popPage();
99 return; 128 return;
100 } 129 }
101 - String currentTime = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toString();  
102 - popPage(data:{'courseLessonId':widget.courseLessonId,'currentTime':currentTime}); 130 + String currentTime =
  131 + (_controller!.value.position.inMinutes.remainder(60) * 60 +
  132 + _controller!.value.position.inSeconds.remainder(60))
  133 + .toString();
  134 + popPage(data: {
  135 + 'courseLessonId': widget.courseLessonId,
  136 + 'currentTime': currentTime
  137 + });
103 } 138 }
104 } else if (type == OperationType.playState) { 139 } else if (type == OperationType.playState) {
105 if (_controller!.value.isPlaying) { 140 if (_controller!.value.isPlaying) {
@@ -107,9 +142,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -107,9 +142,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
107 } else { 142 } else {
108 _controller!.play(); 143 _controller!.play();
109 } 144 }
110 - setState(() {  
111 -  
112 - }); 145 + setState(() {});
113 } 146 }
114 } 147 }
115 148
@@ -118,7 +151,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -118,7 +151,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
118 super.initState(); 151 super.initState();
119 Uri uri = Uri.parse(widget.videoUrl); 152 Uri uri = Uri.parse(widget.videoUrl);
120 _controller = VideoPlayerController.networkUrl(uri) 153 _controller = VideoPlayerController.networkUrl(uri)
121 - ..initialize().then((_){ 154 + ..initialize().then((_) {
122 startTimer(); 155 startTimer();
123 setState(() { 156 setState(() {
124 _currentTime = formatDuration(_controller!.value.position); 157 _currentTime = formatDuration(_controller!.value.position);
@@ -133,82 +166,93 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -133,82 +166,93 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
133 166
134 @override 167 @override
135 Widget build(BuildContext context) { 168 Widget build(BuildContext context) {
136 - return GestureDetector(  
137 - onTap: () {  
138 - setState(() {  
139 - _hiddenTipView = !_hiddenTipView;  
140 - if(!_hiddenTipView) {  
141 - startTimer();  
142 - } else {  
143 - if (timerUtil!.isActive()) {  
144 - cancelTimer();  
145 - }  
146 - }  
147 - }); 169 + return BlocListener<LookVideoBloc, BaseSectionState>(
  170 + listener: (context, state) async {
  171 + if (state is SectionAgainState) {
  172 + await _controller?.seekTo(Duration.zero);
  173 + _controller?.play();
  174 + }
148 }, 175 },
149 - onDoubleTap: () {  
150 - if(_controller!.value.isInitialized) {  
151 - if (_controller!.value.isPlaying) {  
152 - _controller!.pause();  
153 - } else {  
154 - _controller!.play();  
155 - } 176 + child: GestureDetector(
  177 + onTap: () {
156 setState(() { 178 setState(() {
157 - 179 + _hiddenTipView = !_hiddenTipView;
  180 + if (!_hiddenTipView) {
  181 + startTimer();
  182 + } else {
  183 + if (timerUtil!.isActive()) {
  184 + cancelTimer();
  185 + }
  186 + }
158 }); 187 });
159 - }  
160 - },  
161 - child: Center(  
162 - child: _controller!.value.isInitialized ? Stack(  
163 - alignment: Alignment.center,  
164 - children: [  
165 - SizedBox(  
166 - height: double.infinity,  
167 - width: double.infinity,  
168 - child: AspectRatio(  
169 - aspectRatio: _controller!.value.aspectRatio,  
170 - child: VideoPlayer(_controller!),  
171 - ),  
172 - ),  
173 - Offstage(  
174 - offstage: _hiddenTipView,  
175 - child: VideoOperaWidget(  
176 - title: widget.typeTitle??'song',  
177 - degree: _playDegree,  
178 - totalTime: _totalTime,  
179 - currentTime: _currentTime,  
180 - isPlay: _controller!.value.isPlaying,  
181 - actionEvent: (OperationType type) {  
182 - actionType(type);  
183 - },  
184 - sliderChangeEvent: (double degree) {  
185 - int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60);  
186 - int positionSecond = (totalSecond * degree).toInt();  
187 - _controller!.seekTo(Duration(seconds: positionSecond));  
188 - }, 188 + },
  189 + onDoubleTap: () {
  190 + if (_controller!.value.isInitialized) {
  191 + if (_controller!.value.isPlaying) {
  192 + _controller!.pause();
  193 + } else {
  194 + _controller!.play();
  195 + }
  196 + setState(() {});
  197 + }
  198 + },
  199 + child: Center(
  200 + child: _controller!.value.isInitialized
  201 + ? Stack(
  202 + alignment: Alignment.center,
  203 + children: [
  204 + SizedBox(
  205 + height: double.infinity,
  206 + width: double.infinity,
  207 + child: AspectRatio(
  208 + aspectRatio: _controller!.value.aspectRatio,
  209 + child: VideoPlayer(_controller!),
  210 + ),
189 ), 211 ),
190 - ),  
191 - Offstage(  
192 - offstage: _controller!.value.isPlaying,  
193 - child: IconButton(  
194 - onPressed: () {  
195 - _controller!.play();  
196 - },  
197 - icon: Image.asset(  
198 - 'video_stop'.assetPng,  
199 - width: 70.w,  
200 - height: 70.h, 212 + Offstage(
  213 + offstage: _hiddenTipView,
  214 + child: VideoOperaWidget(
  215 + title: widget.typeTitle ?? 'song',
  216 + degree: _playDegree,
  217 + totalTime: _totalTime,
  218 + currentTime: _currentTime,
  219 + isPlay: _controller!.value.isPlaying,
  220 + actionEvent: (OperationType type) {
  221 + actionType(type);
  222 + },
  223 + sliderChangeEvent: (double degree) {
  224 + int totalSecond = _controller!
  225 + .value.duration.inMinutes
  226 + .remainder(60) *
  227 + 60 +
  228 + _controller!.value.duration.inSeconds
  229 + .remainder(60);
  230 + int positionSecond = (totalSecond * degree).toInt();
  231 + _controller!
  232 + .seekTo(Duration(seconds: positionSecond));
  233 + },
201 ), 234 ),
202 ), 235 ),
203 - )  
204 - ],  
205 - ): Container(  
206 - color: Colors.white,  
207 - child: Text(  
208 - '视频加载中....',  
209 - style: TextStyle(  
210 - fontSize: 20.sp,  
211 - color: Colors.black 236 + Offstage(
  237 + offstage: _controller!.value.isPlaying,
  238 + child: IconButton(
  239 + onPressed: () {
  240 + _controller!.play();
  241 + },
  242 + icon: Image.asset(
  243 + 'video_stop'.assetPng,
  244 + width: 70.w,
  245 + height: 70.h,
  246 + ),
  247 + ),
  248 + )
  249 + ],
  250 + )
  251 + : Container(
  252 + color: Colors.white,
  253 + child: Text(
  254 + '视频加载中....',
  255 + style: TextStyle(fontSize: 20.sp, color: Colors.black),
212 ), 256 ),
213 ), 257 ),
214 ), 258 ),