reason

add tiktok video

...@@ -48,6 +48,7 @@ android { ...@@ -48,6 +48,7 @@ android {
48 targetSdkVersion 30 48 targetSdkVersion 30
49 versionCode flutterVersionCode.toInteger() 49 versionCode flutterVersionCode.toInteger()
50 versionName flutterVersionName 50 versionName flutterVersionName
51 + multiDexEnabled true
51 } 52 }
52 53
53 buildTypes { 54 buildTypes {
......
...@@ -9,29 +9,25 @@ import 'package:one_poem/video/page/video_page.dart'; ...@@ -9,29 +9,25 @@ import 'package:one_poem/video/page/video_page.dart';
9 import 'package:one_poem/widgets/double_tap_back_exit_app.dart'; 9 import 'package:one_poem/widgets/double_tap_back_exit_app.dart';
10 import 'package:one_poem/widgets/load_image.dart'; 10 import 'package:one_poem/widgets/load_image.dart';
11 import 'package:provider/provider.dart'; 11 import 'package:provider/provider.dart';
12 - 12 +import 'package:flutter_gen/gen_l10n/one_poem_localizations.dart';
13 import 'provider/home_provider.dart'; 13 import 'provider/home_provider.dart';
14 14
15 class Home extends StatefulWidget { 15 class Home extends StatefulWidget {
16 -
17 const Home({Key? key}) : super(key: key); 16 const Home({Key? key}) : super(key: key);
18 17
19 @override 18 @override
20 _HomeState createState() => _HomeState(); 19 _HomeState createState() => _HomeState();
21 } 20 }
22 21
23 -class _HomeState extends State<Home> with RestorationMixin{ 22 +class _HomeState extends State<Home> with RestorationMixin {
24 -
25 static const double _imageSize = 25.0; 23 static const double _imageSize = 25.0;
26 24
27 late List<Widget> _pageList; 25 late List<Widget> _pageList;
28 - final List<String> _appBarTitles = ['订单', '商品', '统计', '视频','店铺'];
29 final PageController _pageController = PageController(); 26 final PageController _pageController = PageController();
30 27
31 HomeProvider provider = HomeProvider(); 28 HomeProvider provider = HomeProvider();
32 29
33 List<BottomNavigationBarItem>? _list; 30 List<BottomNavigationBarItem>? _list;
34 - List<BottomNavigationBarItem>? _listDark;
35 31
36 @override 32 @override
37 void initState() { 33 void initState() {
...@@ -49,36 +45,109 @@ class _HomeState extends State<Home> with RestorationMixin{ ...@@ -49,36 +45,109 @@ class _HomeState extends State<Home> with RestorationMixin{
49 _pageList = [ 45 _pageList = [
50 const OrderPage(), 46 const OrderPage(),
51 const GoodsPage(), 47 const GoodsPage(),
52 - const NotFoundPage(),//TODO
53 const VideoPage(), 48 const VideoPage(),
54 const ShopPage(), 49 const ShopPage(),
55 ]; 50 ];
56 } 51 }
57 52
58 - List<BottomNavigationBarItem> _buildBottomNavigationBarItem() { 53 + List<BottomNavigationBarItem> _buildBottomNavigationBarItem(
54 + BuildContext context) {
55 + final bool isDark = context.isDark;
56 + List<String> _appBarTitles = [
57 + OnePoemLocalizations.of(context).onePoemBottomNavigationBarItemTitle,
58 + OnePoemLocalizations.of(context).timelineBottomNavigationBarItemTitle,
59 + OnePoemLocalizations.of(context).categoryBottomNavigationBarItemTitle,
60 + OnePoemLocalizations.of(context).profileBottomNavigationBarItemTitle,
61 + ];
59 if (_list == null) { 62 if (_list == null) {
60 - const _tabImages = [ 63 + List<List<LoadAssetImage>> _tabImages;
61 - [ 64 + if (!isDark) {
62 - LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.unselected_item_color,), 65 + _tabImages = [
63 - LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.app_main,), 66 + [
64 - ], 67 + const LoadAssetImage(
65 - [ 68 + 'home/icon_order',
66 - LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.unselected_item_color,), 69 + width: _imageSize,
67 - LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.app_main,), 70 + color: Colours.unselected_item_color,
68 - ], 71 + ),
69 - [ 72 + const LoadAssetImage(
70 - LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.unselected_item_color,), 73 + 'home/icon_order',
71 - LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.app_main,), 74 + width: _imageSize,
72 - ], 75 + color: Colours.app_main,
73 - [ 76 + ),
74 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.unselected_item_color,), 77 + ],
75 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.app_main,), 78 + [
76 - ], 79 + const LoadAssetImage(
77 - [ 80 + 'home/icon_commodity',
78 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.unselected_item_color,), 81 + width: _imageSize,
79 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.app_main,), 82 + color: Colours.unselected_item_color,
80 - ] 83 + ),
81 - ]; 84 + const LoadAssetImage(
85 + 'home/icon_commodity',
86 + width: _imageSize,
87 + color: Colours.app_main,
88 + ),
89 + ],
90 + [
91 + const LoadAssetImage(
92 + 'home/icon_statistics',
93 + width: _imageSize,
94 + color: Colours.unselected_item_color,
95 + ),
96 + const LoadAssetImage(
97 + 'home/icon_statistics',
98 + width: _imageSize,
99 + color: Colours.app_main,
100 + ),
101 + ],
102 + [
103 + const LoadAssetImage(
104 + 'home/icon_shop',
105 + width: _imageSize,
106 + color: Colours.unselected_item_color,
107 + ),
108 + const LoadAssetImage(
109 + 'home/icon_shop',
110 + width: _imageSize,
111 + color: Colours.app_main,
112 + ),
113 + ]
114 + ];
115 + } else {
116 + _tabImages = [
117 + [
118 + const LoadAssetImage('home/icon_order', width: _imageSize),
119 + const LoadAssetImage(
120 + 'home/icon_order',
121 + width: _imageSize,
122 + color: Colours.dark_app_main,
123 + ),
124 + ],
125 + [
126 + const LoadAssetImage('home/icon_commodity', width: _imageSize),
127 + const LoadAssetImage(
128 + 'home/icon_commodity',
129 + width: _imageSize,
130 + color: Colours.dark_app_main,
131 + ),
132 + ],
133 + [
134 + const LoadAssetImage('home/icon_statistics', width: _imageSize),
135 + const LoadAssetImage(
136 + 'home/icon_statistics',
137 + width: _imageSize,
138 + color: Colours.dark_app_main,
139 + ),
140 + ],
141 + [
142 + const LoadAssetImage('home/icon_shop', width: _imageSize),
143 + const LoadAssetImage(
144 + 'home/icon_shop',
145 + width: _imageSize,
146 + color: Colours.dark_app_main,
147 + ),
148 + ]
149 + ];
150 + }
82 _list = List.generate(_tabImages.length, (i) { 151 _list = List.generate(_tabImages.length, (i) {
83 return BottomNavigationBarItem( 152 return BottomNavigationBarItem(
84 icon: _tabImages[i][0], 153 icon: _tabImages[i][0],
...@@ -90,42 +159,6 @@ class _HomeState extends State<Home> with RestorationMixin{ ...@@ -90,42 +159,6 @@ class _HomeState extends State<Home> with RestorationMixin{
90 return _list!; 159 return _list!;
91 } 160 }
92 161
93 - List<BottomNavigationBarItem> _buildDarkBottomNavigationBarItem() {
94 - if (_listDark == null) {
95 - const _tabImagesDark = [
96 - [
97 - LoadAssetImage('home/icon_order', width: _imageSize),
98 - LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.dark_app_main,),
99 - ],
100 - [
101 - LoadAssetImage('home/icon_commodity', width: _imageSize),
102 - LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.dark_app_main,),
103 - ],
104 - [
105 - LoadAssetImage('home/icon_statistics', width: _imageSize),
106 - LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.dark_app_main,),
107 - ],
108 - [
109 - LoadAssetImage('home/icon_shop', width: _imageSize),
110 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.dark_app_main,),
111 - ],
112 - [
113 - LoadAssetImage('home/icon_shop', width: _imageSize),
114 - LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.dark_app_main,),
115 - ]
116 - ];
117 -
118 - _listDark = List.generate(_tabImagesDark.length, (i) {
119 - return BottomNavigationBarItem(
120 - icon: _tabImagesDark[i][0],
121 - activeIcon: _tabImagesDark[i][1],
122 - label: _appBarTitles[i],
123 - );
124 - });
125 - }
126 - return _listDark!;
127 - }
128 -
129 @override 162 @override
130 Widget build(BuildContext context) { 163 Widget build(BuildContext context) {
131 final bool isDark = context.isDark; 164 final bool isDark = context.isDark;
...@@ -133,31 +166,32 @@ class _HomeState extends State<Home> with RestorationMixin{ ...@@ -133,31 +166,32 @@ class _HomeState extends State<Home> with RestorationMixin{
133 create: (_) => provider, 166 create: (_) => provider,
134 child: DoubleTapBackExitApp( 167 child: DoubleTapBackExitApp(
135 child: Scaffold( 168 child: Scaffold(
136 - bottomNavigationBar: Consumer<HomeProvider>( 169 + bottomNavigationBar: Consumer<HomeProvider>(
137 - builder: (_, provider, __) { 170 + builder: (_, provider, __) {
138 - return BottomNavigationBar( 171 + return BottomNavigationBar(
139 - backgroundColor: context.backgroundColor, 172 + backgroundColor: context.backgroundColor,
140 - items: isDark ? _buildDarkBottomNavigationBarItem() : _buildBottomNavigationBarItem(), 173 + items: _buildBottomNavigationBarItem(context),
141 - type: BottomNavigationBarType.fixed, 174 + type: BottomNavigationBarType.fixed,
142 - currentIndex: provider.value, 175 + currentIndex: provider.value,
143 - elevation: 5.0, 176 + elevation: 5.0,
144 - iconSize: 21.0, 177 + iconSize: 21.0,
145 - selectedFontSize: Dimens.font_sp10, 178 + selectedFontSize: Dimens.font_sp10,
146 - unselectedFontSize: Dimens.font_sp10, 179 + unselectedFontSize: Dimens.font_sp10,
147 - selectedItemColor: Theme.of(context).primaryColor, 180 + selectedItemColor: Theme.of(context).primaryColor,
148 - unselectedItemColor: isDark ? Colours.dark_unselected_item_color : Colours.unselected_item_color, 181 + unselectedItemColor: isDark
149 - onTap: (index) => _pageController.jumpToPage(index), 182 + ? Colours.dark_unselected_item_color
150 - ); 183 + : Colours.unselected_item_color,
151 - }, 184 + onTap: (index) => _pageController.jumpToPage(index),
152 - ), 185 + );
153 - // 使用PageView的原因参看 https://zhuanlan.zhihu.com/p/58582876 186 + },
154 - body: PageView( 187 + ),
155 - physics: const NeverScrollableScrollPhysics(), // 禁止滑动 188 + // 使用PageView的原因参看 https://zhuanlan.zhihu.com/p/58582876
156 - controller: _pageController, 189 + body: PageView(
157 - onPageChanged: (int index) => provider.value = index, 190 + physics: const NeverScrollableScrollPhysics(), // 禁止滑动
158 - children: _pageList, 191 + controller: _pageController,
159 - ) 192 + onPageChanged: (int index) => provider.value = index,
160 - ), 193 + children: _pageList,
194 + )),
161 ), 195 ),
162 ); 196 );
163 } 197 }
...@@ -169,5 +203,4 @@ class _HomeState extends State<Home> with RestorationMixin{ ...@@ -169,5 +203,4 @@ class _HomeState extends State<Home> with RestorationMixin{
169 void restoreState(RestorationBucket? oldBucket, bool initialRestore) { 203 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
170 registerForRestoration(provider, 'BottomNavigationBarCurrentIndex'); 204 registerForRestoration(provider, 'BottomNavigationBarCurrentIndex');
171 } 205 }
172 -
173 } 206 }
......
1 import 'package:flutter/cupertino.dart'; 1 import 'package:flutter/cupertino.dart';
2 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
3 +import 'package:one_poem/tiktok/controller/tiktok_video_list_controller.dart';
4 +import 'package:one_poem/tiktok/mock/video.dart';
5 +import 'package:one_poem/tiktok/style/physics.dart';
6 +import 'package:one_poem/tiktok/views/tiktok_header.dart';
7 +import 'package:one_poem/tiktok/views/tiktok_scaffold.dart';
8 +import 'package:one_poem/tiktok/views/tiktok_video.dart';
9 +import 'package:one_poem/tiktok/views/tiktok_video_button_column.dart';
10 +import 'package:video_player/video_player.dart';
3 11
4 class OrderPage extends StatefulWidget { 12 class OrderPage extends StatefulWidget {
5 const OrderPage({Key? key}) : super(key: key); 13 const OrderPage({Key? key}) : super(key: key);
...@@ -9,13 +17,12 @@ class OrderPage extends StatefulWidget { ...@@ -9,13 +17,12 @@ class OrderPage extends StatefulWidget {
9 } 17 }
10 18
11 class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver { 19 class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
12 - TikTokPageTag tabBarType = TikTokPageTag.home;
13 20
14 TikTokScaffoldController tkController = TikTokScaffoldController(); 21 TikTokScaffoldController tkController = TikTokScaffoldController();
15 22
16 - PageController _pageController = PageController(); 23 + final PageController _pageController = PageController();
17 24
18 - TikTokVideoListController _videoListController = TikTokVideoListController(); 25 + final TikTokVideoListController _videoListController = TikTokVideoListController();
19 26
20 /// 记录点赞 27 /// 记录点赞
21 Map<int, bool> favoriteMap = {}; 28 Map<int, bool> favoriteMap = {};
...@@ -45,19 +52,19 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver { ...@@ -45,19 +52,19 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
45 initialList: videoDataList 52 initialList: videoDataList
46 .map( 53 .map(
47 (e) => VPVideoController( 54 (e) => VPVideoController(
48 - videoInfo: e, 55 + videoInfo: e,
49 - builder: () => VideoPlayerController.network(e.url), 56 + builder: () => VideoPlayerController.network(e.url),
50 - ), 57 + ),
51 - ) 58 + )
52 .toList(), 59 .toList(),
53 videoProvider: (int index, List<VPVideoController> list) async { 60 videoProvider: (int index, List<VPVideoController> list) async {
54 return videoDataList 61 return videoDataList
55 .map( 62 .map(
56 (e) => VPVideoController( 63 (e) => VPVideoController(
57 - videoInfo: e, 64 + videoInfo: e,
58 - builder: () => VideoPlayerController.network(e.url), 65 + builder: () => VideoPlayerController.network(e.url),
59 - ), 66 + ),
60 - ) 67 + )
61 .toList(); 68 .toList();
62 }, 69 },
63 ); 70 );
...@@ -65,7 +72,7 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver { ...@@ -65,7 +72,7 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
65 setState(() {}); 72 setState(() {});
66 }); 73 });
67 tkController.addListener( 74 tkController.addListener(
68 - () { 75 + () {
69 if (tkController.value == TikTokPagePositon.middle) { 76 if (tkController.value == TikTokPagePositon.middle) {
70 _videoListController.currentPlayer.play(); 77 _videoListController.currentPlayer.play();
71 } else { 78 } else {
...@@ -80,114 +87,44 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver { ...@@ -80,114 +87,44 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
80 @override 87 @override
81 Widget build(BuildContext context) { 88 Widget build(BuildContext context) {
82 Widget? currentPage; 89 Widget? currentPage;
83 -
84 - switch (tabBarType) {
85 - case TikTokPageTag.home:
86 - break;
87 - case TikTokPageTag.follow:
88 - currentPage = FollowPage();
89 - break;
90 - case TikTokPageTag.msg:
91 - currentPage = MsgPage();
92 - break;
93 - case TikTokPageTag.me:
94 - currentPage = UserPage(isSelfPage: true);
95 - break;
96 - }
97 double a = MediaQuery.of(context).size.aspectRatio; 90 double a = MediaQuery.of(context).size.aspectRatio;
98 bool hasBottomPadding = a < 0.55; 91 bool hasBottomPadding = a < 0.55;
99 92
100 - bool hasBackground = hasBottomPadding; 93 + var header = TikTokHeader(
101 - hasBackground = tabBarType != TikTokPageTag.home;
102 - if (hasBottomPadding) {
103 - hasBackground = true;
104 - }
105 - Widget tikTokTabBar = TikTokTabBar(
106 - hasBackground: hasBackground,
107 - current: tabBarType,
108 - onTabSwitch: (type) async {
109 - setState(() {
110 - tabBarType = type;
111 - if (type == TikTokPageTag.home) {
112 - _videoListController.currentPlayer.play();
113 - } else {
114 - _videoListController.currentPlayer.pause();
115 - }
116 - });
117 - },
118 - onAddButton: () {
119 - Navigator.of(context).push(
120 - MaterialPageRoute(
121 - fullscreenDialog: true,
122 - builder: (context) => CameraPage(),
123 - ),
124 - );
125 - },
126 - );
127 -
128 - var userPage = UserPage(
129 - isSelfPage: false,
130 - canPop: true,
131 - onPop: () {
132 - tkController.animateToMiddle();
133 - },
134 - );
135 - var searchPage = SearchPage(
136 - onPop: tkController.animateToMiddle,
137 - );
138 -
139 - var header = tabBarType == TikTokPageTag.home
140 - ? TikTokHeader(
141 onSearch: () { 94 onSearch: () {
142 tkController.animateToLeft(); 95 tkController.animateToLeft();
143 }, 96 },
144 - ) 97 + );
145 - : Container();
146 98
147 // 组合 99 // 组合
148 return TikTokScaffold( 100 return TikTokScaffold(
149 controller: tkController, 101 controller: tkController,
150 - hasBottomPadding: hasBackground,
151 - tabBar: tikTokTabBar,
152 header: header, 102 header: header,
153 - leftPage: searchPage, 103 + enableGesture: true,
154 - rightPage: userPage,
155 - enableGesture: tabBarType == TikTokPageTag.home,
156 - // onPullDownRefresh: _fetchData,
157 page: Stack( 104 page: Stack(
158 // index: currentPage == null ? 0 : 1, 105 // index: currentPage == null ? 0 : 1,
159 children: <Widget>[ 106 children: <Widget>[
160 PageView.builder( 107 PageView.builder(
161 - key: Key('home'), 108 + key: const Key('home'),
162 - physics: QuickerScrollPhysics(), 109 + physics: const QuickerScrollPhysics(),
163 controller: _pageController, 110 controller: _pageController,
164 scrollDirection: Axis.vertical, 111 scrollDirection: Axis.vertical,
165 itemCount: _videoListController.videoCount, 112 itemCount: _videoListController.videoCount,
166 itemBuilder: (context, i) { 113 itemBuilder: (context, i) {
167 // 拼一个视频组件出来 114 // 拼一个视频组件出来
168 - bool isF = SafeMap(favoriteMap)[i].boolean ?? false;
169 var player = _videoListController.playerOfIndex(i)!; 115 var player = _videoListController.playerOfIndex(i)!;
170 var data = player.videoInfo!; 116 var data = player.videoInfo!;
171 // 右侧按钮列 117 // 右侧按钮列
172 Widget buttons = TikTokButtonColumn( 118 Widget buttons = TikTokButtonColumn(
173 - isFavorite: isF, 119 + isFavorite: false,
174 onAvatar: () { 120 onAvatar: () {
175 tkController.animateToPage(TikTokPagePositon.right); 121 tkController.animateToPage(TikTokPagePositon.right);
176 }, 122 },
177 onFavorite: () { 123 onFavorite: () {
178 setState(() { 124 setState(() {
179 - favoriteMap[i] = !isF;
180 }); 125 });
181 // showAboutDialog(context: context); 126 // showAboutDialog(context: context);
182 }, 127 },
183 - onComment: () {
184 - CustomBottomSheet.showModalBottomSheet(
185 - backgroundColor: Colors.white.withOpacity(0),
186 - context: context,
187 - builder: (BuildContext context) =>
188 - TikTokCommentBottomSheet(),
189 - );
190 - },
191 onShare: () {}, 128 onShare: () {},
192 ); 129 );
193 // video 130 // video
......
1 +import 'dart:async';
2 +
3 +import 'package:flutter/material.dart';
4 +import 'package:one_poem/tiktok/mock/video.dart';
5 +import 'package:video_player/video_player.dart';
6 +
7 +typedef LoadMoreVideo = Future<List<VPVideoController>> Function(
8 + int index,
9 + List<VPVideoController> list,
10 +);
11 +
12 +/// TikTokVideoListController是一系列视频的控制器,内部管理了视频控制器数组
13 +/// 提供了预加载/释放/加载更多功能
14 +class TikTokVideoListController extends ChangeNotifier {
15 + TikTokVideoListController({
16 + this.loadMoreCount = 1,
17 + this.preloadCount = 3,
18 + this.disposeCount = 5,
19 + });
20 +
21 + /// 到第几个触发预加载,例如:1:最后一个,2:倒数第二个
22 + final int loadMoreCount;
23 +
24 + /// 预加载多少个视频
25 + final int preloadCount;
26 +
27 + /// 超出多少个,就释放视频
28 + final int disposeCount;
29 +
30 + /// 提供视频的builder
31 + LoadMoreVideo? _videoProvider;
32 +
33 + loadIndex(int target, {bool reload = false}) {
34 + if (!reload) {
35 + if (index.value == target) return;
36 + }
37 + // 播放当前的,暂停其他的
38 + var oldIndex = index.value;
39 + var newIndex = target;
40 +
41 + // 暂停之前的视频
42 + if (!(oldIndex == 0 && newIndex == 0)) {
43 + playerOfIndex(oldIndex)?.controller.seekTo(Duration.zero);
44 + // playerOfIndex(oldIndex)?.controller.addListener(_didUpdateValue);
45 + // playerOfIndex(oldIndex)?.showPauseIcon.addListener(_didUpdateValue);
46 + playerOfIndex(oldIndex)?.pause();
47 + print('暂停$oldIndex');
48 + }
49 + // 开始播放当前的视频
50 + playerOfIndex(newIndex)?.controller.addListener(_didUpdateValue);
51 + playerOfIndex(newIndex)?.showPauseIcon.addListener(_didUpdateValue);
52 + playerOfIndex(newIndex)?.play();
53 + print('播放$newIndex');
54 + // 处理预加载/释放内存
55 + for (var i = 0; i < playerList.length; i++) {
56 + // 需要释放[disposeCount]之前的视频
57 + if (i < newIndex - disposeCount || i > newIndex + disposeCount) {
58 + print('释放$i');
59 + playerOfIndex(i)?.controller.removeListener(_didUpdateValue);
60 + playerOfIndex(i)?.showPauseIcon.removeListener(_didUpdateValue);
61 + playerOfIndex(i)?.dispose();
62 + } else {
63 + // 需要预加载
64 + if (i > newIndex && i < newIndex + preloadCount) {
65 + print('预加载$i');
66 + playerOfIndex(i)?.init();
67 + }
68 + }
69 + }
70 + // 快到最底部,添加更多视频
71 + if (playerList.length - newIndex <= loadMoreCount + 1) {
72 + _videoProvider?.call(newIndex, playerList).then(
73 + (list) async {
74 + playerList.addAll(list);
75 + notifyListeners();
76 + },
77 + );
78 + }
79 +
80 + // 完成
81 + index.value = target;
82 + }
83 +
84 + _didUpdateValue() {
85 + notifyListeners();
86 + }
87 +
88 + /// 获取指定index的player
89 + VPVideoController? playerOfIndex(int index) {
90 + if (index < 0 || index > playerList.length - 1) {
91 + return null;
92 + }
93 + return playerList[index];
94 + }
95 +
96 + /// 视频总数目
97 + int get videoCount => playerList.length;
98 +
99 + /// 初始化
100 + init({
101 + required PageController pageController,
102 + required List<VPVideoController> initialList,
103 + required LoadMoreVideo videoProvider,
104 + }) async {
105 + playerList.addAll(initialList);
106 + _videoProvider = videoProvider;
107 + pageController.addListener(() {
108 + var p = pageController.page!;
109 + if (p % 1 == 0) {
110 + loadIndex(p ~/ 1);
111 + }
112 + });
113 + loadIndex(0, reload: true);
114 + notifyListeners();
115 + }
116 +
117 + /// 目前的视频序号
118 + ValueNotifier<int> index = ValueNotifier<int>(0);
119 +
120 + /// 视频列表
121 + List<VPVideoController> playerList = [];
122 +
123 + ///
124 + VPVideoController get currentPlayer => playerList[index.value];
125 +
126 + /// 销毁全部
127 + void dispose() {
128 + // 销毁全部
129 + for (var player in playerList) {
130 + player.showPauseIcon.dispose();
131 + player.dispose();
132 + }
133 + playerList = [];
134 + super.dispose();
135 + }
136 +}
137 +
138 +typedef ControllerSetter<T> = Future<void> Function(T controller);
139 +typedef ControllerBuilder<T> = T Function();
140 +
141 +/// 抽象类,作为视频控制器必须实现这些方法
142 +abstract class TikTokVideoController<T> {
143 + /// 获取当前的控制器实例
144 + T? get controller;
145 +
146 + /// 是否显示暂停按钮
147 + ValueNotifier<bool> get showPauseIcon;
148 +
149 + /// 加载视频,在init后,应当开始下载视频内容
150 + Future<void> init({ControllerSetter<T>? afterInit});
151 +
152 + /// 视频销毁,在dispose后,应当释放任何内存资源
153 + Future<void> dispose();
154 +
155 + /// 播放
156 + Future<void> play();
157 +
158 + /// 暂停
159 + Future<void> pause({bool showPauseIcon: false});
160 +}
161 +
162 +class VPVideoController extends TikTokVideoController<VideoPlayerController> {
163 + VideoPlayerController? _controller;
164 + ValueNotifier<bool> _showPauseIcon = ValueNotifier<bool>(false);
165 +
166 + final UserVideo? videoInfo;
167 +
168 + final ControllerBuilder<VideoPlayerController> _builder;
169 + final ControllerSetter<VideoPlayerController>? _afterInit;
170 + VPVideoController({
171 + this.videoInfo,
172 + required ControllerBuilder<VideoPlayerController> builder,
173 + ControllerSetter<VideoPlayerController>? afterInit,
174 + }) : this._builder = builder,
175 + this._afterInit = afterInit;
176 +
177 + @override
178 + VideoPlayerController get controller {
179 + if (_controller == null) {
180 + _controller = _builder.call();
181 + }
182 + return _controller!;
183 + }
184 +
185 + /// 阻止在init的时候dispose,或者在dispose前init
186 + List<Future> _actLocks = [];
187 +
188 + bool get isDispose => _disposeLock != null;
189 + bool get prepared => _prepared;
190 + bool _prepared = false;
191 +
192 + Completer<void>? _disposeLock;
193 +
194 + @override
195 + Future<void> dispose() async {
196 + if (!prepared) return;
197 + await Future.wait(_actLocks);
198 + _actLocks.clear();
199 + var completer = Completer<void>();
200 + _actLocks.add(completer.future);
201 + _prepared = false;
202 + await this.controller.dispose();
203 + _controller = null;
204 + _disposeLock = Completer<void>();
205 + completer.complete();
206 + }
207 +
208 + @override
209 + Future<void> init({
210 + ControllerSetter<VideoPlayerController>? afterInit,
211 + }) async {
212 + if (prepared) return;
213 + await Future.wait(_actLocks);
214 + _actLocks.clear();
215 + var completer = Completer<void>();
216 + _actLocks.add(completer.future);
217 + await this.controller.initialize();
218 + await this.controller.setLooping(true);
219 + afterInit ??= this._afterInit;
220 + await afterInit?.call(this.controller);
221 + _prepared = true;
222 + completer.complete();
223 + if (_disposeLock != null) {
224 + _disposeLock?.complete();
225 + _disposeLock = null;
226 + }
227 + }
228 +
229 + @override
230 + Future<void> pause({bool showPauseIcon: false}) async {
231 + await Future.wait(_actLocks);
232 + _actLocks.clear();
233 + await init();
234 + if (!prepared) return;
235 + if (_disposeLock != null) {
236 + await _disposeLock?.future;
237 + }
238 + await this.controller.pause();
239 + _showPauseIcon.value = true;
240 + }
241 +
242 + @override
243 + Future<void> play() async {
244 + await Future.wait(_actLocks);
245 + _actLocks.clear();
246 + await init();
247 + if (!prepared) return;
248 + if (_disposeLock != null) {
249 + await _disposeLock?.future;
250 + }
251 + await this.controller.play();
252 + _showPauseIcon.value = false;
253 + }
254 +
255 + @override
256 + ValueNotifier<bool> get showPauseIcon => _showPauseIcon;
257 +}
1 +import 'dart:io';
2 +
3 +Socket? socket;
4 +var videoList = [
5 + 'test-video-10.MP4',
6 + 'test-video-6.mp4',
7 + 'test-video-9.MP4',
8 + 'test-video-8.MP4',
9 + 'test-video-7.MP4',
10 + 'test-video-1.mp4',
11 + 'test-video-2.mp4',
12 + 'test-video-3.mp4',
13 + 'test-video-4.mp4',
14 +];
15 +
16 +class UserVideo {
17 + final String url;
18 + final String image;
19 + final String? desc;
20 +
21 + UserVideo({
22 + required this.url,
23 + required this.image,
24 + this.desc,
25 + });
26 +
27 + static List<UserVideo> fetchVideo() {
28 + List<UserVideo> list = videoList
29 + .map((e) => UserVideo(image: '', url: 'https://static.ybhospital.net/$e', desc: 'test_video_desc'))
30 + .toList();
31 + return list;
32 + }
33 +
34 + @override
35 + String toString() {
36 + return 'image:$image' '\nvideo:$url';
37 + }
38 +}
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter/physics.dart';
3 +
4 +class QuickerScrollPhysics extends BouncingScrollPhysics {
5 + const QuickerScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);
6 +
7 + @override
8 + QuickerScrollPhysics applyTo(ScrollPhysics? ancestor) {
9 + return QuickerScrollPhysics(parent: buildParent(ancestor));
10 + }
11 +
12 + @override
13 + SpringDescription get spring => SpringDescription.withDampingRatio(
14 + mass: 0.2,
15 + stiffness: 300.0,
16 + ratio: 1.1,
17 + );
18 +}
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter/rendering.dart';
3 +
4 +class SysSize {
5 + static const double avatar = 56;
6 + // static const double iconBig = 40;
7 + static const double iconNormal = 24;
8 + // static const double big = 18;
9 + // static const double normal = 16;
10 + // static const double small = 12;
11 + static const double iconBig = 40;
12 + static const double big = 16;
13 + static const double normal = 14;
14 + static const double small = 12;
15 +}
16 +
17 +class StandardTextStyle {
18 + static const TextStyle big = TextStyle(
19 + fontWeight: FontWeight.w600,
20 + fontSize: SysSize.big,
21 + inherit: true,
22 + );
23 + static const TextStyle bigWithOpacity = TextStyle(
24 + color: Color.fromRGBO(0xff, 0xff, 0xff, .66),
25 + fontWeight: FontWeight.w600,
26 + fontSize: SysSize.big,
27 + inherit: true,
28 + );
29 + static const TextStyle normalW = TextStyle(
30 + fontWeight: FontWeight.w600,
31 + fontSize: SysSize.normal,
32 + inherit: true,
33 + );
34 + static const TextStyle normal = TextStyle(
35 + fontWeight: FontWeight.normal,
36 + fontSize: SysSize.normal,
37 + inherit: true,
38 + );
39 + static const TextStyle normalWithOpacity = const TextStyle(
40 + color: const Color.fromRGBO(0xff, 0xff, 0xff, .66),
41 + fontWeight: FontWeight.normal,
42 + fontSize: SysSize.normal,
43 + inherit: true,
44 + );
45 + static const TextStyle small = const TextStyle(
46 + fontWeight: FontWeight.normal,
47 + fontSize: SysSize.small,
48 + inherit: true,
49 + );
50 + static const TextStyle smallWithOpacity = const TextStyle(
51 + color: const Color.fromRGBO(0xff, 0xff, 0xff, .66),
52 + fontWeight: FontWeight.normal,
53 + fontSize: SysSize.small,
54 + inherit: true,
55 + );
56 +}
57 +
58 +class ColorPlate {
59 + // 配色
60 + static const Color orange = const Color(0xffFFC459);
61 + static const Color yellow = const Color(0xffF1E300);
62 + static const Color green = const Color(0xff7ED321);
63 + static const Color red = const Color(0xffEB3838);
64 + static const Color darkGray = const Color(0xff4A4A4A);
65 + static const Color gray = const Color(0xff9b9b9b);
66 + static const Color lightGray = const Color(0xfff5f5f4);
67 + static const Color black = const Color(0xff000000);
68 + static const Color white = const Color(0xffffffff);
69 + static const Color clear = const Color(0);
70 +
71 + /// 深色背景
72 + static const Color back1 = const Color(0xff1D1F22);
73 +
74 + /// 比深色背景略深一点
75 + static const Color back2 = const Color(0xff121314);
76 +}
1 +import 'package:flutter/material.dart';
2 +import 'package:one_poem/tiktok/style/style.dart';
3 +
4 +class SelectText extends StatelessWidget {
5 + const SelectText({
6 + Key? key,
7 + this.isSelect: true,
8 + this.title,
9 + }) : super(key: key);
10 +
11 + final bool isSelect;
12 + final String? title;
13 +
14 + @override
15 + Widget build(BuildContext context) {
16 + return Container(
17 + padding: EdgeInsets.symmetric(vertical: 12),
18 + color: Colors.black.withOpacity(0),
19 + child: Text(
20 + title ?? '??',
21 + textAlign: TextAlign.center,
22 + style:
23 + isSelect ? StandardTextStyle.big : StandardTextStyle.bigWithOpacity,
24 + ),
25 + );
26 + }
27 +}
1 +import 'package:flutter/material.dart';
2 +import 'package:tapped/tapped.dart';
3 +
4 +import 'select_text.dart';
5 +
6 +class TikTokHeader extends StatefulWidget {
7 + final Function? onSearch;
8 + const TikTokHeader({
9 + Key? key,
10 + this.onSearch,
11 + }) : super(key: key);
12 +
13 + @override
14 + _TikTokHeaderState createState() => _TikTokHeaderState();
15 +}
16 +
17 +class _TikTokHeaderState extends State<TikTokHeader> {
18 + int currentSelect = 0;
19 + @override
20 + Widget build(BuildContext context) {
21 + List<String> list = ['推荐', '本地'];
22 + List<Widget> headList = [];
23 + for (var i = 0; i < list.length; i++) {
24 + headList.add(Expanded(
25 + child: GestureDetector(
26 + child: SelectText(
27 + title: list[i],
28 + isSelect: i == currentSelect,
29 + ),
30 + onTap: () {
31 + setState(() {
32 + currentSelect = i;
33 + });
34 + },
35 + ),
36 + ));
37 + }
38 + Widget headSwitch = Row(
39 + children: headList,
40 + );
41 + return Container(
42 + // color: Colors.black.withOpacity(0.3),
43 + padding: const EdgeInsets.symmetric(horizontal: 16),
44 + child: Row(
45 + mainAxisAlignment: MainAxisAlignment.center,
46 + children: <Widget>[
47 + Expanded(
48 + child: Tapped(
49 + child: Container(
50 + color: Colors.black.withOpacity(0),
51 + padding: const EdgeInsets.all(4),
52 + alignment: Alignment.centerLeft,
53 + child: Icon(
54 + Icons.search,
55 + color: Colors.white.withOpacity(0.66),
56 + ),
57 + ),
58 + onTap: widget.onSearch,
59 + ),
60 + ),
61 + Expanded(
62 + flex: 1,
63 + child: Container(
64 + color: Colors.black.withOpacity(0),
65 + alignment: Alignment.center,
66 + child: headSwitch,
67 + ),
68 + ),
69 + Expanded(
70 + child: Tapped(
71 + child: Container(
72 + color: Colors.black.withOpacity(0),
73 + padding: const EdgeInsets.all(4),
74 + alignment: Alignment.centerRight,
75 + child: Icon(
76 + Icons.tv,
77 + color: Colors.white.withOpacity(0.66),
78 + ),
79 + ),
80 + ),
81 + ),
82 + ],
83 + ),
84 + );
85 + }
86 +}
This diff is collapsed. Click to expand it.
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_spinkit/flutter_spinkit.dart';
3 +import 'package:one_poem/tiktok/style/style.dart';
4 +import 'package:one_poem/tiktok/views/tiktok_video_gesture.dart';
5 +
6 +///
7 +/// TikTok风格的一个视频页组件,覆盖在video上,提供以下功能:
8 +/// 播放按钮的遮罩
9 +/// 单击事件
10 +/// 点赞事件回调(每次)
11 +/// 长宽比控制
12 +/// 底部padding(用于适配有沉浸式底部状态栏时)
13 +///
14 +class TikTokVideoPage extends StatelessWidget {
15 + final Widget? video;
16 + final double aspectRatio;
17 + final String? tag;
18 + final double bottomPadding;
19 +
20 + final Widget? rightButtonColumn;
21 + final Widget? userInfoWidget;
22 +
23 + final bool hidePauseIcon;
24 +
25 + final Function? onAddFavorite;
26 + final Function? onSingleTap;
27 +
28 + const TikTokVideoPage({
29 + Key? key,
30 + this.bottomPadding: 16,
31 + this.tag,
32 + this.rightButtonColumn,
33 + this.userInfoWidget,
34 + this.onAddFavorite,
35 + this.onSingleTap,
36 + this.video,
37 + this.aspectRatio: 9 / 16.0,
38 + this.hidePauseIcon: false,
39 + }) : super(key: key);
40 + @override
41 + Widget build(BuildContext context) {
42 + // 右边的按钮列表
43 + Widget rightButtons = rightButtonColumn ?? Container();
44 + // 用户信息
45 + Widget userInfo = userInfoWidget ??
46 + VideoUserInfo(
47 + bottomPadding: bottomPadding,
48 + );
49 + // 视频加载的动画
50 + // Widget videoLoading = VideoLoadingPlaceHolder(tag: tag);
51 + // 视频播放页
52 + Widget videoContainer = Stack(
53 + children: <Widget>[
54 + Container(
55 + height: double.infinity,
56 + width: double.infinity,
57 + color: Colors.black,
58 + alignment: Alignment.center,
59 + child: AspectRatio(
60 + aspectRatio: aspectRatio,
61 + child: video,
62 + ),
63 + ),
64 + TikTokVideoGesture(
65 + onAddFavorite: onAddFavorite,
66 + onSingleTap: onSingleTap,
67 + child: Container(
68 + color: ColorPlate.clear,
69 + height: double.infinity,
70 + width: double.infinity,
71 + ),
72 + ),
73 + hidePauseIcon
74 + ? Container()
75 + : Container(
76 + height: double.infinity,
77 + width: double.infinity,
78 + alignment: Alignment.center,
79 + child: Icon(
80 + Icons.play_circle_outline,
81 + size: 120,
82 + color: Colors.white.withOpacity(0.4),
83 + ),
84 + ),
85 + ],
86 + );
87 + Widget body = Stack(
88 + children: <Widget>[
89 + videoContainer,
90 + Container(
91 + height: double.infinity,
92 + width: double.infinity,
93 + alignment: Alignment.bottomRight,
94 + child: rightButtons,
95 + ),
96 + Container(
97 + height: double.infinity,
98 + width: double.infinity,
99 + alignment: Alignment.bottomLeft,
100 + child: userInfo,
101 + ),
102 + ],
103 + );
104 + return body;
105 + }
106 +}
107 +
108 +class VideoLoadingPlaceHolder extends StatelessWidget {
109 + const VideoLoadingPlaceHolder({
110 + Key? key,
111 + required this.tag,
112 + }) : super(key: key);
113 +
114 + final String tag;
115 +
116 + @override
117 + Widget build(BuildContext context) {
118 + return Container(
119 + decoration: const BoxDecoration(
120 + gradient: LinearGradient(
121 + begin: Alignment.topCenter,
122 + colors: <Color>[
123 + Colors.blue,
124 + Colors.green,
125 + ],
126 + ),
127 + ),
128 + child: Column(
129 + mainAxisAlignment: MainAxisAlignment.center,
130 + children: <Widget>[
131 + SpinKitWave(
132 + size: 36,
133 + color: Colors.white.withOpacity(0.3),
134 + ),
135 + Container(
136 + padding: const EdgeInsets.all(50),
137 + child: Text(
138 + tag,
139 + style: StandardTextStyle.normalWithOpacity,
140 + ),
141 + ),
142 + ],
143 + ),
144 + );
145 + }
146 +}
147 +
148 +class VideoUserInfo extends StatelessWidget {
149 + final String? desc;
150 + // final Function onGoodGift;
151 + const VideoUserInfo({
152 + Key? key,
153 + required this.bottomPadding,
154 + // @required this.onGoodGift,
155 + this.desc,
156 + }) : super(key: key);
157 +
158 + final double bottomPadding;
159 +
160 + @override
161 + Widget build(BuildContext context) {
162 + return Container(
163 + padding: EdgeInsets.only(
164 + left: 12,
165 + bottom: bottomPadding,
166 + ),
167 + margin: const EdgeInsets.only(right: 80),
168 + child: Column(
169 + mainAxisAlignment: MainAxisAlignment.end,
170 + crossAxisAlignment: CrossAxisAlignment.start,
171 + children: <Widget>[
172 + const Text(
173 + '@朱二旦的枯燥生活',
174 + style: StandardTextStyle.big,
175 + ),
176 + Container(height: 6),
177 + Text(
178 + desc ?? '#原创 有钱人的生活就是这么朴实无华,且枯燥 #短视频',
179 + style: StandardTextStyle.normal,
180 + ),
181 + Container(height: 6),
182 + Row(
183 + children: const <Widget>[
184 + Icon(Icons.music_note, size: 14),
185 + Expanded(
186 + child: Text(
187 + '朱二旦的枯燥生活创作的原声',
188 + maxLines: 9,
189 + style: StandardTextStyle.normal,
190 + ),
191 + )
192 + ],
193 + )
194 + ],
195 + ),
196 + );
197 + }
198 +}
1 +import 'package:flutter/material.dart';
2 +import 'package:one_poem/tiktok/style/style.dart';
3 +import 'package:tapped/tapped.dart';
4 +
5 +class TikTokButtonColumn extends StatelessWidget {
6 + final double? bottomPadding;
7 + final bool isFavorite;
8 + final Function? onFavorite;
9 + final Function? onComment;
10 + final Function? onShare;
11 + final Function? onAvatar;
12 + const TikTokButtonColumn({
13 + Key? key,
14 + this.bottomPadding,
15 + this.onFavorite,
16 + this.onComment,
17 + this.onShare,
18 + this.isFavorite: false,
19 + this.onAvatar,
20 + }) : super(key: key);
21 +
22 + @override
23 + Widget build(BuildContext context) {
24 + return Container(
25 + width: SysSize.avatar,
26 + margin: EdgeInsets.only(
27 + bottom: bottomPadding ?? 50,
28 + right: 12,
29 + ),
30 + child: Column(
31 + mainAxisAlignment: MainAxisAlignment.end,
32 + crossAxisAlignment: CrossAxisAlignment.end,
33 + children: <Widget>[
34 + Tapped(
35 + child: TikTokAvatar(),
36 + onTap: onAvatar,
37 + ),
38 + FavoriteIcon(
39 + onFavorite: onFavorite,
40 + isFavorite: isFavorite,
41 + ),
42 + _IconButton(
43 + icon: const IconToText(Icons.mode_comment, size: SysSize.iconBig - 4),
44 + text: '4213',
45 + onTap: onComment,
46 + ),
47 + _IconButton(
48 + icon: const IconToText(Icons.share, size: SysSize.iconBig),
49 + text: '346',
50 + onTap: onShare,
51 + ),
52 + Container(
53 + width: SysSize.avatar,
54 + height: SysSize.avatar,
55 + margin: const EdgeInsets.only(top: 10),
56 + decoration: BoxDecoration(
57 + borderRadius: BorderRadius.circular(SysSize.avatar / 2.0),
58 + // color: Colors.black.withOpacity(0.8),
59 + ),
60 + )
61 + ],
62 + ),
63 + );
64 + }
65 +}
66 +
67 +class FavoriteIcon extends StatelessWidget {
68 + const FavoriteIcon({
69 + Key? key,
70 + required this.onFavorite,
71 + this.isFavorite,
72 + }) : super(key: key);
73 + final bool? isFavorite;
74 + final Function? onFavorite;
75 +
76 + @override
77 + Widget build(BuildContext context) {
78 + return _IconButton(
79 + icon: IconToText(
80 + Icons.favorite,
81 + size: SysSize.iconBig,
82 + color: isFavorite! ? ColorPlate.red : null,
83 + ),
84 + text: '1.0w',
85 + onTap: onFavorite,
86 + );
87 + }
88 +}
89 +
90 +class TikTokAvatar extends StatelessWidget {
91 + const TikTokAvatar({
92 + Key? key,
93 + }) : super(key: key);
94 +
95 + @override
96 + Widget build(BuildContext context) {
97 + Widget avatar = Container(
98 + width: SysSize.avatar,
99 + height: SysSize.avatar,
100 + margin: EdgeInsets.only(bottom: 10),
101 + decoration: BoxDecoration(
102 + border: Border.all(
103 + color: Colors.white,
104 + width: 1,
105 + ),
106 + borderRadius: BorderRadius.circular(SysSize.avatar / 2.0),
107 + color: Colors.orange,
108 + ),
109 + child: ClipOval(
110 + child: Image.network(
111 + "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
112 + fit: BoxFit.cover,
113 + ),
114 + ),
115 + );
116 + Widget addButton = Container(
117 + width: 20,
118 + height: 20,
119 + decoration: BoxDecoration(
120 + borderRadius: BorderRadius.circular(10),
121 + color: ColorPlate.orange,
122 + ),
123 + child: Icon(
124 + Icons.add,
125 + size: 16,
126 + ),
127 + );
128 + return Container(
129 + width: SysSize.avatar,
130 + height: 66,
131 + margin: EdgeInsets.only(bottom: 6),
132 + child: Stack(
133 + alignment: Alignment.bottomCenter,
134 + children: <Widget>[avatar, addButton],
135 + ),
136 + );
137 + }
138 +}
139 +
140 +/// 把IconData转换为文字,使其可以使用文字样式
141 +class IconToText extends StatelessWidget {
142 + final IconData? icon;
143 + final TextStyle? style;
144 + final double? size;
145 + final Color? color;
146 +
147 + const IconToText(
148 + this.icon, {
149 + Key? key,
150 + this.style,
151 + this.size,
152 + this.color,
153 + }) : super(key: key);
154 + @override
155 + Widget build(BuildContext context) {
156 + return Text(
157 + String.fromCharCode(icon!.codePoint),
158 + style: style ??
159 + TextStyle(
160 + fontFamily: 'MaterialIcons',
161 + fontSize: size ?? 30,
162 + inherit: true,
163 + color: color ?? ColorPlate.white,
164 + ),
165 + );
166 + }
167 +}
168 +
169 +class _IconButton extends StatelessWidget {
170 + final Widget? icon;
171 + final String? text;
172 + final Function? onTap;
173 + const _IconButton({
174 + Key? key,
175 + this.icon,
176 + this.text,
177 + this.onTap,
178 + }) : super(key: key);
179 +
180 + @override
181 + Widget build(BuildContext context) {
182 + var shadowStyle = TextStyle(
183 + shadows: [
184 + Shadow(
185 + color: Colors.black.withOpacity(0.15),
186 + offset: Offset(0, 1),
187 + blurRadius: 1,
188 + ),
189 + ],
190 + );
191 + Widget body = Column(
192 + children: <Widget>[
193 + Tapped(
194 + child: icon ?? Container(),
195 + onTap: onTap,
196 + ),
197 + Container(height: 2),
198 + Text(
199 + text ?? '??',
200 + style: TextStyle(
201 + fontWeight: FontWeight.normal,
202 + fontSize: SysSize.small,
203 + color: ColorPlate.white,
204 + ),
205 + ),
206 + ],
207 + );
208 + return Container(
209 + padding: EdgeInsets.symmetric(vertical: 10),
210 + child: DefaultTextStyle(
211 + child: body,
212 + style: shadowStyle,
213 + ),
214 + );
215 + }
216 +}
1 +import 'dart:async';
2 +import 'dart:math';
3 +
4 +import 'package:flutter/material.dart';
5 +
6 +/// 视频手势封装
7 +/// 单击:暂停
8 +/// 双击:点赞,双击后再次单击也是增加点赞爱心
9 +class TikTokVideoGesture extends StatefulWidget {
10 + const TikTokVideoGesture({
11 + Key? key,
12 + required this.child,
13 + this.onAddFavorite,
14 + this.onSingleTap,
15 + }) : super(key: key);
16 +
17 + final Function? onAddFavorite;
18 + final Function? onSingleTap;
19 + final Widget child;
20 +
21 + @override
22 + _TikTokVideoGestureState createState() => _TikTokVideoGestureState();
23 +}
24 +
25 +class _TikTokVideoGestureState extends State<TikTokVideoGesture> {
26 + GlobalKey _key = GlobalKey();
27 +
28 + // 内部转换坐标点
29 + Offset _p(Offset p) {
30 + RenderBox getBox = _key.currentContext!.findRenderObject() as RenderBox;
31 + return getBox.globalToLocal(p);
32 + }
33 +
34 + List<Offset> icons = [];
35 +
36 + bool canAddFavorite = false;
37 + bool justAddFavorite = false;
38 + Timer? timer;
39 +
40 + @override
41 + Widget build(BuildContext context) {
42 + var iconStack = Stack(
43 + children: icons
44 + .map<Widget>(
45 + (p) => TikTokFavoriteAnimationIcon(
46 + key: Key(p.toString()),
47 + position: p,
48 + onAnimationComplete: () {
49 + icons.remove(p);
50 + },
51 + ),
52 + )
53 + .toList(),
54 + );
55 + return GestureDetector(
56 + key: _key,
57 + onTapDown: (detail) {
58 + setState(() {
59 + if (canAddFavorite) {
60 + print('添加爱心,当前爱心数量:${icons.length}');
61 + icons.add(_p(detail.globalPosition));
62 + widget.onAddFavorite?.call();
63 + justAddFavorite = true;
64 + } else {
65 + justAddFavorite = false;
66 + }
67 + });
68 + },
69 + onTapUp: (detail) {
70 + timer?.cancel();
71 + var delay = canAddFavorite ? 1200 : 600;
72 + timer = Timer(Duration(milliseconds: delay), () {
73 + canAddFavorite = false;
74 + timer = null;
75 + if (!justAddFavorite) {
76 + widget.onSingleTap?.call();
77 + }
78 + });
79 + canAddFavorite = true;
80 + },
81 + onTapCancel: () {
82 + print('onTapCancel');
83 + },
84 + child: Stack(
85 + children: <Widget>[
86 + widget.child,
87 + iconStack,
88 + ],
89 + ),
90 + );
91 + }
92 +}
93 +
94 +class TikTokFavoriteAnimationIcon extends StatefulWidget {
95 + final Offset? position;
96 + final double size;
97 + final Function? onAnimationComplete;
98 +
99 + const TikTokFavoriteAnimationIcon({
100 + Key? key,
101 + this.onAnimationComplete,
102 + this.position,
103 + this.size: 100,
104 + }) : super(key: key);
105 +
106 + @override
107 + _TikTokFavoriteAnimationIconState createState() =>
108 + _TikTokFavoriteAnimationIconState();
109 +}
110 +
111 +class _TikTokFavoriteAnimationIconState
112 + extends State<TikTokFavoriteAnimationIcon> with TickerProviderStateMixin {
113 + AnimationController? _animationController;
114 + @override
115 + void dispose() {
116 + _animationController?.dispose();
117 + super.dispose();
118 + }
119 +
120 + @override
121 + void didChangeDependencies() {
122 + print('didChangeDependencies');
123 + super.didChangeDependencies();
124 + }
125 +
126 + @override
127 + void initState() {
128 + _animationController = AnimationController(
129 + lowerBound: 0,
130 + upperBound: 1,
131 + duration: Duration(milliseconds: 1600),
132 + vsync: this,
133 + );
134 +
135 + _animationController!.addListener(() {
136 + setState(() {});
137 + });
138 + startAnimation();
139 + super.initState();
140 + }
141 +
142 + startAnimation() async {
143 + await _animationController!.forward();
144 + widget.onAnimationComplete?.call();
145 + }
146 +
147 + double rotate = pi / 10.0 * (2 * Random().nextDouble() - 1);
148 +
149 + double? get value => _animationController?.value;
150 +
151 + double appearDuration = 0.1;
152 + double dismissDuration = 0.8;
153 +
154 + double get opa {
155 + if (value! < appearDuration) {
156 + return 0.99 / appearDuration * value!;
157 + }
158 + if (value! < dismissDuration) {
159 + return 0.99;
160 + }
161 + var res = 0.99 - (value! - dismissDuration) / (1 - dismissDuration);
162 + return res < 0 ? 0 : res;
163 + }
164 +
165 + double get scale {
166 + if (value! < appearDuration) {
167 + return 1 + appearDuration - value!;
168 + }
169 + if (value! < dismissDuration) {
170 + return 1;
171 + }
172 + return (value! - dismissDuration) / (1 - dismissDuration) + 1;
173 + }
174 +
175 + @override
176 + Widget build(BuildContext context) {
177 + Widget content = Icon(
178 + Icons.favorite,
179 + size: widget.size,
180 + color: Colors.redAccent,
181 + );
182 + content = ShaderMask(
183 + child: content,
184 + blendMode: BlendMode.srcATop,
185 + shaderCallback: (Rect bounds) => RadialGradient(
186 + center: Alignment.topLeft.add(Alignment(0.66, 0.66)),
187 + colors: [
188 + Color(0xffEF6F6F),
189 + Color(0xffF03E3E),
190 + ],
191 + ).createShader(bounds),
192 + );
193 + Widget body = Transform.rotate(
194 + angle: rotate,
195 + child: Opacity(
196 + opacity: opa,
197 + child: Transform.scale(
198 + alignment: Alignment.bottomCenter,
199 + scale: scale,
200 + child: content,
201 + ),
202 + ),
203 + );
204 + return widget.position == null
205 + ? Container()
206 + : Positioned(
207 + left: widget.position!.dx - widget.size / 2,
208 + top: widget.position!.dy - widget.size / 2,
209 + child: body,
210 + );
211 + }
212 +}
...@@ -134,6 +134,13 @@ packages: ...@@ -134,6 +134,13 @@ packages:
134 url: "https://pub.dartlang.org" 134 url: "https://pub.dartlang.org"
135 source: hosted 135 source: hosted
136 version: "3.0.1" 136 version: "3.0.1"
137 + csslib:
138 + dependency: transitive
139 + description:
140 + name: csslib
141 + url: "https://pub.dartlang.org"
142 + source: hosted
143 + version: "0.17.1"
137 cupertino_icons: 144 cupertino_icons:
138 dependency: "direct main" 145 dependency: "direct main"
139 description: 146 description:
...@@ -282,6 +289,13 @@ packages: ...@@ -282,6 +289,13 @@ packages:
282 url: "https://pub.dartlang.org" 289 url: "https://pub.dartlang.org"
283 source: hosted 290 source: hosted
284 version: "1.1.0" 291 version: "1.1.0"
292 + flutter_spinkit:
293 + dependency: "direct main"
294 + description:
295 + name: flutter_spinkit
296 + url: "https://pub.dartlang.org"
297 + source: hosted
298 + version: "5.1.0"
285 flutter_swiper_null_safety: 299 flutter_swiper_null_safety:
286 dependency: "direct main" 300 dependency: "direct main"
287 description: 301 description:
...@@ -318,6 +332,13 @@ packages: ...@@ -318,6 +332,13 @@ packages:
318 url: "https://pub.dartlang.org" 332 url: "https://pub.dartlang.org"
319 source: hosted 333 source: hosted
320 version: "2.0.2" 334 version: "2.0.2"
335 + html:
336 + dependency: transitive
337 + description:
338 + name: html
339 + url: "https://pub.dartlang.org"
340 + source: hosted
341 + version: "0.15.0"
321 http: 342 http:
322 dependency: transitive 343 dependency: transitive
323 description: 344 description:
...@@ -603,6 +624,13 @@ packages: ...@@ -603,6 +624,13 @@ packages:
603 url: "https://pub.dartlang.org" 624 url: "https://pub.dartlang.org"
604 source: hosted 625 source: hosted
605 version: "0.27.3" 626 version: "0.27.3"
627 + safemap:
628 + dependency: "direct main"
629 + description:
630 + name: safemap
631 + url: "https://pub.dartlang.org"
632 + source: hosted
633 + version: "2.1.0"
606 shared_preferences: 634 shared_preferences:
607 dependency: transitive 635 dependency: transitive
608 description: 636 description:
...@@ -783,6 +811,13 @@ packages: ...@@ -783,6 +811,13 @@ packages:
783 url: "https://pub.dartlang.org" 811 url: "https://pub.dartlang.org"
784 source: hosted 812 source: hosted
785 version: "3.0.0" 813 version: "3.0.0"
814 + tapped:
815 + dependency: "direct main"
816 + description:
817 + name: tapped
818 + url: "https://pub.dartlang.org"
819 + source: hosted
820 + version: "2.0.0"
786 term_glyph: 821 term_glyph:
787 dependency: transitive 822 dependency: transitive
788 description: 823 description:
...@@ -909,6 +944,27 @@ packages: ...@@ -909,6 +944,27 @@ packages:
909 url: "https://pub.dartlang.org" 944 url: "https://pub.dartlang.org"
910 source: hosted 945 source: hosted
911 version: "1.6.3-nullsafety.0" 946 version: "1.6.3-nullsafety.0"
947 + video_player:
948 + dependency: "direct main"
949 + description:
950 + name: video_player
951 + url: "https://pub.dartlang.org"
952 + source: hosted
953 + version: "2.2.10"
954 + video_player_platform_interface:
955 + dependency: transitive
956 + description:
957 + name: video_player_platform_interface
958 + url: "https://pub.dartlang.org"
959 + source: hosted
960 + version: "5.0.0"
961 + video_player_web:
962 + dependency: transitive
963 + description:
964 + name: video_player_web
965 + url: "https://pub.dartlang.org"
966 + source: hosted
967 + version: "2.0.5"
912 vm_service: 968 vm_service:
913 dependency: transitive 969 dependency: transitive
914 description: 970 description:
......
...@@ -81,6 +81,15 @@ dependencies: ...@@ -81,6 +81,15 @@ dependencies:
81 # Use with the CupertinoIcons class for iOS style icons. 81 # Use with the CupertinoIcons class for iOS style icons.
82 cupertino_icons: ^1.0.2 82 cupertino_icons: ^1.0.2
83 83
84 + # temp
85 + video_player: ^2.1.6
86 + # map取值
87 + safemap: ^2.0.0-nullsafety.0
88 + # 基础的点击
89 + tapped: ^2.0.0-nullsafety.0
90 + # 加载动画库
91 + flutter_spinkit: ^5.0.0
92 +
84 dependency_overrides: 93 dependency_overrides:
85 decimal: 1.5.0 94 decimal: 1.5.0
86 95
......