From bc99865ba8cf8a4ec3050adef87599924ee483e2 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 21 Aug 2024 19:11:27 +0800 Subject: [PATCH] :dizzy: Animated collapsible sidebar --- lib/translations/en_us.dart | 2 + lib/translations/zh_cn.dart | 2 + .../navigation/app_navigation_drawer.dart | 219 +++++++++++++----- ...egions.dart => app_navigation_region.dart} | 6 +- pubspec.yaml | 2 +- 5 files changed, 164 insertions(+), 67 deletions(-) rename lib/widgets/navigation/{app_navigation_regions.dart => app_navigation_region.dart} (95%) diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index db05ab7..035c72c 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -383,4 +383,6 @@ const i18nEnglish = { 'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.', 'messageHistoryWipe': 'Wipe local message history', 'unknown': 'Unknown', + 'collapse': 'Collapse', + 'expand': 'Expand', }; diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index 1d815cd..a3a3029 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -353,4 +353,6 @@ const i18nSimplifiedChinese = { 'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。', 'messageHistoryWipe': '清除消息记录', 'unknown': '未知', + 'collapse': '折叠', + 'expand': '展开', }; diff --git a/lib/widgets/navigation/app_navigation_drawer.dart b/lib/widgets/navigation/app_navigation_drawer.dart index 2e43970..ff5ec67 100644 --- a/lib/widgets/navigation/app_navigation_drawer.dart +++ b/lib/widgets/navigation/app_navigation_drawer.dart @@ -13,7 +13,7 @@ import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_status_action.dart'; import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:badges/badges.dart' as badges; -import 'package:solian/widgets/navigation/app_navigation_regions.dart'; +import 'package:solian/widgets/navigation/app_navigation_region.dart'; class AppNavigationDrawer extends StatefulWidget { final String? routeName; @@ -24,8 +24,22 @@ class AppNavigationDrawer extends StatefulWidget { State createState() => _AppNavigationDrawerState(); } -class _AppNavigationDrawerState extends State { - bool _isCollapsed = true; +class _AppNavigationDrawerState extends State + with TickerProviderStateMixin { + bool _isCollapsed = false; + + late final AnimationController _drawerAnimationController = + AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + late final Animation _drawerAnimation = Tween( + begin: 80.0, + end: 304.0, + ).animate(CurvedAnimation( + parent: _drawerAnimationController, + curve: Curves.easeInOut, + )); AccountStatus? _accountStatus; @@ -42,13 +56,19 @@ class _AppNavigationDrawerState extends State { } } + Color get _unFocusColor => + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + Widget _buildUserInfo() { return Obx(() { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse || auth.userProfile.value == null) { if (_isCollapsed) { return InkWell( - child: const Icon(Icons.account_circle).paddingAll(28), + child: const Icon(Icons.account_circle).paddingSymmetric( + horizontal: 28, + vertical: 20, + ), onTap: () { AppRouter.instance.goNamed('account'); _closeDrawer(); @@ -70,9 +90,7 @@ class _AppNavigationDrawerState extends State { final leading = Obx(() { final statusBadgeColor = _accountStatus != null - ? StatusProvider.determineStatus( - _accountStatus!, - ).$2 + ? StatusProvider.determineStatus(_accountStatus!).$2 : Colors.grey; final RelationshipProvider relations = Get.find(); @@ -104,31 +122,43 @@ class _AppNavigationDrawerState extends State { return InkWell( child: !_isCollapsed - ? ListTile( - contentPadding: const EdgeInsets.only(left: 20, right: 20), - title: Text( - auth.userProfile.value!['nick'], - maxLines: 1, - overflow: TextOverflow.fade, - ), - subtitle: Builder( - builder: (context) { - if (_accountStatus == null) { - return Text('loading'.tr); - } - final info = StatusProvider.determineStatus( - _accountStatus!, - ); - return Text( - info.$3, - maxLines: 1, - overflow: TextOverflow.fade, - ); - }, - ), - leading: leading, - ) - : leading.paddingAll(20), + ? Row( + children: [ + leading, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + auth.userProfile.value!['nick'], + maxLines: 1, + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.bodyLarge, + ).paddingOnly(left: 16), + Builder( + builder: (context) { + if (_accountStatus == null) { + return Text('loading'.tr).paddingOnly(left: 16); + } + final info = StatusProvider.determineStatus( + _accountStatus!, + ); + return Text( + info.$3, + maxLines: 1, + overflow: TextOverflow.fade, + style: TextStyle( + color: _unFocusColor, + ), + ).paddingOnly(left: 16); + }, + ), + ], + ), + ), + ], + ).paddingSymmetric(horizontal: 20, vertical: 16) + : leading.paddingSymmetric(horizontal: 20, vertical: 16), onTap: () { AppRouter.instance.goNamed('account'); _closeDrawer(); @@ -148,22 +178,59 @@ class _AppNavigationDrawerState extends State { }); } + void _expandDrawer() { + _drawerAnimationController.animateTo(1); + } + + void _collapseDrawer() { + _drawerAnimationController.animateTo(0); + } + void _closeDrawer() { + _autoResize(); rootScaffoldKey.currentState!.closeDrawer(); } + void _autoResize() { + if (SolianTheme.isExtraLargeScreen(context)) { + _expandDrawer(); + } else if (SolianTheme.isLargeScreen(context)) { + _collapseDrawer(); + } + } + @override void initState() { super.initState(); _getStatus(); + Future.delayed(Duration.zero, () => _autoResize()); + _drawerAnimationController.addListener(() { + if (_drawerAnimation.value > 180 && _isCollapsed) { + setState(() => _isCollapsed = false); + } else if (_drawerAnimation.value < 180 && !_isCollapsed) { + setState(() => _isCollapsed = true); + } + }); + } + + @override + void dispose() { + _drawerAnimationController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - return Drawer( - width: _isCollapsed ? 80 : null, - backgroundColor: - SolianTheme.isLargeScreen(context) ? Colors.transparent : null, + return AnimatedBuilder( + animation: _drawerAnimation, + builder: (context, child) { + return Drawer( + width: _drawerAnimation.value, + backgroundColor: + SolianTheme.isLargeScreen(context) ? Colors.transparent : null, + child: child, + ); + }, child: SafeArea( bottom: false, child: Column( @@ -175,7 +242,7 @@ class _AppNavigationDrawerState extends State { .map( (e) => _isCollapsed ? InkWell( - child: Icon(e.icon, size: 22).paddingSymmetric( + child: Icon(e.icon, size: 20).paddingSymmetric( horizontal: 28, vertical: 16, ), @@ -201,7 +268,7 @@ class _AppNavigationDrawerState extends State { ), const Divider(thickness: 0.3, height: 1), Expanded( - child: AppNavigationRegions( + child: AppNavigationRegion( isCollapsed: _isCollapsed, onSelected: (item) { _closeDrawer(); @@ -211,31 +278,57 @@ class _AppNavigationDrawerState extends State { const Divider(thickness: 0.3, height: 1), Column( children: [ - _isCollapsed - ? InkWell( - child: const Icon(Icons.settings, size: 22) - .paddingSymmetric( - horizontal: 28, - vertical: 16, - ), - onTap: () { - AppRouter.instance.pushNamed('settings'); - _closeDrawer(); - }, - ) - : ListTile( - minTileHeight: 0, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - ), - leading: - const Icon(Icons.settings, size: 20).paddingAll(2), - title: Text('settings'.tr), - onTap: () { - AppRouter.instance.pushNamed('settings'); - _closeDrawer(); - }, - ), + if (_isCollapsed) + InkWell( + child: const Icon( + Icons.settings, + size: 20, + ).paddingSymmetric( + horizontal: 28, + vertical: 10, + ), + onTap: () { + AppRouter.instance.pushNamed('settings'); + _closeDrawer(); + }, + ) + else + ListTile( + minTileHeight: 0, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + ), + leading: const Icon(Icons.settings, size: 20).paddingAll(2), + title: Text('settings'.tr), + onTap: () { + AppRouter.instance.pushNamed('settings'); + _closeDrawer(); + }, + ), + if (_isCollapsed) + InkWell( + child: const Icon(Icons.chevron_right, size: 20) + .paddingSymmetric( + horizontal: 28, + vertical: 10, + ), + onTap: () { + _expandDrawer(); + }, + ) + else + ListTile( + minTileHeight: 0, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + ), + leading: + const Icon(Icons.chevron_left, size: 20).paddingAll(2), + title: Text('collapse'.tr), + onTap: () { + _collapseDrawer(); + }, + ), ], ).paddingOnly( top: 8, diff --git a/lib/widgets/navigation/app_navigation_regions.dart b/lib/widgets/navigation/app_navigation_region.dart similarity index 95% rename from lib/widgets/navigation/app_navigation_regions.dart rename to lib/widgets/navigation/app_navigation_region.dart index 6c92675..f6c683e 100644 --- a/lib/widgets/navigation/app_navigation_regions.dart +++ b/lib/widgets/navigation/app_navigation_region.dart @@ -5,11 +5,11 @@ import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; import 'package:collection/collection.dart'; -class AppNavigationRegions extends StatelessWidget { +class AppNavigationRegion extends StatelessWidget { final bool isCollapsed; final Function(Channel item) onSelected; - const AppNavigationRegions({ + const AppNavigationRegion({ super.key, required this.onSelected, this.isCollapsed = false, @@ -32,7 +32,7 @@ class AppNavigationRegions extends StatelessWidget { if (isCollapsed) { return InkWell( - child: const Icon(Icons.tag_outlined).paddingSymmetric( + child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric( horizontal: 20, vertical: 16, ), diff --git a/pubspec.yaml b/pubspec.yaml index 82fa37c..0504b99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: solian description: "The Solar Network App" publish_to: "none" -version: 1.2.1+20 +version: 1.2.1+21 environment: sdk: ">=3.3.4 <4.0.0"