reason

add tiktok video

......@@ -48,6 +48,7 @@ android {
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
buildTypes {
......
......@@ -9,29 +9,25 @@ import 'package:one_poem/video/page/video_page.dart';
import 'package:one_poem/widgets/double_tap_back_exit_app.dart';
import 'package:one_poem/widgets/load_image.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/one_poem_localizations.dart';
import 'provider/home_provider.dart';
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with RestorationMixin{
class _HomeState extends State<Home> with RestorationMixin {
static const double _imageSize = 25.0;
late List<Widget> _pageList;
final List<String> _appBarTitles = ['订单', '商品', '统计', '视频','店铺'];
final PageController _pageController = PageController();
HomeProvider provider = HomeProvider();
List<BottomNavigationBarItem>? _list;
List<BottomNavigationBarItem>? _listDark;
@override
void initState() {
......@@ -49,36 +45,109 @@ class _HomeState extends State<Home> with RestorationMixin{
_pageList = [
const OrderPage(),
const GoodsPage(),
const NotFoundPage(),//TODO
const VideoPage(),
const ShopPage(),
];
}
List<BottomNavigationBarItem> _buildBottomNavigationBarItem() {
List<BottomNavigationBarItem> _buildBottomNavigationBarItem(
BuildContext context) {
final bool isDark = context.isDark;
List<String> _appBarTitles = [
OnePoemLocalizations.of(context).onePoemBottomNavigationBarItemTitle,
OnePoemLocalizations.of(context).timelineBottomNavigationBarItemTitle,
OnePoemLocalizations.of(context).categoryBottomNavigationBarItemTitle,
OnePoemLocalizations.of(context).profileBottomNavigationBarItemTitle,
];
if (_list == null) {
const _tabImages = [
[
LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.unselected_item_color,),
LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.app_main,),
],
[
LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.unselected_item_color,),
LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.app_main,),
],
[
LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.unselected_item_color,),
LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.app_main,),
],
[
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.unselected_item_color,),
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.app_main,),
],
[
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.unselected_item_color,),
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.app_main,),
]
];
List<List<LoadAssetImage>> _tabImages;
if (!isDark) {
_tabImages = [
[
const LoadAssetImage(
'home/icon_order',
width: _imageSize,
color: Colours.unselected_item_color,
),
const LoadAssetImage(
'home/icon_order',
width: _imageSize,
color: Colours.app_main,
),
],
[
const LoadAssetImage(
'home/icon_commodity',
width: _imageSize,
color: Colours.unselected_item_color,
),
const LoadAssetImage(
'home/icon_commodity',
width: _imageSize,
color: Colours.app_main,
),
],
[
const LoadAssetImage(
'home/icon_statistics',
width: _imageSize,
color: Colours.unselected_item_color,
),
const LoadAssetImage(
'home/icon_statistics',
width: _imageSize,
color: Colours.app_main,
),
],
[
const LoadAssetImage(
'home/icon_shop',
width: _imageSize,
color: Colours.unselected_item_color,
),
const LoadAssetImage(
'home/icon_shop',
width: _imageSize,
color: Colours.app_main,
),
]
];
} else {
_tabImages = [
[
const LoadAssetImage('home/icon_order', width: _imageSize),
const LoadAssetImage(
'home/icon_order',
width: _imageSize,
color: Colours.dark_app_main,
),
],
[
const LoadAssetImage('home/icon_commodity', width: _imageSize),
const LoadAssetImage(
'home/icon_commodity',
width: _imageSize,
color: Colours.dark_app_main,
),
],
[
const LoadAssetImage('home/icon_statistics', width: _imageSize),
const LoadAssetImage(
'home/icon_statistics',
width: _imageSize,
color: Colours.dark_app_main,
),
],
[
const LoadAssetImage('home/icon_shop', width: _imageSize),
const LoadAssetImage(
'home/icon_shop',
width: _imageSize,
color: Colours.dark_app_main,
),
]
];
}
_list = List.generate(_tabImages.length, (i) {
return BottomNavigationBarItem(
icon: _tabImages[i][0],
......@@ -90,42 +159,6 @@ class _HomeState extends State<Home> with RestorationMixin{
return _list!;
}
List<BottomNavigationBarItem> _buildDarkBottomNavigationBarItem() {
if (_listDark == null) {
const _tabImagesDark = [
[
LoadAssetImage('home/icon_order', width: _imageSize),
LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.dark_app_main,),
],
[
LoadAssetImage('home/icon_commodity', width: _imageSize),
LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.dark_app_main,),
],
[
LoadAssetImage('home/icon_statistics', width: _imageSize),
LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.dark_app_main,),
],
[
LoadAssetImage('home/icon_shop', width: _imageSize),
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.dark_app_main,),
],
[
LoadAssetImage('home/icon_shop', width: _imageSize),
LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.dark_app_main,),
]
];
_listDark = List.generate(_tabImagesDark.length, (i) {
return BottomNavigationBarItem(
icon: _tabImagesDark[i][0],
activeIcon: _tabImagesDark[i][1],
label: _appBarTitles[i],
);
});
}
return _listDark!;
}
@override
Widget build(BuildContext context) {
final bool isDark = context.isDark;
......@@ -133,31 +166,32 @@ class _HomeState extends State<Home> with RestorationMixin{
create: (_) => provider,
child: DoubleTapBackExitApp(
child: Scaffold(
bottomNavigationBar: Consumer<HomeProvider>(
builder: (_, provider, __) {
return BottomNavigationBar(
backgroundColor: context.backgroundColor,
items: isDark ? _buildDarkBottomNavigationBarItem() : _buildBottomNavigationBarItem(),
type: BottomNavigationBarType.fixed,
currentIndex: provider.value,
elevation: 5.0,
iconSize: 21.0,
selectedFontSize: Dimens.font_sp10,
unselectedFontSize: Dimens.font_sp10,
selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: isDark ? Colours.dark_unselected_item_color : Colours.unselected_item_color,
onTap: (index) => _pageController.jumpToPage(index),
);
},
),
// 使用PageView的原因参看 https://zhuanlan.zhihu.com/p/58582876
body: PageView(
physics: const NeverScrollableScrollPhysics(), // 禁止滑动
controller: _pageController,
onPageChanged: (int index) => provider.value = index,
children: _pageList,
)
),
bottomNavigationBar: Consumer<HomeProvider>(
builder: (_, provider, __) {
return BottomNavigationBar(
backgroundColor: context.backgroundColor,
items: _buildBottomNavigationBarItem(context),
type: BottomNavigationBarType.fixed,
currentIndex: provider.value,
elevation: 5.0,
iconSize: 21.0,
selectedFontSize: Dimens.font_sp10,
unselectedFontSize: Dimens.font_sp10,
selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: isDark
? Colours.dark_unselected_item_color
: Colours.unselected_item_color,
onTap: (index) => _pageController.jumpToPage(index),
);
},
),
// 使用PageView的原因参看 https://zhuanlan.zhihu.com/p/58582876
body: PageView(
physics: const NeverScrollableScrollPhysics(), // 禁止滑动
controller: _pageController,
onPageChanged: (int index) => provider.value = index,
children: _pageList,
)),
),
);
}
......@@ -169,5 +203,4 @@ class _HomeState extends State<Home> with RestorationMixin{
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(provider, 'BottomNavigationBarCurrentIndex');
}
}
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:one_poem/tiktok/controller/tiktok_video_list_controller.dart';
import 'package:one_poem/tiktok/mock/video.dart';
import 'package:one_poem/tiktok/style/physics.dart';
import 'package:one_poem/tiktok/views/tiktok_header.dart';
import 'package:one_poem/tiktok/views/tiktok_scaffold.dart';
import 'package:one_poem/tiktok/views/tiktok_video.dart';
import 'package:one_poem/tiktok/views/tiktok_video_button_column.dart';
import 'package:video_player/video_player.dart';
class OrderPage extends StatefulWidget {
const OrderPage({Key? key}) : super(key: key);
......@@ -9,13 +17,12 @@ class OrderPage extends StatefulWidget {
}
class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
TikTokPageTag tabBarType = TikTokPageTag.home;
TikTokScaffoldController tkController = TikTokScaffoldController();
PageController _pageController = PageController();
final PageController _pageController = PageController();
TikTokVideoListController _videoListController = TikTokVideoListController();
final TikTokVideoListController _videoListController = TikTokVideoListController();
/// 记录点赞
Map<int, bool> favoriteMap = {};
......@@ -45,19 +52,19 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
initialList: videoDataList
.map(
(e) => VPVideoController(
videoInfo: e,
builder: () => VideoPlayerController.network(e.url),
),
)
videoInfo: e,
builder: () => VideoPlayerController.network(e.url),
),
)
.toList(),
videoProvider: (int index, List<VPVideoController> list) async {
return videoDataList
.map(
(e) => VPVideoController(
videoInfo: e,
builder: () => VideoPlayerController.network(e.url),
),
)
videoInfo: e,
builder: () => VideoPlayerController.network(e.url),
),
)
.toList();
},
);
......@@ -65,7 +72,7 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
setState(() {});
});
tkController.addListener(
() {
() {
if (tkController.value == TikTokPagePositon.middle) {
_videoListController.currentPlayer.play();
} else {
......@@ -80,114 +87,44 @@ class _OrderPageState extends State<OrderPage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
Widget? currentPage;
switch (tabBarType) {
case TikTokPageTag.home:
break;
case TikTokPageTag.follow:
currentPage = FollowPage();
break;
case TikTokPageTag.msg:
currentPage = MsgPage();
break;
case TikTokPageTag.me:
currentPage = UserPage(isSelfPage: true);
break;
}
double a = MediaQuery.of(context).size.aspectRatio;
bool hasBottomPadding = a < 0.55;
bool hasBackground = hasBottomPadding;
hasBackground = tabBarType != TikTokPageTag.home;
if (hasBottomPadding) {
hasBackground = true;
}
Widget tikTokTabBar = TikTokTabBar(
hasBackground: hasBackground,
current: tabBarType,
onTabSwitch: (type) async {
setState(() {
tabBarType = type;
if (type == TikTokPageTag.home) {
_videoListController.currentPlayer.play();
} else {
_videoListController.currentPlayer.pause();
}
});
},
onAddButton: () {
Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => CameraPage(),
),
);
},
);
var userPage = UserPage(
isSelfPage: false,
canPop: true,
onPop: () {
tkController.animateToMiddle();
},
);
var searchPage = SearchPage(
onPop: tkController.animateToMiddle,
);
var header = tabBarType == TikTokPageTag.home
? TikTokHeader(
var header = TikTokHeader(
onSearch: () {
tkController.animateToLeft();
},
)
: Container();
);
// 组合
return TikTokScaffold(
controller: tkController,
hasBottomPadding: hasBackground,
tabBar: tikTokTabBar,
header: header,
leftPage: searchPage,
rightPage: userPage,
enableGesture: tabBarType == TikTokPageTag.home,
// onPullDownRefresh: _fetchData,
enableGesture: true,
page: Stack(
// index: currentPage == null ? 0 : 1,
children: <Widget>[
PageView.builder(
key: Key('home'),
physics: QuickerScrollPhysics(),
key: const Key('home'),
physics: const QuickerScrollPhysics(),
controller: _pageController,
scrollDirection: Axis.vertical,
itemCount: _videoListController.videoCount,
itemBuilder: (context, i) {
// 拼一个视频组件出来
bool isF = SafeMap(favoriteMap)[i].boolean ?? false;
var player = _videoListController.playerOfIndex(i)!;
var data = player.videoInfo!;
// 右侧按钮列
Widget buttons = TikTokButtonColumn(
isFavorite: isF,
isFavorite: false,
onAvatar: () {
tkController.animateToPage(TikTokPagePositon.right);
},
onFavorite: () {
setState(() {
favoriteMap[i] = !isF;
});
// showAboutDialog(context: context);
},
onComment: () {
CustomBottomSheet.showModalBottomSheet(
backgroundColor: Colors.white.withOpacity(0),
context: context,
builder: (BuildContext context) =>
TikTokCommentBottomSheet(),
);
},
onShare: () {},
);
// video
......
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:one_poem/tiktok/mock/video.dart';
import 'package:video_player/video_player.dart';
typedef LoadMoreVideo = Future<List<VPVideoController>> Function(
int index,
List<VPVideoController> list,
);
/// TikTokVideoListController是一系列视频的控制器,内部管理了视频控制器数组
/// 提供了预加载/释放/加载更多功能
class TikTokVideoListController extends ChangeNotifier {
TikTokVideoListController({
this.loadMoreCount = 1,
this.preloadCount = 3,
this.disposeCount = 5,
});
/// 到第几个触发预加载,例如:1:最后一个,2:倒数第二个
final int loadMoreCount;
/// 预加载多少个视频
final int preloadCount;
/// 超出多少个,就释放视频
final int disposeCount;
/// 提供视频的builder
LoadMoreVideo? _videoProvider;
loadIndex(int target, {bool reload = false}) {
if (!reload) {
if (index.value == target) return;
}
// 播放当前的,暂停其他的
var oldIndex = index.value;
var newIndex = target;
// 暂停之前的视频
if (!(oldIndex == 0 && newIndex == 0)) {
playerOfIndex(oldIndex)?.controller.seekTo(Duration.zero);
// playerOfIndex(oldIndex)?.controller.addListener(_didUpdateValue);
// playerOfIndex(oldIndex)?.showPauseIcon.addListener(_didUpdateValue);
playerOfIndex(oldIndex)?.pause();
print('暂停$oldIndex');
}
// 开始播放当前的视频
playerOfIndex(newIndex)?.controller.addListener(_didUpdateValue);
playerOfIndex(newIndex)?.showPauseIcon.addListener(_didUpdateValue);
playerOfIndex(newIndex)?.play();
print('播放$newIndex');
// 处理预加载/释放内存
for (var i = 0; i < playerList.length; i++) {
// 需要释放[disposeCount]之前的视频
if (i < newIndex - disposeCount || i > newIndex + disposeCount) {
print('释放$i');
playerOfIndex(i)?.controller.removeListener(_didUpdateValue);
playerOfIndex(i)?.showPauseIcon.removeListener(_didUpdateValue);
playerOfIndex(i)?.dispose();
} else {
// 需要预加载
if (i > newIndex && i < newIndex + preloadCount) {
print('预加载$i');
playerOfIndex(i)?.init();
}
}
}
// 快到最底部,添加更多视频
if (playerList.length - newIndex <= loadMoreCount + 1) {
_videoProvider?.call(newIndex, playerList).then(
(list) async {
playerList.addAll(list);
notifyListeners();
},
);
}
// 完成
index.value = target;
}
_didUpdateValue() {
notifyListeners();
}
/// 获取指定index的player
VPVideoController? playerOfIndex(int index) {
if (index < 0 || index > playerList.length - 1) {
return null;
}
return playerList[index];
}
/// 视频总数目
int get videoCount => playerList.length;
/// 初始化
init({
required PageController pageController,
required List<VPVideoController> initialList,
required LoadMoreVideo videoProvider,
}) async {
playerList.addAll(initialList);
_videoProvider = videoProvider;
pageController.addListener(() {
var p = pageController.page!;
if (p % 1 == 0) {
loadIndex(p ~/ 1);
}
});
loadIndex(0, reload: true);
notifyListeners();
}
/// 目前的视频序号
ValueNotifier<int> index = ValueNotifier<int>(0);
/// 视频列表
List<VPVideoController> playerList = [];
///
VPVideoController get currentPlayer => playerList[index.value];
/// 销毁全部
void dispose() {
// 销毁全部
for (var player in playerList) {
player.showPauseIcon.dispose();
player.dispose();
}
playerList = [];
super.dispose();
}
}
typedef ControllerSetter<T> = Future<void> Function(T controller);
typedef ControllerBuilder<T> = T Function();
/// 抽象类,作为视频控制器必须实现这些方法
abstract class TikTokVideoController<T> {
/// 获取当前的控制器实例
T? get controller;
/// 是否显示暂停按钮
ValueNotifier<bool> get showPauseIcon;
/// 加载视频,在init后,应当开始下载视频内容
Future<void> init({ControllerSetter<T>? afterInit});
/// 视频销毁,在dispose后,应当释放任何内存资源
Future<void> dispose();
/// 播放
Future<void> play();
/// 暂停
Future<void> pause({bool showPauseIcon: false});
}
class VPVideoController extends TikTokVideoController<VideoPlayerController> {
VideoPlayerController? _controller;
ValueNotifier<bool> _showPauseIcon = ValueNotifier<bool>(false);
final UserVideo? videoInfo;
final ControllerBuilder<VideoPlayerController> _builder;
final ControllerSetter<VideoPlayerController>? _afterInit;
VPVideoController({
this.videoInfo,
required ControllerBuilder<VideoPlayerController> builder,
ControllerSetter<VideoPlayerController>? afterInit,
}) : this._builder = builder,
this._afterInit = afterInit;
@override
VideoPlayerController get controller {
if (_controller == null) {
_controller = _builder.call();
}
return _controller!;
}
/// 阻止在init的时候dispose,或者在dispose前init
List<Future> _actLocks = [];
bool get isDispose => _disposeLock != null;
bool get prepared => _prepared;
bool _prepared = false;
Completer<void>? _disposeLock;
@override
Future<void> dispose() async {
if (!prepared) return;
await Future.wait(_actLocks);
_actLocks.clear();
var completer = Completer<void>();
_actLocks.add(completer.future);
_prepared = false;
await this.controller.dispose();
_controller = null;
_disposeLock = Completer<void>();
completer.complete();
}
@override
Future<void> init({
ControllerSetter<VideoPlayerController>? afterInit,
}) async {
if (prepared) return;
await Future.wait(_actLocks);
_actLocks.clear();
var completer = Completer<void>();
_actLocks.add(completer.future);
await this.controller.initialize();
await this.controller.setLooping(true);
afterInit ??= this._afterInit;
await afterInit?.call(this.controller);
_prepared = true;
completer.complete();
if (_disposeLock != null) {
_disposeLock?.complete();
_disposeLock = null;
}
}
@override
Future<void> pause({bool showPauseIcon: false}) async {
await Future.wait(_actLocks);
_actLocks.clear();
await init();
if (!prepared) return;
if (_disposeLock != null) {
await _disposeLock?.future;
}
await this.controller.pause();
_showPauseIcon.value = true;
}
@override
Future<void> play() async {
await Future.wait(_actLocks);
_actLocks.clear();
await init();
if (!prepared) return;
if (_disposeLock != null) {
await _disposeLock?.future;
}
await this.controller.play();
_showPauseIcon.value = false;
}
@override
ValueNotifier<bool> get showPauseIcon => _showPauseIcon;
}
import 'dart:io';
Socket? socket;
var videoList = [
'test-video-10.MP4',
'test-video-6.mp4',
'test-video-9.MP4',
'test-video-8.MP4',
'test-video-7.MP4',
'test-video-1.mp4',
'test-video-2.mp4',
'test-video-3.mp4',
'test-video-4.mp4',
];
class UserVideo {
final String url;
final String image;
final String? desc;
UserVideo({
required this.url,
required this.image,
this.desc,
});
static List<UserVideo> fetchVideo() {
List<UserVideo> list = videoList
.map((e) => UserVideo(image: '', url: 'https://static.ybhospital.net/$e', desc: 'test_video_desc'))
.toList();
return list;
}
@override
String toString() {
return 'image:$image' '\nvideo:$url';
}
}
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class QuickerScrollPhysics extends BouncingScrollPhysics {
const QuickerScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);
@override
QuickerScrollPhysics applyTo(ScrollPhysics? ancestor) {
return QuickerScrollPhysics(parent: buildParent(ancestor));
}
@override
SpringDescription get spring => SpringDescription.withDampingRatio(
mass: 0.2,
stiffness: 300.0,
ratio: 1.1,
);
}
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class SysSize {
static const double avatar = 56;
// static const double iconBig = 40;
static const double iconNormal = 24;
// static const double big = 18;
// static const double normal = 16;
// static const double small = 12;
static const double iconBig = 40;
static const double big = 16;
static const double normal = 14;
static const double small = 12;
}
class StandardTextStyle {
static const TextStyle big = TextStyle(
fontWeight: FontWeight.w600,
fontSize: SysSize.big,
inherit: true,
);
static const TextStyle bigWithOpacity = TextStyle(
color: Color.fromRGBO(0xff, 0xff, 0xff, .66),
fontWeight: FontWeight.w600,
fontSize: SysSize.big,
inherit: true,
);
static const TextStyle normalW = TextStyle(
fontWeight: FontWeight.w600,
fontSize: SysSize.normal,
inherit: true,
);
static const TextStyle normal = TextStyle(
fontWeight: FontWeight.normal,
fontSize: SysSize.normal,
inherit: true,
);
static const TextStyle normalWithOpacity = const TextStyle(
color: const Color.fromRGBO(0xff, 0xff, 0xff, .66),
fontWeight: FontWeight.normal,
fontSize: SysSize.normal,
inherit: true,
);
static const TextStyle small = const TextStyle(
fontWeight: FontWeight.normal,
fontSize: SysSize.small,
inherit: true,
);
static const TextStyle smallWithOpacity = const TextStyle(
color: const Color.fromRGBO(0xff, 0xff, 0xff, .66),
fontWeight: FontWeight.normal,
fontSize: SysSize.small,
inherit: true,
);
}
class ColorPlate {
// 配色
static const Color orange = const Color(0xffFFC459);
static const Color yellow = const Color(0xffF1E300);
static const Color green = const Color(0xff7ED321);
static const Color red = const Color(0xffEB3838);
static const Color darkGray = const Color(0xff4A4A4A);
static const Color gray = const Color(0xff9b9b9b);
static const Color lightGray = const Color(0xfff5f5f4);
static const Color black = const Color(0xff000000);
static const Color white = const Color(0xffffffff);
static const Color clear = const Color(0);
/// 深色背景
static const Color back1 = const Color(0xff1D1F22);
/// 比深色背景略深一点
static const Color back2 = const Color(0xff121314);
}
import 'package:flutter/material.dart';
import 'package:one_poem/tiktok/style/style.dart';
class SelectText extends StatelessWidget {
const SelectText({
Key? key,
this.isSelect: true,
this.title,
}) : super(key: key);
final bool isSelect;
final String? title;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 12),
color: Colors.black.withOpacity(0),
child: Text(
title ?? '??',
textAlign: TextAlign.center,
style:
isSelect ? StandardTextStyle.big : StandardTextStyle.bigWithOpacity,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:tapped/tapped.dart';
import 'select_text.dart';
class TikTokHeader extends StatefulWidget {
final Function? onSearch;
const TikTokHeader({
Key? key,
this.onSearch,
}) : super(key: key);
@override
_TikTokHeaderState createState() => _TikTokHeaderState();
}
class _TikTokHeaderState extends State<TikTokHeader> {
int currentSelect = 0;
@override
Widget build(BuildContext context) {
List<String> list = ['推荐', '本地'];
List<Widget> headList = [];
for (var i = 0; i < list.length; i++) {
headList.add(Expanded(
child: GestureDetector(
child: SelectText(
title: list[i],
isSelect: i == currentSelect,
),
onTap: () {
setState(() {
currentSelect = i;
});
},
),
));
}
Widget headSwitch = Row(
children: headList,
);
return Container(
// color: Colors.black.withOpacity(0.3),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Tapped(
child: Container(
color: Colors.black.withOpacity(0),
padding: const EdgeInsets.all(4),
alignment: Alignment.centerLeft,
child: Icon(
Icons.search,
color: Colors.white.withOpacity(0.66),
),
),
onTap: widget.onSearch,
),
),
Expanded(
flex: 1,
child: Container(
color: Colors.black.withOpacity(0),
alignment: Alignment.center,
child: headSwitch,
),
),
Expanded(
child: Tapped(
child: Container(
color: Colors.black.withOpacity(0),
padding: const EdgeInsets.all(4),
alignment: Alignment.centerRight,
child: Icon(
Icons.tv,
color: Colors.white.withOpacity(0.66),
),
),
),
),
],
),
);
}
}
This diff is collapsed. Click to expand it.
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:one_poem/tiktok/style/style.dart';
import 'package:one_poem/tiktok/views/tiktok_video_gesture.dart';
///
/// TikTok风格的一个视频页组件,覆盖在video上,提供以下功能:
/// 播放按钮的遮罩
/// 单击事件
/// 点赞事件回调(每次)
/// 长宽比控制
/// 底部padding(用于适配有沉浸式底部状态栏时)
///
class TikTokVideoPage extends StatelessWidget {
final Widget? video;
final double aspectRatio;
final String? tag;
final double bottomPadding;
final Widget? rightButtonColumn;
final Widget? userInfoWidget;
final bool hidePauseIcon;
final Function? onAddFavorite;
final Function? onSingleTap;
const TikTokVideoPage({
Key? key,
this.bottomPadding: 16,
this.tag,
this.rightButtonColumn,
this.userInfoWidget,
this.onAddFavorite,
this.onSingleTap,
this.video,
this.aspectRatio: 9 / 16.0,
this.hidePauseIcon: false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// 右边的按钮列表
Widget rightButtons = rightButtonColumn ?? Container();
// 用户信息
Widget userInfo = userInfoWidget ??
VideoUserInfo(
bottomPadding: bottomPadding,
);
// 视频加载的动画
// Widget videoLoading = VideoLoadingPlaceHolder(tag: tag);
// 视频播放页
Widget videoContainer = Stack(
children: <Widget>[
Container(
height: double.infinity,
width: double.infinity,
color: Colors.black,
alignment: Alignment.center,
child: AspectRatio(
aspectRatio: aspectRatio,
child: video,
),
),
TikTokVideoGesture(
onAddFavorite: onAddFavorite,
onSingleTap: onSingleTap,
child: Container(
color: ColorPlate.clear,
height: double.infinity,
width: double.infinity,
),
),
hidePauseIcon
? Container()
: Container(
height: double.infinity,
width: double.infinity,
alignment: Alignment.center,
child: Icon(
Icons.play_circle_outline,
size: 120,
color: Colors.white.withOpacity(0.4),
),
),
],
);
Widget body = Stack(
children: <Widget>[
videoContainer,
Container(
height: double.infinity,
width: double.infinity,
alignment: Alignment.bottomRight,
child: rightButtons,
),
Container(
height: double.infinity,
width: double.infinity,
alignment: Alignment.bottomLeft,
child: userInfo,
),
],
);
return body;
}
}
class VideoLoadingPlaceHolder extends StatelessWidget {
const VideoLoadingPlaceHolder({
Key? key,
required this.tag,
}) : super(key: key);
final String tag;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
colors: <Color>[
Colors.blue,
Colors.green,
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SpinKitWave(
size: 36,
color: Colors.white.withOpacity(0.3),
),
Container(
padding: const EdgeInsets.all(50),
child: Text(
tag,
style: StandardTextStyle.normalWithOpacity,
),
),
],
),
);
}
}
class VideoUserInfo extends StatelessWidget {
final String? desc;
// final Function onGoodGift;
const VideoUserInfo({
Key? key,
required this.bottomPadding,
// @required this.onGoodGift,
this.desc,
}) : super(key: key);
final double bottomPadding;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: 12,
bottom: bottomPadding,
),
margin: const EdgeInsets.only(right: 80),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'@朱二旦的枯燥生活',
style: StandardTextStyle.big,
),
Container(height: 6),
Text(
desc ?? '#原创 有钱人的生活就是这么朴实无华,且枯燥 #短视频',
style: StandardTextStyle.normal,
),
Container(height: 6),
Row(
children: const <Widget>[
Icon(Icons.music_note, size: 14),
Expanded(
child: Text(
'朱二旦的枯燥生活创作的原声',
maxLines: 9,
style: StandardTextStyle.normal,
),
)
],
)
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:one_poem/tiktok/style/style.dart';
import 'package:tapped/tapped.dart';
class TikTokButtonColumn extends StatelessWidget {
final double? bottomPadding;
final bool isFavorite;
final Function? onFavorite;
final Function? onComment;
final Function? onShare;
final Function? onAvatar;
const TikTokButtonColumn({
Key? key,
this.bottomPadding,
this.onFavorite,
this.onComment,
this.onShare,
this.isFavorite: false,
this.onAvatar,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: SysSize.avatar,
margin: EdgeInsets.only(
bottom: bottomPadding ?? 50,
right: 12,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Tapped(
child: TikTokAvatar(),
onTap: onAvatar,
),
FavoriteIcon(
onFavorite: onFavorite,
isFavorite: isFavorite,
),
_IconButton(
icon: const IconToText(Icons.mode_comment, size: SysSize.iconBig - 4),
text: '4213',
onTap: onComment,
),
_IconButton(
icon: const IconToText(Icons.share, size: SysSize.iconBig),
text: '346',
onTap: onShare,
),
Container(
width: SysSize.avatar,
height: SysSize.avatar,
margin: const EdgeInsets.only(top: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(SysSize.avatar / 2.0),
// color: Colors.black.withOpacity(0.8),
),
)
],
),
);
}
}
class FavoriteIcon extends StatelessWidget {
const FavoriteIcon({
Key? key,
required this.onFavorite,
this.isFavorite,
}) : super(key: key);
final bool? isFavorite;
final Function? onFavorite;
@override
Widget build(BuildContext context) {
return _IconButton(
icon: IconToText(
Icons.favorite,
size: SysSize.iconBig,
color: isFavorite! ? ColorPlate.red : null,
),
text: '1.0w',
onTap: onFavorite,
);
}
}
class TikTokAvatar extends StatelessWidget {
const TikTokAvatar({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget avatar = Container(
width: SysSize.avatar,
height: SysSize.avatar,
margin: EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 1,
),
borderRadius: BorderRadius.circular(SysSize.avatar / 2.0),
color: Colors.orange,
),
child: ClipOval(
child: Image.network(
"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
fit: BoxFit.cover,
),
),
);
Widget addButton = Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: ColorPlate.orange,
),
child: Icon(
Icons.add,
size: 16,
),
);
return Container(
width: SysSize.avatar,
height: 66,
margin: EdgeInsets.only(bottom: 6),
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[avatar, addButton],
),
);
}
}
/// 把IconData转换为文字,使其可以使用文字样式
class IconToText extends StatelessWidget {
final IconData? icon;
final TextStyle? style;
final double? size;
final Color? color;
const IconToText(
this.icon, {
Key? key,
this.style,
this.size,
this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
String.fromCharCode(icon!.codePoint),
style: style ??
TextStyle(
fontFamily: 'MaterialIcons',
fontSize: size ?? 30,
inherit: true,
color: color ?? ColorPlate.white,
),
);
}
}
class _IconButton extends StatelessWidget {
final Widget? icon;
final String? text;
final Function? onTap;
const _IconButton({
Key? key,
this.icon,
this.text,
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var shadowStyle = TextStyle(
shadows: [
Shadow(
color: Colors.black.withOpacity(0.15),
offset: Offset(0, 1),
blurRadius: 1,
),
],
);
Widget body = Column(
children: <Widget>[
Tapped(
child: icon ?? Container(),
onTap: onTap,
),
Container(height: 2),
Text(
text ?? '??',
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: SysSize.small,
color: ColorPlate.white,
),
),
],
);
return Container(
padding: EdgeInsets.symmetric(vertical: 10),
child: DefaultTextStyle(
child: body,
style: shadowStyle,
),
);
}
}
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
/// 视频手势封装
/// 单击:暂停
/// 双击:点赞,双击后再次单击也是增加点赞爱心
class TikTokVideoGesture extends StatefulWidget {
const TikTokVideoGesture({
Key? key,
required this.child,
this.onAddFavorite,
this.onSingleTap,
}) : super(key: key);
final Function? onAddFavorite;
final Function? onSingleTap;
final Widget child;
@override
_TikTokVideoGestureState createState() => _TikTokVideoGestureState();
}
class _TikTokVideoGestureState extends State<TikTokVideoGesture> {
GlobalKey _key = GlobalKey();
// 内部转换坐标点
Offset _p(Offset p) {
RenderBox getBox = _key.currentContext!.findRenderObject() as RenderBox;
return getBox.globalToLocal(p);
}
List<Offset> icons = [];
bool canAddFavorite = false;
bool justAddFavorite = false;
Timer? timer;
@override
Widget build(BuildContext context) {
var iconStack = Stack(
children: icons
.map<Widget>(
(p) => TikTokFavoriteAnimationIcon(
key: Key(p.toString()),
position: p,
onAnimationComplete: () {
icons.remove(p);
},
),
)
.toList(),
);
return GestureDetector(
key: _key,
onTapDown: (detail) {
setState(() {
if (canAddFavorite) {
print('添加爱心,当前爱心数量:${icons.length}');
icons.add(_p(detail.globalPosition));
widget.onAddFavorite?.call();
justAddFavorite = true;
} else {
justAddFavorite = false;
}
});
},
onTapUp: (detail) {
timer?.cancel();
var delay = canAddFavorite ? 1200 : 600;
timer = Timer(Duration(milliseconds: delay), () {
canAddFavorite = false;
timer = null;
if (!justAddFavorite) {
widget.onSingleTap?.call();
}
});
canAddFavorite = true;
},
onTapCancel: () {
print('onTapCancel');
},
child: Stack(
children: <Widget>[
widget.child,
iconStack,
],
),
);
}
}
class TikTokFavoriteAnimationIcon extends StatefulWidget {
final Offset? position;
final double size;
final Function? onAnimationComplete;
const TikTokFavoriteAnimationIcon({
Key? key,
this.onAnimationComplete,
this.position,
this.size: 100,
}) : super(key: key);
@override
_TikTokFavoriteAnimationIconState createState() =>
_TikTokFavoriteAnimationIconState();
}
class _TikTokFavoriteAnimationIconState
extends State<TikTokFavoriteAnimationIcon> with TickerProviderStateMixin {
AnimationController? _animationController;
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
print('didChangeDependencies');
super.didChangeDependencies();
}
@override
void initState() {
_animationController = AnimationController(
lowerBound: 0,
upperBound: 1,
duration: Duration(milliseconds: 1600),
vsync: this,
);
_animationController!.addListener(() {
setState(() {});
});
startAnimation();
super.initState();
}
startAnimation() async {
await _animationController!.forward();
widget.onAnimationComplete?.call();
}
double rotate = pi / 10.0 * (2 * Random().nextDouble() - 1);
double? get value => _animationController?.value;
double appearDuration = 0.1;
double dismissDuration = 0.8;
double get opa {
if (value! < appearDuration) {
return 0.99 / appearDuration * value!;
}
if (value! < dismissDuration) {
return 0.99;
}
var res = 0.99 - (value! - dismissDuration) / (1 - dismissDuration);
return res < 0 ? 0 : res;
}
double get scale {
if (value! < appearDuration) {
return 1 + appearDuration - value!;
}
if (value! < dismissDuration) {
return 1;
}
return (value! - dismissDuration) / (1 - dismissDuration) + 1;
}
@override
Widget build(BuildContext context) {
Widget content = Icon(
Icons.favorite,
size: widget.size,
color: Colors.redAccent,
);
content = ShaderMask(
child: content,
blendMode: BlendMode.srcATop,
shaderCallback: (Rect bounds) => RadialGradient(
center: Alignment.topLeft.add(Alignment(0.66, 0.66)),
colors: [
Color(0xffEF6F6F),
Color(0xffF03E3E),
],
).createShader(bounds),
);
Widget body = Transform.rotate(
angle: rotate,
child: Opacity(
opacity: opa,
child: Transform.scale(
alignment: Alignment.bottomCenter,
scale: scale,
child: content,
),
),
);
return widget.position == null
? Container()
: Positioned(
left: widget.position!.dx - widget.size / 2,
top: widget.position!.dy - widget.size / 2,
child: body,
);
}
}
......@@ -134,6 +134,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.1"
cupertino_icons:
dependency: "direct main"
description:
......@@ -282,6 +289,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
flutter_spinkit:
dependency: "direct main"
description:
name: flutter_spinkit
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
flutter_swiper_null_safety:
dependency: "direct main"
description:
......@@ -318,6 +332,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
http:
dependency: transitive
description:
......@@ -603,6 +624,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.3"
safemap:
dependency: "direct main"
description:
name: safemap
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
shared_preferences:
dependency: transitive
description:
......@@ -783,6 +811,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
tapped:
dependency: "direct main"
description:
name: tapped
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
term_glyph:
dependency: transitive
description:
......@@ -909,6 +944,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.3-nullsafety.0"
video_player:
dependency: "direct main"
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.10"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
vm_service:
dependency: transitive
description:
......
......@@ -81,6 +81,15 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
# temp
video_player: ^2.1.6
# map取值
safemap: ^2.0.0-nullsafety.0
# 基础的点击
tapped: ^2.0.0-nullsafety.0
# 加载动画库
flutter_spinkit: ^5.0.0
dependency_overrides:
decimal: 1.5.0
......