mod: 视频详情、跳转Hero效果

This commit is contained in:
guozhigq
2023-04-21 11:12:51 +08:00
parent 297020a16f
commit f3b7ad0302
11 changed files with 1188 additions and 16 deletions

View File

@ -15,6 +15,7 @@ class VideoCardV extends StatelessWidget {
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 0.8,
clipBehavior: Clip.hardEdge,
@ -26,7 +27,7 @@ class VideoCardV extends StatelessWidget {
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
Get.toNamed('/video?aid=${videoItem.id}',
arguments: {'videoItem': videoItem});
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
onLongPress: () {
print('长按');
@ -46,12 +47,15 @@ class VideoCardV extends StatelessWidget {
double PR = MediaQuery.of(context).devicePixelRatio;
return Stack(
children: [
NetworkImgLayer(
// 指定图片尺寸
// src: videoItem.pic + '@${(maxWidth * 2).toInt()}w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
Hero(
tag: heroTag,
child: NetworkImgLayer(
// 指定图片尺寸
// src: videoItem.pic + '@${(maxWidth * 2).toInt()}w',
src: videoItem.pic + '@.webp',
width: maxWidth,
height: maxHeight,
),
),
Positioned(
left: 0,

32
lib/http/video.dart Normal file
View File

@ -0,0 +1,32 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/video_detail_res.dart';
class VideoHttp {
// 视频信息 标题、简介
static Future videoDetail(data) async {
var res = await Request().get(Api.videoDetail, data: data);
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
if (result.code == 0) {
return {'status': true, 'data': result.data!};
} else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '无视频',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'msg': errMap[result.code] ?? '请求异常'
};
}
}
// static Future videoRecommend(data) async {
// var res = await Request().get(Api.videoRecommend, data: data);
// return res;
// }
}

View File

@ -0,0 +1,524 @@
import 'dart:convert';
class VideoDetailResponse {
int? code;
String? message;
int? ttl;
VideoDetailData? data;
VideoDetailResponse({
this.code,
this.message,
this.ttl,
this.data,
});
VideoDetailResponse.fromJson(Map<String, dynamic> json) {
code = json["code"];
message = json["message"];
ttl = json["ttl"];
data = json["data"] == null ? null : VideoDetailData.fromJson(json["data"]);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["code"] = code;
data["message"] = message;
data["ttl"] = ttl;
data["data"] = data;
return data;
}
}
class VideoDetailData {
String? bvid;
int? aid;
int? videos;
int? tid;
String? tname;
int? copyright;
String? pic;
String? title;
int? pubdate;
int? ctime;
String? desc;
List<DescV2>? descV2;
int? state;
int? duration;
Map<String, int>? rights;
Owner? owner;
Stat? stat;
String? videoDynamic;
int? cid;
Dimension? dimension;
dynamic premiere;
int? teenageMode;
bool? isChargeableSeason;
bool? isStory;
bool? noCache;
List<Page>? pages;
Subtitle? subtitle;
// Label? label;
bool? isSeasonDisplay;
UserGarb? userGarb;
HonorReply? honorReply;
String? likeIcon;
bool? needJumpBv;
VideoDetailData({
this.bvid,
this.aid,
this.videos,
this.tid,
this.tname,
this.copyright,
this.pic,
this.title,
this.pubdate,
this.ctime,
this.desc,
this.descV2,
this.state,
this.duration,
this.rights,
this.owner,
this.stat,
this.videoDynamic,
this.cid,
this.dimension,
this.premiere,
this.teenageMode,
this.isChargeableSeason,
this.isStory,
this.noCache,
this.pages,
this.subtitle,
this.isSeasonDisplay,
this.userGarb,
this.honorReply,
this.likeIcon,
this.needJumpBv,
});
VideoDetailData.fromJson(Map<String, dynamic> json) {
bvid = json["bvid"];
aid = json["aid"];
videos = json["videos"];
tid = json["tid"];
tname = json["tname"];
copyright = json["copyright"];
pic = json["pic"];
title = json["title"];
pubdate = json["pubdate"];
ctime = json["ctime"];
desc = json["desc"];
descV2 = json["desc_v2"] == null
? []
: List<DescV2>.from(json["desc_v2"]!.map((e) => DescV2.fromJson(e)));
state = json["state"];
duration = json["duration"];
rights =
Map.from(json["rights"]!).map((k, v) => MapEntry<String, int>(k, v));
owner = json["owner"] == null ? null : Owner.fromJson(json["owner"]);
stat = json["stat"] == null ? null : Stat.fromJson(json["stat"]);
videoDynamic = json["dynamic"];
cid = json["cid"];
dimension = json["dimension"] == null
? null
: Dimension.fromJson(json["dimension"]);
premiere = json["premiere"];
teenageMode = json["teenage_mode"];
isChargeableSeason = json["is_chargeable_season"];
isStory = json["is_story"];
noCache = json["no_cache"];
pages = json["pages"] == null
? []
: List<Page>.from(json["pages"]!.map((e) => Page.fromJson(e)));
subtitle =
json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]);
isSeasonDisplay = json["is_season_display"];
userGarb =
json["user_garb"] == null ? null : UserGarb.fromJson(json["user_garb"]);
honorReply = json["honor_reply"] == null
? null
: HonorReply.fromJson(json["honor_reply"]);
likeIcon = json["like_icon"];
needJumpBv = json["need_jump_bv"];
}
Map<String, dynamic> toJson() => {
"bvid": bvid,
"aid": aid,
"videos": videos,
"tid": tid,
"tname": tname,
"copyright": copyright,
"pic": pic,
"title": title,
"pubdate": pubdate,
"ctime": ctime,
"desc": desc,
"desc_v2": descV2 == null
? []
: List<dynamic>.from(descV2!.map((e) => e.toJson())),
"state": state,
"duration": duration,
"rights":
Map.from(rights!).map((k, v) => MapEntry<String, dynamic>(k, v)),
"owner": owner?.toJson(),
"stat": stat?.toJson(),
"dynamic": videoDynamic,
"cid": cid,
"dimension": dimension?.toJson(),
"premiere": premiere,
"teenage_mode": teenageMode,
"is_chargeable_season": isChargeableSeason,
"is_story": isStory,
"no_cache": noCache,
"pages": pages == null
? []
: List<dynamic>.from(pages!.map((e) => e.toJson())),
"subtitle": subtitle?.toJson(),
"is_season_display": isSeasonDisplay,
"user_garb": userGarb?.toJson(),
"honor_reply": honorReply?.toJson(),
"like_icon": likeIcon,
"need_jump_bv": needJumpBv,
};
}
class DescV2 {
String? rawText;
int? type;
int? bizId;
DescV2({
this.rawText,
this.type,
this.bizId,
});
fromRawJson(String str) {
return DescV2.fromJson(json.decode(str));
}
String toRawJson() => json.encode(toJson());
DescV2.fromJson(Map<String, dynamic> json) {
rawText = json["raw_text"];
type = json["type"];
bizId = json["biz_id"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["raw_text"] = rawText;
data["type"] = type;
data["biz_id"] = bizId;
return data;
}
}
class Dimension {
int? width;
int? height;
int? rotate;
Dimension({
this.width,
this.height,
this.rotate,
});
fromRawJson(String str) => Dimension.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Dimension.fromJson(Map<String, dynamic> json) {
width = json["width"];
height = json["height"];
rotate = json["rotate"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["width"] = width;
data["height"] = height;
data["rotate"] = rotate;
data["data"] = data;
return data;
}
}
class HonorReply {
List<Honor>? honor;
HonorReply({
this.honor,
});
fromRawJson(String str) => HonorReply.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
HonorReply.fromJson(Map<String, dynamic> json) {
honor = json["honor"] == null
? []
: List<Honor>.from(json["honor"]!.map((x) => Honor.fromJson(x)));
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["honor"] =
honor == null ? [] : List<dynamic>.from(honor!.map((x) => x.toJson()));
return data;
}
}
class Honor {
int? aid;
int? type;
String? desc;
int? weeklyRecommendNum;
Honor({
this.aid,
this.type,
this.desc,
this.weeklyRecommendNum,
});
fromRawJson(String str) => Honor.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Honor.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
type = json["type"];
desc = json["desc"];
weeklyRecommendNum = json["weekly_recommend_num"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["aid"] = aid;
data["type"] = type;
data["desc"] = desc;
data["weekly_recommend_num"] = weeklyRecommendNum;
return data;
}
}
class Owner {
int? mid;
String? name;
String? face;
Owner({
this.mid,
this.name,
this.face,
});
fromRawJson(String str) => Owner.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Owner.fromJson(Map<String, dynamic> json) {
mid = json["mid"];
name = json["name"];
face = json["face"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["mid"] = mid;
data["name"] = name;
data["face"] = face;
return data;
}
}
class Page {
int? cid;
int? page;
String? from;
String? pagePart;
int? duration;
String? vid;
String? weblink;
Dimension? dimension;
String? firstFrame;
Page({
this.cid,
this.page,
this.from,
this.pagePart,
this.duration,
this.vid,
this.weblink,
this.dimension,
this.firstFrame,
});
fromRawJson(String str) => Page.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Page.fromJson(Map<String, dynamic> json) {
cid = json["cid"];
page = json["page"];
from = json["from"];
pagePart = json["part"];
duration = json["duration"];
vid = json["vid"];
weblink = json["weblink"];
dimension = json["dimension"] == null
? null
: Dimension.fromJson(json["dimension"]);
firstFrame = json["first_frame"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["cid"] = cid;
data["page"] = page;
data["from"] = from;
data["part"] = pagePart;
data["duration"] = duration;
data["vid"] = vid;
data["weblink"] = weblink;
data["dimension"] = dimension?.toJson();
data["first_frame"] = firstFrame;
return data;
}
}
class Stat {
int? aid;
int? view;
int? danmaku;
int? reply;
int? favorite;
int? coin;
int? share;
int? nowRank;
int? hisRank;
int? like;
int? dislike;
String? evaluation;
String? argueMsg;
Stat({
this.aid,
this.view,
this.danmaku,
this.reply,
this.favorite,
this.coin,
this.share,
this.nowRank,
this.hisRank,
this.like,
this.dislike,
this.evaluation,
this.argueMsg,
});
fromRawJson(String str) => Stat.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Stat.fromJson(Map<String, dynamic> json) {
aid = json["aid"];
view = json["view"];
danmaku = json["danmaku"];
reply = json["reply"];
favorite = json["favorite"];
coin = json["coin"];
share = json["share"];
nowRank = json["now_rank"];
hisRank = json["his_rank"];
like = json["like"];
dislike = json["dislike"];
evaluation = json["evaluation"];
argueMsg = json["argue_msg"];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["aid"] = aid;
data["view"] = view;
data["danmaku"] = danmaku;
data["reply"] = reply;
data["favorite"] = favorite;
data["coin"] = coin;
data["share"] = share;
data["now_rank"] = nowRank;
data["his_rank"] = hisRank;
data["like"] = like;
data["dislike"] = dislike;
data["evaluation"] = evaluation;
data["argue_msg"] = argueMsg;
return data;
}
}
class Subtitle {
bool? allowSubmit;
List<dynamic>? list;
Subtitle({
this.allowSubmit,
this.list,
});
fromRawJson(String str) => Subtitle.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
Subtitle.fromJson(Map<String, dynamic> json) {
allowSubmit = json["allow_submit"];
list = json["list"] == null
? []
: List<dynamic>.from(json["list"]!.map((x) => x));
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["allow_submit"] = allowSubmit;
data["list"] = list == null ? [] : List<dynamic>.from(list!.map((x) => x));
return data;
}
}
class UserGarb {
String? urlImageAniCut;
UserGarb({
this.urlImageAniCut,
});
fromRawJson(String str) => UserGarb.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
UserGarb.fromJson(Map<String, dynamic> json) {
urlImageAniCut = json["url_image_ani_cut"];
}
Map<String, dynamic> toJson() => {"url_image_ani_cut": urlImageAniCut};
}
class Label {}

View File

@ -47,7 +47,6 @@ class HomeController extends GetxController {
// 上拉加载
Future onLoad() async {
await Future.delayed(const Duration(milliseconds: 500));
queryRcmdFeed('onLoad');
}

View File

@ -13,6 +13,7 @@ class VideoDetailController extends GetxController {
// 请求状态
RxBool isLoading = false.obs;
String heroTag = '';
@override
void onInit() {
super.onInit();
@ -24,6 +25,7 @@ class VideoDetailController extends GetxController {
videoItem['pic'] = args.pic;
}
}
heroTag = Get.arguments['heroTag'];
}
}
}

View File

@ -0,0 +1,47 @@
import 'package:get/get.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/video_detail_res.dart';
class VideoIntroController extends GetxController {
// 视频aid
String aid = Get.parameters['aid']!;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
Map? videoItem = {};
// 请求状态
RxBool isLoading = false.obs;
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
@override
void onInit() {
super.onInit();
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
videoItem!['pic'] = args.pic;
videoItem!['title'] = args.title;
videoItem!['stat'] = args.stat;
videoItem!['pubdate'] = args.pubdate;
videoItem!['owner'] = args.owner;
}
}
}
Future queryVideoDetail() async {
var res = await Request().get(Api.videoDetail, data: {
'aid': aid,
});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
videoDetail.value = result.data!;
// await Future.delayed(const Duration(seconds: 3));
return true;
}
}

View File

@ -0,0 +1,4 @@
library video_detail_introduction;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,471 @@
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/utils/utils.dart';
class VideoIntroPanel extends StatefulWidget {
const VideoIntroPanel({Key? key}) : super(key: key);
@override
State<VideoIntroPanel> createState() => _VideoIntroPanelState();
}
class _VideoIntroPanelState extends State<VideoIntroPanel> {
final VideoIntroController videoIntroController =
Get.put(VideoIntroController());
VideoDetailData? videoDetail;
@override
void initState() {
super.initState();
videoIntroController.videoDetail.listen((value) {
videoDetail = value;
});
}
@override
void dispose() {
videoIntroController.onClose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: videoIntroController.queryVideoDetail(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
print(snapshot.data);
if (snapshot.data) {
// 请求成功
return _buildView(context, false, videoDetail);
} else {
// 请求错误
return Center(
child: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {});
},
),
);
}
} else {
return _buildView(context, true, videoDetail);
}
},
);
}
Widget _buildView(context, loadingStatus, videoDetail) {
// return CustomScrollView(
// key: const PageStorageKey<String>('简介'),
// slivers: <Widget>[
// SliverOverlapInjector(
// handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
// VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail),
// SliverToBoxAdapter(
// child:
// Divider(color: Theme.of(context).dividerColor.withOpacity(0.1)),
// ),
// const RecommendList()
// ],
// );
return VideoInfo(loadingStatus: loadingStatus, videoDetail: videoDetail);
}
}
class VideoInfo extends StatefulWidget {
bool loadingStatus = false;
VideoDetailData? videoDetail;
VideoInfo({Key? key, required this.loadingStatus, this.videoDetail})
: super(key: key);
@override
State<VideoInfo> createState() => _VideoInfoState();
}
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Map videoItem = Get.put(VideoIntroController()).videoItem!;
bool isExpand = false;
/// 手动控制动画的控制器
late AnimationController? _manualController;
/// 手动控制
late Animation<double>? _manualAnimation;
@override
void initState() {
super.initState();
/// 不设置重复使用代码控制进度动画时间1秒
_manualController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_manualAnimation =
Tween<double>(begin: 0.5, end: 1.5).animate(_manualController!);
}
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 25),
sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face,
width: 38,
height: 38,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name),
const SizedBox(height: 2),
// Text.rich(
// TextSpan(
// style: TextStyle(
// color: Theme.of(context)
// .colorScheme
// .outline,
// fontSize: 11),
// children: const [
// TextSpan(text: '2.6万粉丝'),
// TextSpan(text: ' '),
// TextSpan(text: '2.6万粉丝'),
// ]),
// ),
]),
const Spacer(),
AnimatedOpacity(
opacity: widget.loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 35,
child: ElevatedButton(
onPressed: () {}, child: const Text('+ 关注')),
),
),
const SizedBox(width: 4),
],
),
const SizedBox(height: 18),
// 标题 超过两行收起
// Container(
// color: Colors.blue[50],
// child: SizedOverflowBox(
// size: const Size(50.0, 50.0),
// alignment: AlignmentDirectional.bottomStart,
// child: Container(height: 150.0, width: 150.0, color: Colors.blue,),
// ),
// ),
// Row(
// children: [
// Expanded(
// child: ExpandedSection(
// expand: false,
// begin: 1,
// end: 1,
// child: Text(
// !widget.loadingStatus
// ? widget.videoDetail!.title
// : videoItem['title'],
// overflow: TextOverflow.ellipsis,
// maxLines: 1,
// ),
// ),
// ),
// const SizedBox(width: 10),
// RotationTransition(
// turns: _manualAnimation!,
// child: IconButton(
// onPressed: () {
// /// 获取动画当前的值
// var value = _manualController!.value;
// /// 0.5代表 180弧度
// if (value == 0) {
// _manualController!.animateTo(0.5);
// } else {
// _manualController!.animateTo(0);
// }
// setState(() {
// isExpand = !isExpand;
// });
// },
// icon: const Icon(Icons.expand_less)),
// ),
// ],
// ),
SizedBox(
width: double.infinity,
child: Text(
!widget.loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
// style: Theme.of(context).textTheme.titleMedium,
// maxLines: 2,
),
),
// const SizedBox(height: 5),
// 播放量、评论、日期
Row(
children: [
const SizedBox(width: 2),
StatView(
theme: 'gray',
view: !widget.loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline),
),
const Spacer(),
RotationTransition(
turns: _manualAnimation!,
child: IconButton(
onPressed: () {
/// 获取动画当前的值
var value = _manualController!.value;
/// 0.5代表 180弧度
if (value == 0) {
_manualController!.animateTo(0.5);
} else {
_manualController!.animateTo(0);
}
setState(() {
isExpand = !isExpand;
});
},
icon: Icon(
Icons.expand_less,
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
// const SizedBox(height: 5),
// 简介 默认收起
if (!widget.loadingStatus)
ExpandedSection(
expand: isExpand,
begin: 0.0,
end: 1.0,
child: DefaultTextStyle(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
height: 1.5,
fontSize:
Theme.of(context).textTheme.labelMedium?.fontSize,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SelectableRegion(
magnifierConfiguration:
const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.videoDetail!.bvid!),
Text(widget.videoDetail!.desc!),
],
),
),
),
),
),
_actionGrid(context),
const SizedBox(height: 5),
],
)
: const Center(child: CircularProgressIndicator()),
),
);
}
// 喜欢 投币 分享
Widget _actionGrid(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Container(
color: Colors.black12,
height: constraints.maxWidth / 5,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
children: <Widget>[
ActionItem(
icon: const Icon(Icons.thumb_up),
onTap: () => {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
ActionItem(
icon: const Icon(Icons.thumb_down),
onTap: () => {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: '不喜欢'),
ActionItem(
icon: const Icon(Icons.generating_tokens),
onTap: () => {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
ActionItem(
icon: const Icon(Icons.star),
onTap: () => {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
ActionItem(
icon: const Icon(Icons.share),
onTap: () => {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.share!.toString()
: '-'),
],
),
);
});
}
}
class ActionItem extends StatelessWidget {
Icon? icon;
Function? onTap;
bool? loadingStatus;
String? text;
bool selectStatus = false;
ActionItem({
Key? key,
this.icon,
this.onTap,
this.loadingStatus,
this.text,
required this.selectStatus,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
child: Ink(
child: InkWell(
onTap: () {},
borderRadius: StyleString.mdRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon!.icon!,
color: selectStatus
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.outline),
const SizedBox(height: 2),
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: Text(
text!,
style: TextStyle(
color: selectStatus
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall?.fontSize),
),
),
],
),
),
));
}
}
class RecommendList extends StatelessWidget {
const RecommendList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return Material(
child: InkWell(
onTap: () {},
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text(
'$index」 求推荐一些高质量的系统地介绍 ChatGPT 及相关技术的视频、文章或者书',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(height: 1.6),
),
),
),
);
}, childCount: 50),
);
}
}
class ActionGrid extends StatelessWidget {
const ActionGrid({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -2,6 +2,7 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart';
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@ -52,10 +53,13 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
double PR = MediaQuery.of(context).devicePixelRatio;
return NetworkImgLayer(
src: videoDetailController.videoItem['pic'],
width: maxWidth,
height: maxHeight,
return Hero(
tag: videoDetailController.heroTag,
child: NetworkImgLayer(
src: videoDetailController.videoItem['pic'],
width: maxWidth,
height: maxHeight,
),
);
},
),
@ -112,10 +116,7 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
// const VideoIntroPanel(),
const SliverToBoxAdapter(
child: Text('简介'),
)
const VideoIntroPanel(),
],
);
}),

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
class ExpandedSection extends StatefulWidget {
final Widget child;
final bool expand;
double begin = 0.0;
double end = 1.0;
ExpandedSection(
{this.expand = false,
required this.child,
required this.begin,
required this.end});
@override
_ExpandedSectionState createState() => _ExpandedSectionState();
}
class _ExpandedSectionState extends State<ExpandedSection>
with SingleTickerProviderStateMixin {
late AnimationController expandController;
late Animation<double> animation;
@override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
// void prepareAnimations() {
// expandController = AnimationController(
// vsync: this, duration: const Duration(milliseconds: 500));
// animation = CurvedAnimation(
// parent: expandController,
// curve: Curves.fastOutSlowIn,
// );
// }
void prepareAnimations() {
expandController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 400));
Animation<double> curve = CurvedAnimation(
parent: expandController,
curve: Curves.fastOutSlowIn,
);
animation = Tween(begin: widget.begin, end: widget.end).animate(curve);
// animation = CurvedAnimation(
// parent: expandController,
// curve: Curves.fastOutSlowIn,
// );
}
void _runExpandCheck() {
if (widget.expand) {
expandController.forward();
} else {
expandController.reverse();
}
}
@override
void didUpdateWidget(ExpandedSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
@override
void dispose() {
expandController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
axisAlignment: -1.0,
sizeFactor: animation,
child: widget.child,
);
}
}

View File

@ -1,5 +1,6 @@
// 工具函数
import 'dart:io';
import 'dart:math';
import 'package:get/get_utils/get_utils.dart';
import 'package:path_provider/path_provider.dart';
@ -130,4 +131,8 @@ class Utils {
}
return date;
}
static String makeHeroTag(v) {
return v.toString() + Random().nextInt(9999).toString();
}
}