more fab anim

Closes #2265

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-06-04 10:54:58 +08:00
parent af1cd30ed7
commit 7b01c33657
9 changed files with 339 additions and 197 deletions

View File

@@ -22,7 +22,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
with SingleTickerProviderStateMixin, FabMixin {
with SingleTickerProviderStateMixin, BaseFabMixin, FabMixin {
CommonDynController get controller;
late final ScrollController scrollController;

View File

@@ -1,18 +1,19 @@
import 'package:flutter/material.dart';
mixin FabMixin<T extends StatefulWidget> on State<T>, TickerProvider {
bool _isFabVisible = true;
late final AnimationController _fabAnimationCtr;
late final Animation<Offset> fabAnimation;
mixin BaseFabMixin<T extends StatefulWidget> on State<T>, TickerProvider {
late bool _isFabVisible = true;
AnimationController get fabAnimationCtr;
Animation<Offset> get fabAnimation;
@override
void initState() {
super.initState();
_fabAnimationCtr = AnimationController(
AnimationController _initController() {
return AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
fabAnimation = _fabAnimationCtr.drive(
}
Animation<Offset> _initAnimation() {
return fabAnimationCtr.drive(
Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.0, 1.0),
@@ -23,20 +24,51 @@ mixin FabMixin<T extends StatefulWidget> on State<T>, TickerProvider {
void showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
_fabAnimationCtr.reverse();
fabAnimationCtr.reverse();
}
}
void hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
_fabAnimationCtr.forward();
fabAnimationCtr.forward();
}
}
}
mixin FabMixin<T extends StatefulWidget> on BaseFabMixin<T> {
@override
late final AnimationController fabAnimationCtr;
@override
late final Animation<Offset> fabAnimation;
@override
void initState() {
super.initState();
fabAnimationCtr = _initController();
fabAnimation = _initAnimation();
}
@override
void dispose() {
_fabAnimationCtr.dispose();
fabAnimationCtr.dispose();
super.dispose();
}
}
mixin LazyFabMixin<T extends StatefulWidget> on BaseFabMixin<T> {
AnimationController? _fabAnimationCtr;
Animation<Offset>? _fabAnimation;
@override
AnimationController get fabAnimationCtr =>
_fabAnimationCtr ??= _initController();
@override
Animation<Offset> get fabAnimation => _fabAnimation ??= _initAnimation();
@override
void dispose() {
_fabAnimationCtr?.dispose();
super.dispose();
}
}

View File

@@ -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<DynTopicPage> createState() => _DynTopicPageState();
}
class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
class _DynTopicPageState extends State<DynTopicPage>
with DynMixin, SingleTickerProviderStateMixin, BaseFabMixin, FabMixin {
final DynTopicController _controller = Get.put(
DynTopicController(),
tag: Utils.generateRandomString(8),
@@ -48,27 +50,22 @@ class _DynTopicPageState extends State<DynTopicPage> 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(
return Material(
child: Stack(
clipBehavior: .none,
children: [
refreshIndicator(
onRefresh: _controller.onRefresh,
child: NotificationListener<UserScrollNotification>(
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(),
@@ -81,7 +78,8 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
),
),
Obx(() {
final allSortBy = _controller.topicSortByConf.value?.allSortBy;
final allSortBy =
_controller.topicSortByConf.value?.allSortBy;
if (allSortBy != null && allSortBy.isNotEmpty) {
return SliverPinnedHeader(
backgroundColor: colorScheme.surface,
@@ -145,6 +143,38 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
],
),
),
),
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('参与话题'),
),
),
),
),
],
),
);
}

View File

@@ -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<FollowChildPage>
with AutomaticKeepAliveClientMixin {
with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
BaseFabMixin,
LazyFabMixin {
late String _tag;
late FollowChildController _followController;
@@ -107,10 +112,27 @@ class _FollowChildPageState extends State<FollowChildPage>
return Stack(
clipBehavior: Clip.none,
children: [
child,
NotificationListener<UserScrollNotification>(
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: 0,
child: SlideTransition(
position: fabAnimation,
child: Padding(
padding: .only(
bottom: kFloatingActionButtonMargin + padding.bottom,
),
child: FloatingActionButton.extended(
onPressed: () => _followController
..setOrderType(
@@ -120,7 +142,11 @@ class _FollowChildPageState extends State<FollowChildPage>
)
..onReload(),
icon: const Icon(Icons.format_list_bulleted, size: 20),
label: Obx(() => Text(_followController.orderType.value.title)),
label: Obx(
() => Text(_followController.orderType.value.title),
),
),
),
),
),
],

View File

@@ -40,7 +40,7 @@ class MainReplyPage extends StatefulWidget {
}
class _MainReplyPageState extends State<MainReplyPage>
with SingleTickerProviderStateMixin, FabMixin {
with SingleTickerProviderStateMixin, BaseFabMixin, FabMixin {
final _controller = Get.put(
MainReplyController(),
tag: Utils.generateRandomString(8),

View File

@@ -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<MemberOpus>
with AutomaticKeepAliveClientMixin {
with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
BaseFabMixin,
LazyFabMixin {
late final MemberOpusController _controller;
@override
@@ -50,9 +55,20 @@ class _MemberOpusState extends State<MemberOpus>
super.build(context);
final bottom = MediaQuery.viewPaddingOf(context).bottom;
return Stack(
clipBehavior: .none,
children: [
refreshIndicator(
onRefresh: _controller.onRefresh,
child: NotificationListener<UserScrollNotification>(
onNotification: (notification) {
final direction = notification.direction;
if (direction == .forward) {
showFab();
} else if (direction == .reverse) {
hideFab();
}
return false;
},
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
@@ -68,10 +84,17 @@ class _MemberOpusState extends State<MemberOpus>
],
),
),
),
if (_controller.filter?.isNotEmpty == true)
Positioned(
right: kFloatingActionButtonMargin,
bottom: 0,
child: SlideTransition(
position: fabAnimation,
child: Padding(
padding: .only(
bottom: bottom + kFloatingActionButtonMargin,
),
child: FloatingActionButton.extended(
onPressed: () => showDialog(
context: context,
@@ -117,6 +140,8 @@ class _MemberOpusState extends State<MemberOpus>
),
),
),
),
),
],
);
}

View File

@@ -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<MemberVideo>
with AutomaticKeepAliveClientMixin, GridMixin {
with
AutomaticKeepAliveClientMixin,
GridMixin,
SingleTickerProviderStateMixin,
BaseFabMixin,
LazyFabMixin {
@override
bool get wantKeepAlive => true;
@@ -121,19 +127,38 @@ class _MemberVideoState extends State<MemberVideo>
return Stack(
clipBehavior: Clip.none,
children: [
child,
NotificationListener<UserScrollNotification>(
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,
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) ??
?.indexWhere(
(i) => i.param == fromViewAid,
) ??
-1;
if (locatedIndex == -1) {
_controller
@@ -148,6 +173,8 @@ class _MemberVideoState extends State<MemberVideo>
},
label: const Text('定位至上次观看'),
),
),
),
)
: const SizedBox.shrink(),
),

View File

@@ -121,6 +121,7 @@ abstract class BaseVideoWebState<
child: Padding(
padding: const .fromLTRB(14, 0, 8, 4),
child: Stack(
clipBehavior: .none,
alignment: .centerLeft,
children: [
?buildCount(),

View File

@@ -36,6 +36,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
with
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin,
BaseFabMixin,
FabMixin {
late VideoReplyController _videoReplyController;