Showing
15 changed files
with
1344 additions
and
180 deletions
| ... | @@ -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 | +} |
lib/tiktok/mock/video.dart
0 → 100644
| 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 | +} |
lib/tiktok/style/physics.dart
0 → 100644
| 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 | +} |
lib/tiktok/style/style.dart
0 → 100644
| 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 | +} |
lib/tiktok/views/select_text.dart
0 → 100644
| 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 | +} |
lib/tiktok/views/tiktok_header.dart
0 → 100644
| 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 | +} |
lib/tiktok/views/tiktok_scaffold.dart
0 → 100644
This diff is collapsed. Click to expand it.
lib/tiktok/views/tiktok_video.dart
0 → 100644
| 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 | +} |
lib/tiktok/views/tiktok_video_gesture.dart
0 → 100644
| 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 | ... | ... |
-
Please register or login to post a comment