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 |