diff --git a/lib/common/widgets/animated_dialog.dart b/lib/common/widgets/animated_dialog.dart new file mode 100644 index 00000000..4d35e3a0 --- /dev/null +++ b/lib/common/widgets/animated_dialog.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class AnimatedDialog extends StatefulWidget { + const AnimatedDialog({Key? key, required this.child}) : super(key: key); + + final Widget child; + + @override + State createState() => AnimatedDialogState(); +} + +class AnimatedDialogState extends State + with SingleTickerProviderStateMixin { + late AnimationController? controller; + late Animation? opacityAnimation; + late Animation? scaleAnimation; + + @override + void initState() { + super.initState(); + + controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 800)); + opacityAnimation = Tween(begin: 0.0, end: 0.6).animate( + CurvedAnimation(parent: controller!, curve: Curves.easeOutExpo)); + scaleAnimation = + CurvedAnimation(parent: controller!, curve: Curves.easeOutExpo); + controller!.addListener(() => setState(() {})); + controller!.forward(); + } + + @override + void dispose() { + controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.black.withOpacity(opacityAnimation!.value), + child: Center( + child: FadeTransition( + opacity: scaleAnimation!, + child: ScaleTransition( + scale: scaleAnimation!, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/overlay_pop.dart b/lib/common/widgets/overlay_pop.dart new file mode 100644 index 00000000..91212d88 --- /dev/null +++ b/lib/common/widgets/overlay_pop.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; + +class OverlayPop extends StatelessWidget { + var videoItem; + OverlayPop({super.key, this.videoItem}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6.0), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetworkImgLayer( + width: (MediaQuery.of(context).size.width - 16), + height: (MediaQuery.of(context).size.width - 16) / + StyleString.aspectRatio, + src: videoItem.pic!, + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 15, 10, 15), + child: Text( + videoItem.title!, + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 1217e700..690abe7c 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -9,89 +9,111 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; class VideoCardH extends StatelessWidget { // ignore: prefer_typing_uninitialized_variables var videoItem; + Function()? longPress; + Function()? longPressEnd; - VideoCardH({Key? key, required this.videoItem}) : super(key: key); + VideoCardH({ + Key? key, + required this.videoItem, + this.longPress, + this.longPressEnd, + }) : super(key: key); @override Widget build(BuildContext context) { int aid = videoItem.aid; String heroTag = Utils.makeHeroTag(aid); - return InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - Get.toNamed('/video?aid=$aid', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + return GestureDetector( + onLongPress: () { + if (longPress != null) { + longPress!(); + } }, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - StyleString.cardSpace, 7, StyleString.cardSpace, 7), - child: LayoutBuilder( - builder: (context, boxConstraints) { - double width = - (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; - return SizedBox( - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - // src: videoItem['pic'] + - // '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, - ), - ), - // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), - Positioned( - right: 4, - bottom: 4, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 1, horizontal: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.black54.withOpacity(0.4)), - child: Text( - Utils.timeFormat(videoItem.duration!), - style: const TextStyle( - fontSize: 11, color: Colors.white), + onLongPressEnd: (details) { + if (longPressEnd != null) { + longPressEnd!(); + } + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$aid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = + MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, ), ), - ) - ], - ); - }, + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + color: + Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem.duration!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), ), - ), - VideoContent(videoItem: videoItem) - ], - ), - ); - }, + VideoContent(videoItem: videoItem) + ], + ), + ); + }, + ), ), - ), - Divider( - height: 1, - indent: 8, - endIndent: 12, - color: Theme.of(context).dividerColor.withOpacity(0.08), - ) - ], + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), ), ); } diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index e0aa9ff4..8e182a14 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -10,8 +10,15 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 垂直布局 class VideoCardV extends StatelessWidget { var videoItem; + Function()? longPress; + Function()? longPressEnd; - VideoCardV({Key? key, required this.videoItem}) : super(key: key); + VideoCardV({ + Key? key, + required this.videoItem, + this.longPress, + this.longPressEnd, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -23,61 +30,70 @@ class VideoCardV extends StatelessWidget { borderRadius: StyleString.mdRadius, ), margin: EdgeInsets.zero, - child: InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - Get.toNamed('/video?aid=${videoItem.id}', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); - }, + child: GestureDetector( onLongPress: () { - print('长按'); + if (longPress != null) { + longPress!(); + } }, - child: Column( - children: [ - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: StyleString.imgRadius, - topRight: StyleString.imgRadius, - ), - child: AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - // 指定图片尺寸 - // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', - src: videoItem.pic + '@.webp', - width: maxWidth, - height: maxHeight, - ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 200), - child: VideoStat( - view: videoItem.stat.view, - danmaku: videoItem.stat.danmaku, - duration: videoItem.duration, + onLongPressEnd: (details) { + if (longPressEnd != null) { + longPressEnd!(); + } + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=${videoItem.id}', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: StyleString.imgRadius, + topRight: StyleString.imgRadius, + ), + child: AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // 指定图片尺寸 + // src: videoItem.pic + '@${(maxWidth * 2).toInt()}w', + src: videoItem.pic + '@.webp', + width: maxWidth, + height: maxHeight, ), ), - ) - ], - ); - }), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: VideoStat( + view: videoItem.stat.view, + danmaku: videoItem.stat.danmaku, + duration: videoItem.duration, + ), + ), + ), + ], + ); + }), + ), ), - ), - VideoContent(videoItem: videoItem) - ], + VideoContent(videoItem: videoItem) + ], + ), ), ), ); diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 7182b134..7d05e312 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -11,6 +11,7 @@ class HomeController extends GetxController { RxList videoList = [RecVideoItemModel()].obs; bool isLoadingMore = false; bool flag = false; + OverlayEntry? popupDialog; @override void onInit() { diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 0d08ed3e..37bd73d2 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; +import 'package:pilipala/common/widgets/animated_dialog.dart'; +import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; @@ -101,6 +103,13 @@ class _HomePageState extends State ); } + OverlayEntry _createPopupDialog(videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + )); + } + Widget contentGrid(ctr, videoList) { return SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( @@ -118,7 +127,19 @@ class _HomePageState extends State delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return videoList!.isNotEmpty - ? VideoCardV(videoItem: videoList![index]) + ? + // VideoCardV(videoItem: videoList![index]) + VideoCardV( + videoItem: videoList[index], + longPress: () { + _homeController.popupDialog = + _createPopupDialog(videoList[index]); + Overlay.of(context).insert(_homeController.popupDialog!); + }, + longPressEnd: () { + _homeController.popupDialog?.remove(); + }, + ) : const VideoCardVSkeleton(); }, childCount: videoList!.isNotEmpty ? videoList!.length : 10, diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 7f475c2d..65706c32 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -10,6 +10,7 @@ class HotController extends GetxController { RxList videoList = [HotVideoItemModel()].obs; bool isLoadingMore = false; bool flag = false; + OverlayEntry? popupDialog; // 获取推荐 Future queryHotFeed(type) async { diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 51abee1e..3a8c51af 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/animated_dialog.dart'; +import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; @@ -62,6 +64,15 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { delegate: SliverChildBuilderDelegate((context, index) { return VideoCardH( videoItem: _hotController.videoList[index], + longPress: () { + _hotController.popupDialog = _createPopupDialog( + _hotController.videoList[index]); + Overlay.of(context) + .insert(_hotController.popupDialog!); + }, + longPressEnd: () { + _hotController.popupDialog?.remove(); + }, ); }, childCount: _hotController.videoList.length), ), @@ -92,4 +103,12 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { ), ); } + + OverlayEntry _createPopupDialog(videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + ), + ); + } } diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart index cb9081dc..20cf3b55 100644 --- a/lib/pages/video/detail/related/controller.dart +++ b/lib/pages/video/detail/related/controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/video.dart'; @@ -7,5 +8,7 @@ class ReleatedController extends GetxController { // 推荐视频列表 List relatedVideoList = []; + OverlayEntry? popupDialog; + Future queryRelatedVideo() => VideoHttp.relatedVideoList(aid: aid); } diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 3c79ee50..2fc11573 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/animated_dialog.dart'; +import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; -import 'package:pilipala/common/widgets/video_card_v.dart'; import './controller.dart'; class RelatedVideoPanel extends StatefulWidget { @@ -31,6 +32,15 @@ class _RelatedVideoPanelState extends State { } else { return VideoCardH( videoItem: snapshot.data['data'][index], + longPress: () { + _releatedController.popupDialog = + _createPopupDialog(snapshot.data['data'][index]); + Overlay.of(context) + .insert(_releatedController.popupDialog!); + }, + longPressEnd: () { + _releatedController.popupDialog?.remove(); + }, ); } }, childCount: snapshot.data['data'].length + 1)); @@ -51,4 +61,12 @@ class _RelatedVideoPanelState extends State { }, ); } + + OverlayEntry _createPopupDialog(videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + ), + ); + } }