From e98f666caf7d64d9d935f42ecdbeeb60f604fb29 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 20 May 2026 16:22:23 +0300 Subject: [PATCH] animation added. --- .../widgets/app_bar_widget.dart | 185 ++++++++++--- .../widgets/progress_fragment.dart | 243 +++++++++++------- .../widgets/request_category_fragment.dart | 97 +++++-- .../widgets/request_category_list.dart | 69 ++++- .../create_request_type_bottom_sheet.dart | 81 +++++- .../my_request/my_requests_page.dart | 167 ++++++++---- .../widgets/request_item_view_list.dart | 132 +++++++--- .../widgets/equipment/my_assets_page.dart | 98 ++++++- 8 files changed, 799 insertions(+), 273 deletions(-) diff --git a/lib/dashboard_latest/widgets/app_bar_widget.dart b/lib/dashboard_latest/widgets/app_bar_widget.dart index 35c592ff..ef7a95f7 100644 --- a/lib/dashboard_latest/widgets/app_bar_widget.dart +++ b/lib/dashboard_latest/widgets/app_bar_widget.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -14,16 +12,89 @@ import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_page.dart'; import 'package:test_sa/views/pages/user/notifications/unread_chat_list.dart'; -import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; import '../../controllers/providers/api/user_provider.dart'; import '../../models/user.dart'; -class AppBarWidget extends StatelessWidget { - final VoidCallback onDrawerPress; // Made nullable +class AppBarWidget extends StatefulWidget { + final VoidCallback onDrawerPress; const AppBarWidget({Key? key, required this.onDrawerPress}) : super(key: key); + @override + State createState() => _AppBarWidgetState(); +} + +class _AppBarWidgetState extends State with TickerProviderStateMixin { + late AnimationController _leftController; + late AnimationController _rightController; + late Animation _leftSlideAnimation; + late Animation _leftFadeAnimation; + late Animation _rightSlideAnimation; + late Animation _rightFadeAnimation; + + @override + void initState() { + super.initState(); + + // Profile section animation (left to right with fade) + _leftController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _leftSlideAnimation = Tween( + begin: const Offset(-0.3, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _leftController, + curve: Curves.easeOutCubic, + )); + + _leftFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _leftController, + curve: Curves.easeIn, + )); + + // Action icons animation (right to left with fade) + _rightController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _rightSlideAnimation = Tween( + begin: const Offset(0.3, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _rightController, + curve: Curves.easeOutCubic, + )); + + _rightFadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _rightController, + curve: Curves.easeIn, + )); + + // Start animations with slight delay between them + _leftController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _rightController.forward(); + }); + } + + @override + void dispose() { + _leftController.dispose(); + _rightController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return AppBar( @@ -35,37 +106,55 @@ class AppBarWidget extends StatelessWidget { // mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - CircleAvatar( - radius: 24, - backgroundColor: context.isDark ? AppColor.neutral50 : AppColor.neutral40, - child: Padding( - padding: const EdgeInsets.all(1), // Border radius - child: ClipOval( - child: snapshot.profileImage != null - ? Image.file(snapshot.profileImage!) // Added null check - : (snapshot.user?.profilePhotoName?.isNotEmpty ?? false) - ? Image.network(snapshot.user!.profilePhotoName!) // Added null check - : const Icon(Icons.person, size: 24, color: Colors.white), - ), + // Profile section with left-to-right fade animation + SlideTransition( + position: _leftSlideAnimation, + child: FadeTransition( + opacity: _leftFadeAnimation, + child: CircleAvatar( + radius: 24, + backgroundColor: context.isDark ? AppColor.neutral50 : AppColor.neutral40, + child: Padding( + padding: const EdgeInsets.all(1), // Border radius + child: ClipOval( + child: snapshot.profileImage != null + ? Image.file(snapshot.profileImage!) // Added null check + : (snapshot.user?.profilePhotoName?.isNotEmpty ?? false) + ? Image.network(snapshot.user!.profilePhotoName!) // Added null check + : const Icon(Icons.person, size: 24, color: Colors.white), + ), + ), + ).onPress(widget.onDrawerPress), ), - ).onPress(onDrawerPress), // Handle potential null + ), 8.width, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - Text( - snapshot.user?.username ?? "", - style: AppTextStyles.heading6.copyWith(color: AppColor.headingTextColor(context), fontSize: context.isTablet() ? 14 : null), - ), - Text( - snapshot.user?.type?.name.toCamelCase ?? "", - style: AppTextStyles.bodyText.copyWith(color: AppColor.lightTextColor(context), fontSize: context.isTablet() ? 12 : null), - ), - ], - ).expanded, + SlideTransition( + position: _leftSlideAnimation, + child: FadeTransition( + opacity: _leftFadeAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text( + snapshot.user?.username ?? "", + style: AppTextStyles.heading6.copyWith(color: AppColor.headingTextColor(context), fontSize: context.isTablet() ? 14 : null), + ), + Text( + snapshot.user?.type?.name.toCamelCase ?? "", + style: AppTextStyles.bodyText.copyWith(color: AppColor.lightTextColor(context), fontSize: context.isTablet() ? 12 : null), + ), + ], + ).expanded, + ), + ), const Spacer(), - Consumer(builder: (context, settingProvider, child) { + // Action icons with right-to-left fade animation + SlideTransition( + position: _rightSlideAnimation, + child: FadeTransition( + opacity: _rightFadeAnimation, + child: Consumer(builder: (context, settingProvider, child) { return MenuAnchor( style: MenuStyle( backgroundColor: WidgetStateProperty.all(AppColor.background(context)), @@ -229,16 +318,28 @@ class AppBarWidget extends StatelessWidget { ), ); }), + ), + ), 16.width, - if (context.settingProvider.isUserFlowMedical) ...[ - ("dashboard/chat").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() { - Navigator.of(context).push(CupertinoPageRoute(builder: (_) => UnReadChatList())); - }), - 16.width, - ], - ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() { - Navigator.of(context).pushNamed(NotificationsPage.id); - }), + SlideTransition( + position: _rightSlideAnimation, + child: FadeTransition( + opacity: _rightFadeAnimation, + child: Row( + children: [ + if (context.settingProvider.isUserFlowMedical) ...[ + ("dashboard/chat").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() { + Navigator.of(context).push(CupertinoPageRoute(builder: (_) => UnReadChatList())); + }), + 16.width, + ], + ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() { + Navigator.of(context).pushNamed(NotificationsPage.id); + }), + ], + ), + ), + ), ], ).paddingOnly(start: 16, end: 16); }), diff --git a/lib/dashboard_latest/widgets/progress_fragment.dart b/lib/dashboard_latest/widgets/progress_fragment.dart index ba946d59..4d48e4c2 100644 --- a/lib/dashboard_latest/widgets/progress_fragment.dart +++ b/lib/dashboard_latest/widgets/progress_fragment.dart @@ -154,7 +154,6 @@ import 'package:flutter/material.dart'; import 'package:pie_chart/pie_chart.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; -import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; @@ -164,11 +163,53 @@ import 'package:test_sa/new_views/app_style/app_color.dart'; import '../../controllers/providers/api/user_provider.dart'; import '../../models/enums/user_types.dart'; -class ProgressFragment extends StatelessWidget { - ProgressFragment({Key? key}) : super(key: key); +class ProgressFragment extends StatefulWidget { + const ProgressFragment({Key? key}) : super(key: key); + @override + State createState() => _ProgressFragmentState(); +} + +class _ProgressFragmentState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _scaleAnimation; late UserProvider _userProvider; + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.6, curve: Curves.easeIn), + )); + + _scaleAnimation = Tween( + begin: 0.92, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.8, curve: Curves.easeOutCubic), + )); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { _userProvider = Provider.of(context); @@ -200,107 +241,113 @@ class ProgressFragment extends StatelessWidget { ]; } - return Column( - children: [ - AspectRatio( - aspectRatio: (359.toScreenWidth / 214.toScreenWidth), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Work Order Status".addTranslation, - style: AppTextStyles.heading4.copyWith(color: AppColor.headingTextColor(context)), - ), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (int i = 0; i < chartData.length; i++) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 6, - width: 6, - decoration: BoxDecoration(color: chartData[i].color, borderRadius: BorderRadius.circular(1)), - ), - 4.width, - Text(chartData[i].x, style: AppTextStyles.textStyle9.copyWith(color: AppColor.text9Color(context))), - ], - ) - ], - ), - 8.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - spacing: 60.toScreenWidth, + return FadeTransition( + opacity: _fadeAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Column( + children: [ + AspectRatio( + aspectRatio: (359.toScreenWidth / 214.toScreenWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 3, - child: SizedBox( - height: 124.toScreenWidth, - width: 124.toScreenWidth, - child: PieChart( - dataMap: {for (var v in chartData) v.x: v.y}, - animationDuration: const Duration(milliseconds: 750), - chartLegendSpacing: 0, - chartRadius: 124, - colorList: chartData.map((element) => element.color).toList(), - initialAngleInDegree: 270, - chartType: ChartType.ring, - ringStrokeWidth: 32, - legendOptions: LegendOptions( - showLegendsInRow: true, - legendPosition: LegendPosition.top, - showLegends: false, - legendTextStyle: AppTextStyles.textStyle9, - ), - chartValuesOptions: ChartValuesOptions( - showChartValueBackground: true, - chartValueBackgroundColor: const Color(0xff226187).withValues(alpha: .75), - chartValueStyle: AppTextStyles.tinyFont2.copyWith(height: 1.5, color: Colors.white), - showChartValues: true, - showChartValuesInPercentage: true, - showChartValuesOutside: true, - decimalPlaces: 0, - ), - ).toShimmer(isShow: snapshot.isAllCountLoading, radius: 250, context: context).paddingAll(0), - ), + Text( + "Work Order Status".addTranslation, + style: AppTextStyles.heading4.copyWith(color: AppColor.headingTextColor(context)), + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (int i = 0; i < chartData.length; i++) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 6, + width: 6, + decoration: BoxDecoration(color: chartData[i].color, borderRadius: BorderRadius.circular(1)), + ), + 4.width, + Text(chartData[i].x, style: AppTextStyles.textStyle9.copyWith(color: AppColor.text9Color(context))), + ], + ) + ], ), - Expanded( - flex: 2, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 8, - children: [ - for (int i = 0; i < chartData.length; i++) - Row( - spacing: 16, - children: [ - chartData[i].icon.toSvgAsset(height: 26, width: 26, color: chartData[i].color).toShimmer(isShow: snapshot.isAllCountLoading, context: context), - Column( - spacing: 4, - crossAxisAlignment: CrossAxisAlignment.start, + 8.height, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + spacing: 60.toScreenWidth, + children: [ + Expanded( + flex: 3, + child: SizedBox( + height: 124.toScreenWidth, + width: 124.toScreenWidth, + child: PieChart( + dataMap: {for (var v in chartData) v.x: v.y}, + animationDuration: const Duration(milliseconds: 750), + chartLegendSpacing: 0, + chartRadius: 124, + colorList: chartData.map((element) => element.color).toList(), + initialAngleInDegree: 270, + chartType: ChartType.ring, + ringStrokeWidth: 32, + legendOptions: LegendOptions( + showLegendsInRow: true, + legendPosition: LegendPosition.top, + showLegends: false, + legendTextStyle: AppTextStyles.textStyle9, + ), + chartValuesOptions: ChartValuesOptions( + showChartValueBackground: true, + chartValueBackgroundColor: const Color(0xff226187).withValues(alpha: .75), + chartValueStyle: AppTextStyles.tinyFont2.copyWith(height: 1.5, color: Colors.white), + showChartValues: true, + showChartValuesInPercentage: true, + showChartValuesOutside: true, + decimalPlaces: 0, + ), + ).toShimmer(isShow: snapshot.isAllCountLoading, radius: 250, context: context).paddingAll(0), + ), + ), + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + for (int i = 0; i < chartData.length; i++) + Row( + spacing: 16, children: [ - Text(chartData[i].x, style: AppTextStyles.textStyle9.copyWith(color: AppColor.text9Color(context))) - .toShimmer(isShow: snapshot.isAllCountLoading, context: context), - Text("${chartData[i].y.toInt()}", style: AppTextStyles.bodyText.copyWith(color: AppColor.headingTextColor(context))) - .toShimmer(isShow: snapshot.isAllCountLoading, context: context), + chartData[i].icon.toSvgAsset(height: 26, width: 26, color: chartData[i].color).toShimmer(isShow: snapshot.isAllCountLoading, context: context), + Column( + spacing: 4, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(chartData[i].x, style: AppTextStyles.textStyle9.copyWith(color: AppColor.text9Color(context))) + .toShimmer(isShow: snapshot.isAllCountLoading, context: context), + Text("${chartData[i].y.toInt()}", style: AppTextStyles.bodyText.copyWith(color: AppColor.headingTextColor(context))) + .toShimmer(isShow: snapshot.isAllCountLoading, context: context), + ], + ) ], ) - ], - ) - ], - ), - ), + ], + ), + ), + ], + ).expanded, ], - ).expanded, - ], - ).toShadowContainer(context, borderRadius: 20, paddingObject: const EdgeInsets.symmetric(horizontal: 24, vertical: 16)), - ), - ], - ).paddingOnly(start: 16, end: 16); + ).toShadowContainer(context, borderRadius: 20, paddingObject: const EdgeInsets.symmetric(horizontal: 24, vertical: 16)), + ), + ], + ).paddingOnly(start: 16, end: 16), + ), + ); }, ); } diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index 5fb56375..af5797a3 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -1,4 +1,3 @@ -import 'package:date_picker_timeline/extra/color.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; @@ -23,8 +22,25 @@ class RequestCategoryFragment extends StatefulWidget { State createState() => _RequestCategoryFragmentState(); } -class _RequestCategoryFragmentState extends State { +class _RequestCategoryFragmentState extends State with SingleTickerProviderStateMixin { bool isAscending = false; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } void _toggleSort(DashBoardProvider dashboardProvider, UsersTypes? userType) { setState(() { @@ -163,28 +179,61 @@ class _RequestCategoryFragmentState extends State { shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: context.isTablet() ? 5 : 4, childAspectRatio: 85 / 85, crossAxisSpacing: 8, mainAxisSpacing: 8), itemBuilder: (cxt, index) { - return RequestTypeButton( - label: tabs[index].label, - loading: requestsProvider.isDetailLoading && requestsProvider.currentListIndex == index, - isSelected: requestsProvider.currentListIndex == index, - count: requestsProvider.getStatusCount(tabs[index].requestType), - onPressed: () async { - setState(() { - isAscending = false; - }); - requestsProvider.currentListIndex = index; - requestsProvider.resetRequestDataList(); - requestsProvider.getRequestDetail( - usersType: userType ?? UsersTypes.normal_user, - showLoader: true, - status: tabs[index].tag, - tabId: tabs[index].id, - isHighPriority: tabs[index].isHighPriority, - isOverdue: tabs[index].isOverDue, - ); - }, - icon: tabs[index].icon, - iconColor: tabs[index].iconColor, + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: RequestTypeButton( + label: tabs[index].label, + loading: requestsProvider.isDetailLoading && requestsProvider.currentListIndex == index, + isSelected: requestsProvider.currentListIndex == index, + count: requestsProvider.getStatusCount(tabs[index].requestType), + onPressed: () async { + setState(() { + isAscending = false; + }); + requestsProvider.currentListIndex = index; + requestsProvider.resetRequestDataList(); + requestsProvider.getRequestDetail( + usersType: userType ?? UsersTypes.normal_user, + showLoader: true, + status: tabs[index].tag, + tabId: tabs[index].id, + isHighPriority: tabs[index].isHighPriority, + isOverdue: tabs[index].isOverDue, + ); + }, + icon: tabs[index].icon, + iconColor: tabs[index].iconColor, + ), + ), ); }, itemCount: tabs.length, diff --git a/lib/dashboard_latest/widgets/request_category_list.dart b/lib/dashboard_latest/widgets/request_category_list.dart index f0fbafb9..f61f08bd 100644 --- a/lib/dashboard_latest/widgets/request_category_list.dart +++ b/lib/dashboard_latest/widgets/request_category_list.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; @@ -19,26 +17,83 @@ import 'package:test_sa/new_views/pages/land_page/requests/ppm_item_view.dart'; import 'package:test_sa/new_views/pages/land_page/requests/recurrent_wo_item_view.dart'; import 'package:test_sa/new_views/pages/land_page/requests/service_request_item_view.dart'; -class RequestCategoryList extends StatelessWidget { +class RequestCategoryList extends StatefulWidget { final List list; final int totalCount; final bool isLoading; const RequestCategoryList(this.list, this.isLoading, this.totalCount, {Key? key}) : super(key: key); + @override + State createState() => _RequestCategoryListState(); +} + +class _RequestCategoryListState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return ListView.separated( - itemCount: list.length < totalCount ? list.length + 1 : list.length, + itemCount: widget.list.length < widget.totalCount ? widget.list.length + 1 : widget.list.length, shrinkWrap: true, padding: const EdgeInsets.all(16), physics: const NeverScrollableScrollPhysics(), separatorBuilder: (cxt, index) => 12.height, itemBuilder: (context, index) { - if (index == list.length) { - return CircularProgressIndicator(color: AppColor.loadingColor(context), strokeWidth: 1).center; + if (index == widget.list.length) { + return CircularProgressIndicator(color: AppColor.loadingColor(context), strokeWidth: 1).center; } - return _buildRequestItem(list[index]); + + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.7), + (delay + 0.4).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.15), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.7), + (delay + 0.4).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: _buildRequestItem(widget.list[index]), + ), + ); }, ); } diff --git a/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart b/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart index 0f6a103f..bb50addf 100644 --- a/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart +++ b/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart @@ -22,9 +22,32 @@ import 'package:test_sa/modules/traf_module/create_traf_request_page.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_request_form.dart'; -class CreateRequestTypeBottomSheet extends StatelessWidget { +class CreateRequestTypeBottomSheet extends StatefulWidget { const CreateRequestTypeBottomSheet({super.key}); + @override + State createState() => _CreateRequestTypeBottomSheetState(); +} + +class _CreateRequestTypeBottomSheetState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { List requestList = CreateRequestModel.requestsList(context); @@ -40,28 +63,60 @@ class CreateRequestTypeBottomSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ context.translation.selectWorkOrder.heading4(context), - GridView( + GridView.builder( padding: const EdgeInsets.only(top: 16, bottom: 0), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 16, mainAxisSpacing: 16), - children: [ - for (var request in requestList) - listCard( - context: context, - icon: request.icon, - label: request.title, - onTap: () { - Navigator.pushReplacementNamed(context, request.routeName); - }, + itemCount: requestList.length, + itemBuilder: (context, index) { + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: listCard( + context: context, + icon: requestList[index].icon, + label: requestList[index].title, + onTap: () { + Navigator.pushReplacementNamed(context, requestList[index].routeName); + }, + ), ), - ], + ); + }, ), ], ), ), ); - return const Placeholder(); } Widget listCard({required String icon, required String label, required VoidCallback onTap, required BuildContext context}) { diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index 464447ae..49527cd7 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -31,12 +31,51 @@ class MyRequestsPage extends StatefulWidget { State createState() => _MyRequestsPageState(); } -class _MyRequestsPageState extends State { +class _MyRequestsPageState extends State with SingleTickerProviderStateMixin { AllRequestsProvider? _provider; SearchAllRequestsModel? _search; late List requestsList; int selectedRequest = 0; + late AnimationController _tabAnimationController; + late Animation _tabContainerAnimation; + late Animation _tabContainerSlideAnimation; + int _listAnimationKey = 0; // Key to force list re-animation + + @override + void initState() { + super.initState(); + _tabAnimationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + // Container entrance animation (faster than individual tabs) + _tabContainerAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _tabAnimationController, + curve: const Interval(0.0, 0.4, curve: Curves.easeOut), + ), + ); + + _tabContainerSlideAnimation = Tween( + begin: const Offset(0.0, -0.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _tabAnimationController, + curve: const Interval(0.0, 0.5, curve: Curves.easeOutCubic), + ), + ); + + _tabAnimationController.forward(); + } + + @override + void dispose() { + _tabAnimationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -190,59 +229,93 @@ class _MyRequestsPageState extends State { children: [ if (!_filterIsEmpty()) _buildActiveFiltersChips(), if (_filterIsEmpty()) - SizedBox( - height: 50.toScreenHeight, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only(top: 16, right: 16, left: 16), - itemBuilder: (cxt, index) => Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 12), - // foregroundDecoration: selectedRequest != index - // ? null - // : ShapeDecoration( - // color: (AppColor.red20), - // shape: RoundedRectangleBorder( - // side: BorderSide(width: 1, color: AppColor.blueStatus(context)), - // borderRadius: BorderRadius.circular(7), - // ), - // ), - decoration: ShapeDecoration( - color: //selectedRequest == index ? - AppColor.background(context), //: AppColor.unSelectedButtonColor(context).withOpacity(snapshot.isAllLoading ? 0.7 : 1), - shape: RoundedRectangleBorder( - side: BorderSide(width: 1, color: selectedRequest == index ? AppColor.primary10 : AppColor.borderColor(context)), - borderRadius: BorderRadius.circular(6), + FadeTransition( + opacity: _tabContainerAnimation, + child: SlideTransition( + position: _tabContainerSlideAnimation, + child: SizedBox( + height: 50.toScreenHeight, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(top: 16, right: 16, left: 16), + itemBuilder: (cxt, index) { + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _tabAnimationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _tabAnimationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), ), - shadows: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 14, offset: const Offset(0, 0), spreadRadius: 0)], - ), - child: Text( - requestsList[index].value, - style: AppTextStyles.tinyFont.copyWith(height: 1, color: selectedRequest == index ? AppColor.primary10 : AppColor.filterButtonTextColor(context)), - ), - ).onPress(() { - if (snapshot.isAllLoading) { - return; - } - - if (selectedRequest != index) { - setState(() { - selectedRequest = index; - }); - - _provider!.reset(); - _provider!.getAllRequests(context, typeTransaction: requestsList[selectedRequest].id); - } - }), - separatorBuilder: (cxt, index) => 8.width, - itemCount: requestsList.length), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: ShapeDecoration( + color: AppColor.background(context), + shape: RoundedRectangleBorder( + side: BorderSide(width: 1, color: selectedRequest == index ? AppColor.primary10 : AppColor.borderColor(context)), + borderRadius: BorderRadius.circular(6), + ), + shadows: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 14, offset: const Offset(0, 0), spreadRadius: 0)], + ), + child: Text( + requestsList[index].value, + style: AppTextStyles.tinyFont.copyWith(height: 1, color: selectedRequest == index ? AppColor.primary10 : AppColor.filterButtonTextColor(context)), + ), + ).onPress(() { + if (snapshot.isAllLoading) { + return; + } + + setState(() { + selectedRequest = index; + _listAnimationKey++; // Force list to re-animate + }); + + _provider!.reset(); + _provider!.getAllRequests(context, typeTransaction: requestsList[selectedRequest].id); + }), + ), + ); + }, + separatorBuilder: (cxt, index) => 8.width, + itemCount: requestsList.length), + ), + ), ), LazyLoading( nextPage: snapshot.nextPage, onLazyLoad: () async { snapshot.getAllRequests(context, typeTransaction: requestsList[selectedRequest].id, search: _search); }, - child: RequestItemViewList(snapshot.allRequestsAndCount?.requestsDetails ?? [], snapshot.isAllLoading), + child: RequestItemViewList( + snapshot.allRequestsAndCount?.requestsDetails ?? [], + snapshot.isAllLoading, + key: ValueKey('request_list_${selectedRequest}_$_listAnimationKey'), // Key changes on every tab click + ), ).expanded, ], ); diff --git a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart index edec75d5..6a52e813 100644 --- a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart +++ b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart @@ -1,5 +1,4 @@ //request tab page -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -16,7 +15,6 @@ import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart' import 'package:test_sa/modules/medical_gas_inspection/medical_gas_inspection_item_view.dart'; import 'package:test_sa/modules/tm_module/tasks/task_request_item_view.dart'; import 'package:test_sa/modules/traf_module/traf_request_item_view.dart'; -import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/pages/land_page/requests/device_item_view.dart'; import 'package:test_sa/new_views/pages/land_page/requests/gas_refill_item_view.dart'; import 'package:test_sa/new_views/pages/land_page/requests/ppm_item_view.dart'; @@ -24,67 +22,141 @@ import 'package:test_sa/new_views/pages/land_page/requests/recurrent_wo_item_vie import 'package:test_sa/new_views/pages/land_page/requests/service_request_item_view.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; -class RequestItemViewList extends StatelessWidget { - List list; - bool isLoading; +class RequestItemViewList extends StatefulWidget { + final List list; + final bool isLoading; - RequestItemViewList(this.list, this.isLoading, {Key? key}) : super(key: key); + const RequestItemViewList(this.list, this.isLoading, {Key? key}) : super(key: key); + + @override + State createState() => _RequestItemViewListState(); +} + +class _RequestItemViewListState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return (list.isEmpty && !isLoading) + return (widget.list.isEmpty && !widget.isLoading) ? NoDataFound(message: context.translation.noDataFound).center : ListView.separated( padding: const EdgeInsets.all(16), shrinkWrap: true, itemBuilder: (cxt, index) { - if (isLoading) return const SizedBox().toRequestShimmer(cxt, isLoading); + if (widget.isLoading) return const SizedBox().toRequestShimmer(cxt, widget.isLoading); + + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); - switch (list[index].transactionType) { + Widget itemWidget; + switch (widget.list[index].transactionType) { case 1: - return ServiceRequestItemView(requestDetails: list[index]); - // case 1: - // return InternalAuditItemView(requestDetails: list[index]); + itemWidget = ServiceRequestItemView(requestDetails: widget.list[index]); + break; case 2: - return GasRefillItemView(requestDetails: list[index]); + itemWidget = GasRefillItemView(requestDetails: widget.list[index]); + break; case 3: - return DeviceItemView(requestDetails: list[index]); + itemWidget = DeviceItemView(requestDetails: widget.list[index]); + break; case 4: - return PpmItemView(requestDetails: list[index]); + itemWidget = PpmItemView(requestDetails: widget.list[index]); + break; case 5: - return RecurrentWoItemView(requestDetails: list[index]); + itemWidget = RecurrentWoItemView(requestDetails: widget.list[index]); + break; case 6: - return TaskRequestItemView(requestDetails: list[index]); + itemWidget = TaskRequestItemView(requestDetails: widget.list[index]); + break; case 7: - return TaskRequestItemView(requestDetails: list[index]); + itemWidget = TaskRequestItemView(requestDetails: widget.list[index]); + break; case 8: - return InventorySessionItemView(requestDetails: list[index]); + itemWidget = InventorySessionItemView(requestDetails: widget.list[index]); + break; case 9: - return TrafRequestItemView(requestDetails: list[index]); - //Need to verify the type No.. + itemWidget = TrafRequestItemView(requestDetails: widget.list[index]); + break; case 10: - return EquipmentInternalAuditItemView(requestDetails: list[index]); + itemWidget = EquipmentInternalAuditItemView(requestDetails: widget.list[index]); + break; case 11: - return SystemInternalAuditItemView(requestDetails: list[index]); + itemWidget = SystemInternalAuditItemView(requestDetails: widget.list[index]); + break; case 12: - return LoanEquipmentItemView(requestDetails: list[index]); + itemWidget = LoanEquipmentItemView(requestDetails: widget.list[index]); + break; case 13: - return AssetDeliveryItemView(requestDetails: list[index]); + itemWidget = AssetDeliveryItemView(requestDetails: widget.list[index]); + break; case 14: - return MedicalGasInspectionItemView(requestDetails: list[index]); + itemWidget = MedicalGasInspectionItemView(requestDetails: widget.list[index]); + break; case 15: - return DemoRequestItemView(requestDetails: list[index]); + itemWidget = DemoRequestItemView(requestDetails: widget.list[index]); + break; case 16: - return IncidentItemView(requestDetails: list[index]); + itemWidget = IncidentItemView(requestDetails: widget.list[index]); + break; default: - Container( + itemWidget = Container( height: 100, width: double.infinity, color: Colors.grey, ); } + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: itemWidget, + ), + ); }, separatorBuilder: (cxt, index) => 12.height, - itemCount: isLoading ? 6 : list.length); + itemCount: widget.isLoading ? 6 : widget.list.length); } } + + diff --git a/lib/views/widgets/equipment/my_assets_page.dart b/lib/views/widgets/equipment/my_assets_page.dart index 6342f3d9..92a168a8 100644 --- a/lib/views/widgets/equipment/my_assets_page.dart +++ b/lib/views/widgets/equipment/my_assets_page.dart @@ -35,7 +35,7 @@ class MyAssetsPage extends StatefulWidget { _MyAssetsPageState createState() => _MyAssetsPageState(); } -class _MyAssetsPageState extends State { +class _MyAssetsPageState extends State with SingleTickerProviderStateMixin { late AssetProvider _devicesProvider; late UserProvider _userProvider; late SettingProvider _settingProvider; @@ -45,6 +45,7 @@ class _MyAssetsPageState extends State { DeviceSearch? _searchAsset; bool showListView = true; + late AnimationController _animationController; TextEditingController numberController = TextEditingController(); TextEditingController snController = TextEditingController(); @@ -59,12 +60,18 @@ class _MyAssetsPageState extends State { @override void initState() { super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + _animationController.forward(); } @override void dispose() { numberController.dispose(); snController.dispose(); + _animationController.dispose(); _devicesProvider.reset(); super.dispose(); } @@ -193,29 +200,96 @@ class _MyAssetsPageState extends State { duration: const Duration(milliseconds: 500), child: showListView ? ListView.separated( + key: const ValueKey('list_view'), padding: const EdgeInsets.all(16), itemCount: _searchableList.length, separatorBuilder: (listContext, itemIndex) => 12.height, itemBuilder: (listContext, itemIndex) { - return AssetItemListView( - device: _searchableList[itemIndex], - onPressed: (device) { - widget.fromBottomBar ? Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: device.id) : Navigator.of(context).pop(device); - // Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: device.id); - }, + // Calculate staggered animation delay + final delay = itemIndex * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.7), + (delay + 0.4).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.15), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.7), + (delay + 0.4).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: AssetItemListView( + device: _searchableList[itemIndex], + onPressed: (device) { + widget.fromBottomBar ? Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: device.id) : Navigator.of(context).pop(device); + }, + ), + ), ); }, ) : GridView.builder( + key: const ValueKey('grid_view'), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 188 / 261, crossAxisSpacing: 12, mainAxisSpacing: 12), itemCount: _searchableList.length, padding: const EdgeInsets.all(16), itemBuilder: (context, index) { - return AssetItemGridView( - device: _searchableList[index], - onPressed: (device) { - widget.fromBottomBar ? Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: device.id) : Navigator.of(context).pop(device); - }, + // Calculate staggered animation delay + final delay = index * 0.08; + final animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOut, + ), + ), + ); + + final slideAnimation = Tween( + begin: const Offset(0.0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + delay.clamp(0.0, 0.8), + (delay + 0.3).clamp(0.0, 1.0), + curve: Curves.easeOutCubic, + ), + ), + ); + + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: slideAnimation, + child: AssetItemGridView( + device: _searchableList[index], + onPressed: (device) { + widget.fromBottomBar ? Navigator.of(context).pushNamed(AssetDetailPage.id, arguments: device.id) : Navigator.of(context).pop(device); + }, + ), + ), ); }, ),