diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index a463774a2..1c4649ba5 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -22,7 +22,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; abstract class CommonDynPageState extends State - with SingleTickerProviderStateMixin, FabMixin { + with SingleTickerProviderStateMixin, BaseFabMixin, FabMixin { CommonDynController get controller; late final ScrollController scrollController; diff --git a/lib/pages/common/fab_mixin.dart b/lib/pages/common/fab_mixin.dart index 50255f8c7..365c823e0 100644 --- a/lib/pages/common/fab_mixin.dart +++ b/lib/pages/common/fab_mixin.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; -mixin FabMixin on State, TickerProvider { - bool _isFabVisible = true; - late final AnimationController _fabAnimationCtr; - late final Animation fabAnimation; +mixin BaseFabMixin on State, TickerProvider { + late bool _isFabVisible = true; + AnimationController get fabAnimationCtr; + Animation get fabAnimation; - @override - void initState() { - super.initState(); - _fabAnimationCtr = AnimationController( + AnimationController _initController() { + return AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); - fabAnimation = _fabAnimationCtr.drive( + } + + Animation _initAnimation() { + return fabAnimationCtr.drive( Tween( begin: Offset.zero, end: const Offset(0.0, 1.0), @@ -23,20 +24,51 @@ mixin FabMixin on State, TickerProvider { void showFab() { if (!_isFabVisible) { _isFabVisible = true; - _fabAnimationCtr.reverse(); + fabAnimationCtr.reverse(); } } void hideFab() { if (_isFabVisible) { _isFabVisible = false; - _fabAnimationCtr.forward(); + fabAnimationCtr.forward(); } } +} + +mixin FabMixin on BaseFabMixin { + @override + late final AnimationController fabAnimationCtr; + @override + late final Animation fabAnimation; + + @override + void initState() { + super.initState(); + fabAnimationCtr = _initController(); + fabAnimation = _initAnimation(); + } @override void dispose() { - _fabAnimationCtr.dispose(); + fabAnimationCtr.dispose(); + super.dispose(); + } +} + +mixin LazyFabMixin on BaseFabMixin { + AnimationController? _fabAnimationCtr; + Animation? _fabAnimation; + + @override + AnimationController get fabAnimationCtr => + _fabAnimationCtr ??= _initController(); + @override + Animation get fabAnimation => _fabAnimation ??= _initAnimation(); + + @override + void dispose() { + _fabAnimationCtr?.dispose(); super.dispose(); } } diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 9249e2d68..f292e74ae 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -11,6 +11,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics_create/view.dart'; import 'package:PiliPlus/pages/dynamics_topic/controller.dart'; @@ -38,7 +39,8 @@ class DynTopicPage extends StatefulWidget { State createState() => _DynTopicPageState(); } -class _DynTopicPageState extends State with DynMixin { +class _DynTopicPageState extends State + with DynMixin, SingleTickerProviderStateMixin, BaseFabMixin, FabMixin { final DynTopicController _controller = Get.put( DynTopicController(), tag: Utils.generateRandomString(8), @@ -48,102 +50,130 @@ class _DynTopicPageState extends State with DynMixin { Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - if (_controller.isLogin) { - CreateDynPanel.onCreateDyn( - context, - topic: Pair( - first: int.parse(_controller.topicId), - second: _controller.topicName, - ), - ); - } else { - SmartDialog.showToast('账号未登录'); - } - }, - icon: const Icon(CustomIcons.topic_tag, size: 20), - label: const Text('参与话题'), - ), - body: refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - controller: _controller.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - Obx( - () => _buildAppBar( - colorScheme, - padding, - _controller.topState.value, - ), - ), - Obx(() { - final allSortBy = _controller.topicSortByConf.value?.allSortBy; - if (allSortBy != null && allSortBy.isNotEmpty) { - return SliverPinnedHeader( - backgroundColor: colorScheme.surface, - child: Padding( - padding: EdgeInsets.only( - left: 12 + padding.left, - top: 6, - bottom: 6, - ), - child: Builder( - builder: (context) { - return ToggleButtons( - fillColor: colorScheme.secondaryContainer, - selectedColor: colorScheme.onSecondaryContainer, - constraints: const BoxConstraints( - minWidth: 54, - minHeight: 24, - ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - borderRadius: const .all(.circular(25)), - onPressed: (index) { - _controller.onSort(allSortBy[index].sortBy!); - (context as Element).markNeedsBuild(); - }, - isSelected: allSortBy - .map((e) => e.sortBy == _controller.sortBy) - .toList(), - children: allSortBy.map((e) { - return Text( - e.sortName!, - style: const TextStyle( - fontSize: 13, - height: 1, - ), - strutStyle: const StrutStyle( - height: 1, - leading: 0, - fontSize: 13, - ), - textScaler: TextScaler.noScaling, - ); - }).toList(), - ); - }, + return Material( + child: Stack( + clipBehavior: .none, + children: [ + refreshIndicator( + onRefresh: _controller.onRefresh, + child: NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: CustomScrollView( + controller: _controller.scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + Obx( + () => _buildAppBar( + colorScheme, + padding, + _controller.topState.value, ), ), - ); - } - return const SliverToBoxAdapter(); - }), - SliverPadding( - padding: EdgeInsets.only( - left: padding.left, - right: padding.right, - bottom: padding.bottom + 100, - ), - sliver: buildPage( - Obx(() => _buildBody(_controller.loadingState.value)), + Obx(() { + final allSortBy = + _controller.topicSortByConf.value?.allSortBy; + if (allSortBy != null && allSortBy.isNotEmpty) { + return SliverPinnedHeader( + backgroundColor: colorScheme.surface, + child: Padding( + padding: EdgeInsets.only( + left: 12 + padding.left, + top: 6, + bottom: 6, + ), + child: Builder( + builder: (context) { + return ToggleButtons( + fillColor: colorScheme.secondaryContainer, + selectedColor: colorScheme.onSecondaryContainer, + constraints: const BoxConstraints( + minWidth: 54, + minHeight: 24, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + borderRadius: const .all(.circular(25)), + onPressed: (index) { + _controller.onSort(allSortBy[index].sortBy!); + (context as Element).markNeedsBuild(); + }, + isSelected: allSortBy + .map((e) => e.sortBy == _controller.sortBy) + .toList(), + children: allSortBy.map((e) { + return Text( + e.sortName!, + style: const TextStyle( + fontSize: 13, + height: 1, + ), + strutStyle: const StrutStyle( + height: 1, + leading: 0, + fontSize: 13, + ), + textScaler: TextScaler.noScaling, + ); + }).toList(), + ); + }, + ), + ), + ); + } + return const SliverToBoxAdapter(); + }), + SliverPadding( + padding: EdgeInsets.only( + left: padding.left, + right: padding.right, + bottom: padding.bottom + 100, + ), + sliver: buildPage( + Obx(() => _buildBody(_controller.loadingState.value)), + ), + ), + ], ), ), - ], - ), + ), + Positioned( + right: padding.right + kFloatingActionButtonMargin, + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: padding.bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () { + if (_controller.isLogin) { + CreateDynPanel.onCreateDyn( + context, + topic: Pair( + first: int.parse(_controller.topicId), + second: _controller.topicName, + ), + ); + } else { + SmartDialog.showToast('账号未登录'); + } + }, + icon: const Icon(CustomIcons.topic_tag, size: 20), + label: const Text('参与话题'), + ), + ), + ), + ), + ], ), ); } diff --git a/lib/pages/follow/child/child_view.dart b/lib/pages/follow/child/child_view.dart index 775e1cd62..adbeda4ab 100644 --- a/lib/pages/follow/child/child_view.dart +++ b/lib/pages/follow/child/child_view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/follow_order_type.dart'; import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/follow/child/child_controller.dart'; import 'package:PiliPlus/pages/follow/controller.dart'; import 'package:PiliPlus/pages/follow/widgets/follow_item.dart'; @@ -37,7 +38,11 @@ class FollowChildPage extends StatefulWidget { } class _FollowChildPageState extends State - with AutomaticKeepAliveClientMixin { + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { late String _tag; late FollowChildController _followController; @@ -107,20 +112,41 @@ class _FollowChildPageState extends State return Stack( clipBehavior: Clip.none, children: [ - child, + NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: child, + ), Positioned( right: kFloatingActionButtonMargin + padding.right, - bottom: kFloatingActionButtonMargin + padding.bottom, - child: FloatingActionButton.extended( - onPressed: () => _followController - ..setOrderType( - _followController.orderType.value == FollowOrderType.def - ? FollowOrderType.attention - : FollowOrderType.def, - ) - ..onReload(), - icon: const Icon(Icons.format_list_bulleted, size: 20), - label: Obx(() => Text(_followController.orderType.value.title)), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: kFloatingActionButtonMargin + padding.bottom, + ), + child: FloatingActionButton.extended( + onPressed: () => _followController + ..setOrderType( + _followController.orderType.value == FollowOrderType.def + ? FollowOrderType.attention + : FollowOrderType.def, + ) + ..onReload(), + icon: const Icon(Icons.format_list_bulleted, size: 20), + label: Obx( + () => Text(_followController.orderType.value.title), + ), + ), + ), ), ), ], diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart index 40b305c10..124216978 100644 --- a/lib/pages/main_reply/view.dart +++ b/lib/pages/main_reply/view.dart @@ -40,7 +40,7 @@ class MainReplyPage extends StatefulWidget { } class _MainReplyPageState extends State - with SingleTickerProviderStateMixin, FabMixin { + with SingleTickerProviderStateMixin, BaseFabMixin, FabMixin { final _controller = Get.put( MainReplyController(), tag: Utils.generateRandomString(8), diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index bbc67d92b..22e4152b5 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_opus/item.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/member_opus/controller.dart'; import 'package:PiliPlus/pages/member_opus/widgets/space_opus_item.dart'; import 'package:PiliPlus/utils/grid.dart'; @@ -30,7 +31,11 @@ class MemberOpus extends StatefulWidget { } class _MemberOpusState extends State - with AutomaticKeepAliveClientMixin { + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { late final MemberOpusController _controller; @override @@ -50,71 +55,91 @@ class _MemberOpusState extends State super.build(context); final bottom = MediaQuery.viewPaddingOf(context).bottom; return Stack( + clipBehavior: .none, children: [ refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - top: widget.isSingle ? 12 : 0, - left: Style.safeSpace, - right: Style.safeSpace, - bottom: bottom + 100, + child: NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: widget.isSingle ? 12 : 0, + left: Style.safeSpace, + right: Style.safeSpace, + bottom: bottom + 100, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), ), - sliver: Obx(() => _buildBody(_controller.loadingState.value)), - ), - ], + ], + ), ), ), if (_controller.filter?.isNotEmpty == true) Positioned( right: kFloatingActionButtonMargin, - bottom: bottom + kFloatingActionButtonMargin, - child: FloatingActionButton.extended( - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: _controller.filter! - .map( - (e) => ListTile( - onTap: () { - if (e == _controller.type.value) { - return; - } - Get.back(); - _controller - ..type.value = e - ..onReload(); - }, - tileColor: e == _controller.type.value - ? Theme.of( - context, - ).colorScheme.onInverseSurface - : null, - dense: true, - title: Text( - e.text ?? e.tabName!, - style: const TextStyle(fontSize: 14), - ), - ), - ) - .toList(), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: _controller.filter! + .map( + (e) => ListTile( + onTap: () { + if (e == _controller.type.value) { + return; + } + Get.back(); + _controller + ..type.value = e + ..onReload(); + }, + tileColor: e == _controller.type.value + ? Theme.of( + context, + ).colorScheme.onInverseSurface + : null, + dense: true, + title: Text( + e.text ?? e.tabName!, + style: const TextStyle(fontSize: 14), + ), + ), + ) + .toList(), + ), + ), + ), + icon: const Icon(size: 20, Icons.sort), + label: Obx( + () { + final type = _controller.type.value; + return Text(type.text ?? type.tabName!); + }, ), ), ), - icon: const Icon(size: 20, Icons.sort), - label: Obx( - () { - final type = _controller.type.value; - return Text(type.text ?? type.tabName!); - }, - ), ), ), ], diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 1ec37c9d5..d9185d8e2 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/member/controller.dart'; import 'package:PiliPlus/pages/member_video/controller.dart'; import 'package:PiliPlus/pages/member_video/widgets/video_card_h_member_video.dart'; @@ -41,7 +42,12 @@ class MemberVideo extends StatefulWidget { } class _MemberVideoState extends State - with AutomaticKeepAliveClientMixin, GridMixin { + with + AutomaticKeepAliveClientMixin, + GridMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { @override bool get wantKeepAlive => true; @@ -121,32 +127,53 @@ class _MemberVideoState extends State return Stack( clipBehavior: Clip.none, children: [ - child, + NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: child, + ), Obx( () => !_controller.isLocating.value ? Positioned( right: kFloatingActionButtonMargin, - bottom: kFloatingActionButtonMargin + padding.bottom, - child: FloatingActionButton.extended( - onPressed: () { - final fromViewAid = _controller.fromViewAid; - _controller.isLocating.value = true; - final locatedIndex = - _controller.loadingState.value.dataOrNull - ?.indexWhere((i) => i.param == fromViewAid) ?? - -1; - if (locatedIndex == -1) { - _controller - ..lastAid = fromViewAid - ..reload = true - ..page = 0 - ..loadingState.value = LoadingState.loading() - ..queryData(); - } else { - _jumpToIndex(locatedIndex); - } - }, - label: const Text('定位至上次观看'), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: padding.bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () { + final fromViewAid = _controller.fromViewAid; + _controller.isLocating.value = true; + final locatedIndex = + _controller.loadingState.value.dataOrNull + ?.indexWhere( + (i) => i.param == fromViewAid, + ) ?? + -1; + if (locatedIndex == -1) { + _controller + ..lastAid = fromViewAid + ..reload = true + ..page = 0 + ..loadingState.value = LoadingState.loading() + ..queryData(); + } else { + _jumpToIndex(locatedIndex); + } + }, + label: const Text('定位至上次观看'), + ), + ), ), ) : const SizedBox.shrink(), diff --git a/lib/pages/member_video_web/base/view.dart b/lib/pages/member_video_web/base/view.dart index 24552e1ae..56abe1cc4 100644 --- a/lib/pages/member_video_web/base/view.dart +++ b/lib/pages/member_video_web/base/view.dart @@ -121,6 +121,7 @@ abstract class BaseVideoWebState< child: Padding( padding: const .fromLTRB(14, 0, 8, 4), child: Stack( + clipBehavior: .none, alignment: .centerLeft, children: [ ?buildCount(), diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index 3c4480aa2..72e3e4510 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -36,6 +36,7 @@ class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, + BaseFabMixin, FabMixin { late VideoReplyController _videoReplyController;