From 39e8d4669ce5326b59c98e3dec94b2d73b23651e Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 19 Apr 2023 15:47:20 +0800 Subject: [PATCH] mod: longPress enlarge img --- lib/common/widgets/animated_dialog.dart | 53 +++++++++ lib/common/widgets/overlay_pop.dart | 43 +++++++ lib/common/widgets/video_card_h.dart | 142 +++++++++++++----------- lib/common/widgets/video_card_v.dart | 110 ++++++++++-------- lib/pages/home/controller.dart | 1 + lib/pages/home/view.dart | 25 ++++- lib/pages/hot/controller.dart | 1 + lib/pages/hot/view.dart | 18 +++ 8 files changed, 280 insertions(+), 113 deletions(-) create mode 100644 lib/common/widgets/animated_dialog.dart create mode 100644 lib/common/widgets/overlay_pop.dart 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 4003ce3b..671a3d92 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -8,78 +8,94 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; // 视频卡片 - 水平布局 class VideoCardH extends StatelessWidget { 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) { return Material( child: Ink( - child: InkWell( - onTap: () async { - await Future.delayed(const Duration(milliseconds: 200)); - int aid = videoItem['id'] ?? videoItem['aid']; - Get.toNamed('/video?aid=$aid', arguments: {'videoItem': videoItem}); + child: GestureDetector( + onLongPress: () { + longPress!(); }, - child: Container( - padding: const EdgeInsets.fromLTRB( - StyleString.cardSpace, 5, StyleString.cardSpace, 5), - 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: ClipRRect( - // borderRadius: StyleString.mdRadius, - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - 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, - ), - // 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) { + longPressEnd!(); + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + int aid = videoItem['id'] ?? videoItem['aid']; + Get.toNamed('/video?aid=$aid', + arguments: {'videoItem': videoItem}); + }, + child: Container( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 5, StyleString.cardSpace, 5), + 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: ClipRRect( + // borderRadius: StyleString.mdRadius, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + 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, ), - ) - ], - ); - }, + // 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) - ], - ), - ); - }), - // height: 124, + VideoContent(videoItem: videoItem) + ], + ), + ); + }), + // height: 124, + ), ), ), ), diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 8481e540..3165fbec 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) { @@ -22,58 +29,63 @@ 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}); - }, + child: GestureDetector( onLongPress: () { - print('长按'); + 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: [ - 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) { + longPressEnd!(); + }, + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=${videoItem.id}', + arguments: {'videoItem': videoItem}); + }, + 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: [ + 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 32aa8111..f6cf6f0d 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -12,6 +12,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 fba2f82c..f9882381 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,7 +1,11 @@ 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/network_img_layer.dart'; +import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; +import 'package:pilipala/models/models_rec_video_item.dart'; import './controller.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/pages/home/widgets/app_bar.dart'; @@ -84,7 +88,18 @@ class _HomePageState extends State delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return videoList.isNotEmpty - ? 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, @@ -98,6 +113,14 @@ class _HomePageState extends State // ), ); } + + OverlayEntry _createPopupDialog(RecVideoItemModel videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + ), + ); + } } class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 1df71824..8464ee10 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -11,6 +11,7 @@ class HotController extends GetxController { RxList videoList = [].obs; bool isLoadingMore = false; bool flag = false; + OverlayEntry? popupDialog; @override void onInit() { diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 29a0b177..f0a5ab0b 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/widgets/video_card_h.dart'; import 'package:pilipala/pages/hot/controller.dart'; import 'package:pilipala/pages/home/widgets/app_bar.dart'; @@ -56,6 +58,14 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { delegate: SliverChildBuilderDelegate((context, index) { return VideoCardH( videoItem: videoList[index], + longPress: () { + _hotController.popupDialog = + _createPopupDialog(videoList[index]); + Overlay.of(context).insert(_hotController.popupDialog!); + }, + longPressEnd: () { + _hotController.popupDialog?.remove(); + }, ); }, childCount: videoList.length)), SliverToBoxAdapter( @@ -68,4 +78,12 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { ), ); } + + OverlayEntry _createPopupDialog(videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + child: OverlayPop(videoItem: videoItem), + ), + ); + } }