Created
June 6, 2022 13:08
-
-
Save mondoktamas/f546c08ce2632e9ddcf1b287db399497 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:auto_route/auto_route.dart'; | |
import 'package:flutter/material.dart' hide DateUtils; | |
import 'package:flutter/services.dart'; | |
import 'package:flutter_bloc/flutter_bloc.dart'; | |
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | |
import 'package:flutter_svg/flutter_svg.dart'; | |
import 'package:sas/application/di/injection.dart'; | |
import 'package:sas/application/domain/entity/extended_airport_entity.dart'; | |
import 'package:sas/application/domain/entity/flights/flight_calendar_prices_entity.dart'; | |
import 'package:sas/application/presentation/features/main/features/book/features/booking_flow/cubit/booking_flow_cubit.dart'; | |
import 'package:sas/application/presentation/features/main/features/book/features/booking_flow/features/flights_page/cubit/flights_cubit.dart'; | |
import 'package:sas/application/presentation/resources/redesign/colors.dart'; | |
import 'package:sas/application/presentation/resources/redesign/images.dart'; | |
import 'package:sas/application/presentation/resources/redesign/theme.dart'; | |
import 'package:sas/application/presentation/widgets/redesign/breadcrumb/bread_crumb_widget.dart'; | |
import 'package:sas/application/presentation/widgets/redesign/buttons/circle_icon_button.dart'; | |
import 'package:sas/core/utils/currency_formatter.dart'; | |
import 'package:sas/core/utils/date_utils.dart'; | |
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; | |
import 'package:shimmer/shimmer.dart'; | |
const double _tabBarHeight = 108; | |
const double _listViewHeight = 260; | |
const double _collapsedAppBarHeight = _tabBarHeight + kToolbarHeight * 2; | |
const double _expandedAppBarHeight = _listViewHeight + kToolbarHeight * 2; | |
const double _listViewItemMaxHeight = 196; | |
const double _listViewItemMinHeight = 72; | |
const double _indicatorHeight = 3; | |
const double _pageViewItemHeight = 92; | |
const Duration _animationDuration = Duration(milliseconds: 300); | |
const double collapsedScrollPosition = _expandedAppBarHeight - _collapsedAppBarHeight; | |
double _selectedItemWidth(final BuildContext context) => MediaQuery.of(context).size.width / 3; | |
double _notSelectedItemWidth(final BuildContext context) => MediaQuery.of(context).size.width / 6; | |
class FlightsAppBarWidget extends StatefulWidget { | |
const FlightsAppBarWidget({final Key? key}) : super(key: key); | |
@override | |
State<FlightsAppBarWidget> createState() => _FlightsAppBarWidgetState(); | |
} | |
class _FlightsAppBarWidgetState extends State<FlightsAppBarWidget> with SingleTickerProviderStateMixin { | |
final ItemScrollController _itemScrollController = ItemScrollController(); | |
final PageController _pageController = PageController(viewportFraction: 0.9); | |
@override | |
void dispose() { | |
_pageController.dispose(); | |
super.dispose(); | |
} | |
void _scrollToSelectedItem( | |
final List<CalendarDayPriceEntity> models, | |
final DateTime selectedDate, | |
) { | |
if (!_itemScrollController.isAttached) { | |
return; | |
} | |
final index = models.indexWhere((final e) => e.dateTime == selectedDate); | |
if (index == -1) { | |
return; | |
} | |
_itemScrollController.scrollTo(index: index - 2, duration: _animationDuration); | |
} | |
@override | |
Widget build(final BuildContext context) => MultiBlocListener( | |
listeners: [ | |
BlocListener<FlightsCubit, FlightsState>( | |
listenWhen: (final previous, final current) => | |
previous.calendarPrices == null && current.calendarPrices != null, | |
listener: (final context, final state) async { | |
Future.delayed( | |
const Duration(milliseconds: 100), | |
() => _scrollToSelectedItem(state.modelsForExpandedAppBar, state.selectedDate), | |
); | |
}, | |
), | |
BlocListener<FlightsCubit, FlightsState>( | |
listenWhen: (final previous, final current) => previous.selectedPage != current.selectedPage, | |
listener: (final context, final state) { | |
_pageController.animateToPage( | |
state.selectedPage, | |
duration: _animationDuration, | |
curve: Curves.linear, | |
); | |
Future.delayed( | |
const Duration(milliseconds: 100), | |
() => _scrollToSelectedItem(state.modelsForExpandedAppBar, state.selectedDate), | |
); | |
}, | |
), | |
BlocListener<FlightsCubit, FlightsState>( | |
listenWhen: (final previous, final current) => previous.selectedDate != current.selectedDate, | |
listener: (final context, final state) { | |
Future.delayed( | |
const Duration(milliseconds: 100), | |
() => _scrollToSelectedItem(state.modelsForExpandedAppBar, state.selectedDate), | |
); | |
}, | |
), | |
], | |
child: BlocBuilder<FlightsCubit, FlightsState>( | |
builder: (final context, final flightsState) => SliverAppBar( | |
toolbarHeight: _collapsedAppBarHeight, | |
collapsedHeight: _collapsedAppBarHeight, | |
expandedHeight: _expandedAppBarHeight, | |
pinned: true, | |
backgroundColor: white, | |
automaticallyImplyLeading: false, | |
systemOverlayStyle: const SystemUiOverlayStyle(statusBarIconBrightness: Brightness.light), | |
flexibleSpace: _CustomFlexibleSpaceBar( | |
onExpanded: () { | |
if (flightsState.loading) { | |
return; | |
} | |
if (flightsState.calendarPrices != null) { | |
return; | |
} | |
if (!flightsState.firstAttemptSkipped) { | |
FlightsCubit.of(context).skipFirstAttempt(); | |
return; | |
} | |
FlightsCubit.of(context).fetchCalendarPrices( | |
fromAirportCode: flightsState.searchData.originAirport.code, | |
toAirportCode: flightsState.searchData.destinationAirport.code, | |
fromMonth: flightsState.searchData.outboundDate, | |
toMonth: flightsState.searchData.inboundDate, | |
bookingType: flightsState.searchData.bookingType, | |
roundTrip: flightsState.searchData.isRoundTrip, | |
); | |
}, | |
pinnedHeader: Stack( | |
children: [ | |
Column( | |
children: [ | |
BreadCrumbHeader( | |
currentStep: flightsState.selectedPage, | |
steps: [ | |
AppLocalizations.of(context)!.departure_flight, | |
if (flightsState.searchData.isRoundTrip) AppLocalizations.of(context)!.return_flight, | |
], | |
onStepChanged: context.read<FlightsCubit>().onPageChanged, | |
), | |
Container(height: kToolbarHeight, color: b2), | |
], | |
), | |
Positioned( | |
top: 12, | |
left: 12, | |
child: CircleIconButton( | |
onPressed: context.router.pop, | |
icon: SvgPicture.asset(icArrowLeft, color: white), | |
backgroundColor: b2LLight, | |
), | |
), | |
], | |
), | |
pinnedPageView: SizedBox( | |
height: _pageViewItemHeight + 16, | |
child: PageView.builder( | |
controller: _pageController, | |
onPageChanged: context.read<FlightsCubit>().onPageChanged, | |
physics: const PageScrollPhysics(parent: BouncingScrollPhysics()), | |
itemCount: 1 + (flightsState.searchData.isRoundTrip ? 1 : 0), | |
itemBuilder: (final context, final index) => _PageViewItemWidget( | |
origin: | |
index == 0 ? flightsState.searchData.originAirport : flightsState.searchData.destinationAirport, | |
destination: | |
index == 0 ? flightsState.searchData.destinationAirport : flightsState.searchData.originAirport, | |
onPressed: () => context.read<FlightsCubit>().onPageChanged(index), | |
), | |
), | |
), | |
collapsedTabBar: _CollapsedTabBar(flightsState: flightsState), | |
expandedListView: flightsState.modelsForExpandedAppBar.isEmpty | |
? const _LoadingListView() | |
: _ExpandedListView(flightsState: flightsState, itemScrollController: _itemScrollController), | |
), | |
), | |
), | |
); | |
} | |
class _LoadingListView extends StatelessWidget { | |
const _LoadingListView({final Key? key}) : super(key: key); | |
@override | |
Widget build(final BuildContext context) => SizedBox( | |
height: _listViewHeight, | |
child: Row( | |
crossAxisAlignment: CrossAxisAlignment.end, | |
children: [ | |
_LpcShimmer( | |
child: Container( | |
height: _listViewItemMaxHeight / 5 * 3, | |
width: _notSelectedItemWidth(context), | |
color: white, | |
), | |
), | |
_LpcShimmer( | |
child: Container( | |
height: _listViewItemMaxHeight / 5 * 4, | |
width: _notSelectedItemWidth(context), | |
color: white, | |
), | |
), | |
_LpcShimmer( | |
child: Container( | |
height: _listViewItemMaxHeight, | |
width: _selectedItemWidth(context), | |
color: white, | |
), | |
), | |
_LpcShimmer( | |
child: Container( | |
height: _listViewItemMaxHeight / 5 * 4, | |
width: _notSelectedItemWidth(context), | |
color: white, | |
), | |
), | |
_LpcShimmer( | |
child: Container( | |
height: _listViewItemMaxHeight / 5 * 3, | |
width: _notSelectedItemWidth(context), | |
color: white, | |
), | |
), | |
], | |
), | |
); | |
} | |
class _LpcShimmer extends StatelessWidget { | |
const _LpcShimmer({final Key? key, required this.child}) : super(key: key); | |
final Widget child; | |
@override | |
Widget build(final BuildContext context) => Shimmer( | |
gradient: LinearGradient( | |
colors: <Color>[white, white, g2.withOpacity(0.7), white, white], | |
stops: const <double>[0.0, 0.35, 0.5, 0.65, 1.0]), | |
child: child); | |
} | |
class _ExpandedListView extends StatelessWidget { | |
const _ExpandedListView({ | |
final Key? key, | |
required this.flightsState, | |
required this.itemScrollController, | |
}) : super(key: key); | |
final FlightsState flightsState; | |
final ItemScrollController itemScrollController; | |
@override | |
Widget build(final BuildContext context) => IgnorePointer( | |
ignoring: flightsState.loading, | |
child: SizedBox( | |
height: _listViewHeight, | |
child: ScrollablePositionedList.builder( | |
itemScrollController: itemScrollController, | |
scrollDirection: Axis.horizontal, | |
itemCount: flightsState.modelsForExpandedAppBar.length, | |
itemBuilder: (final context, final index) { | |
final model = flightsState.modelsForExpandedAppBar[index]; | |
final isDummyItem = index == 0 || | |
index == 1 || | |
index == flightsState.modelsForExpandedAppBar.length - 1 || | |
index == flightsState.modelsForExpandedAppBar.length - 2; | |
if (isDummyItem) { | |
return _DummyListViewItemWidget(model: model); | |
} | |
final selected = flightsState.selectedDate == model.dateTime; | |
const tempMaxHeight = _listViewItemMaxHeight - _listViewItemMinHeight; | |
final currentHeight = ((tempMaxHeight / flightsState.maxPrice) * model.price) + _listViewItemMinHeight; | |
return Container( | |
alignment: Alignment.bottomCenter, | |
child: AnimatedContainer( | |
duration: _animationDuration, | |
height: currentHeight, | |
width: selected ? _selectedItemWidth(context) : _notSelectedItemWidth(context), | |
decoration: BoxDecoration( | |
color: selected ? b1LLight : white, | |
boxShadow: selected | |
? null | |
: [BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 5, spreadRadius: 5)], | |
), | |
child: selected | |
? Stack( | |
children: [ | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Padding( | |
padding: const EdgeInsets.only(bottom: 52), | |
child: Container( | |
width: 50, | |
height: 50, | |
alignment: Alignment.center, | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: b1LLight, | |
boxShadow: [ | |
BoxShadow( | |
color: Colors.grey.withOpacity(0.1), | |
blurRadius: 15, | |
offset: const Offset(0, 5), | |
), | |
], | |
), | |
child: Container( | |
width: 30, | |
height: 30, | |
decoration: const BoxDecoration(shape: BoxShape.circle, color: g1), | |
), | |
), | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Padding( | |
padding: const EdgeInsets.only(bottom: 74), | |
child: _SelectedPriceWidget(model: model), | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Padding( | |
padding: const EdgeInsets.only(bottom: 16), | |
child: Text( | |
DateUtils.getWeekdayMonthDay(model.dateTime), | |
maxLines: 1, | |
style: Theme.of(context).textTheme.titleMediumBlack.copyWith(color: b2), | |
), | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Container( | |
width: _selectedItemWidth(context) - 32, | |
height: _indicatorHeight, | |
decoration: const BoxDecoration( | |
color: accent3, | |
borderRadius: BorderRadius.only( | |
topLeft: Radius.circular(24), | |
topRight: Radius.circular(24), | |
), | |
), | |
), | |
), | |
], | |
) | |
: GestureDetector( | |
onTap: () { | |
if (selected) { | |
return; | |
} | |
if (flightsState.isInboundSelected) { | |
context.read<BookingFlowCubit>().resetSelectedInboundFlightPage(); | |
context.read<BookingFlowCubit>().loadFlights(inboundDate: model.dateTime); | |
} else { | |
context.read<BookingFlowCubit>().resetSelectedOutboundFlightPage(); | |
context.read<BookingFlowCubit>().loadFlights(outboundDate: model.dateTime); | |
} | |
}, | |
child: Container( | |
width: _notSelectedItemWidth(context), | |
height: _expandedAppBarHeight, | |
padding: const EdgeInsets.fromLTRB(0, 16, 0, 16), | |
color: Colors.white, | |
child: Column( | |
children: [ | |
Text( | |
model.price.toString(), | |
style: Theme.of(context).textTheme.titleMediumBlack.copyWith(color: secondary2), | |
), | |
const Spacer(), | |
Text( | |
DateUtils.getDay(model.dateTime), | |
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: b3), | |
) | |
], | |
), | |
), | |
), | |
), | |
); | |
}, | |
), | |
), | |
); | |
} | |
class _DummyListViewItemWidget extends StatelessWidget { | |
const _DummyListViewItemWidget({final Key? key, required this.model}) : super(key: key); | |
final CalendarDayPriceEntity model; | |
@override | |
Widget build(final BuildContext context) => Container( | |
alignment: Alignment.bottomCenter, | |
child: Container( | |
width: _notSelectedItemWidth(context), | |
height: _listViewItemMaxHeight / 2, | |
alignment: Alignment.bottomCenter, | |
padding: const EdgeInsets.only(bottom: 16), | |
decoration: BoxDecoration( | |
color: white, | |
boxShadow: [ | |
BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 5, spreadRadius: 5), | |
], | |
), | |
child: Text( | |
DateUtils.getDay(model.dateTime), | |
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: b3.withOpacity(0.5)), | |
), | |
), | |
); | |
} | |
class _SelectedPriceWidget extends StatefulWidget { | |
const _SelectedPriceWidget({final Key? key, required this.model}) : super(key: key); | |
final CalendarDayPriceEntity model; | |
@override | |
State<_SelectedPriceWidget> createState() => _SelectedPriceWidgetState(); | |
} | |
class _SelectedPriceWidgetState extends State<_SelectedPriceWidget> with SingleTickerProviderStateMixin { | |
late final AnimationController _controller = AnimationController( | |
duration: _animationDuration, | |
vsync: this, | |
)..forward(); | |
late final Animation<double> _animation = Tween<double>(begin: 0.5, end: 1).animate(_controller); | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(final BuildContext context) => ScaleTransition( | |
scale: _animation, | |
child: FittedBox( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Container( | |
height: 32, | |
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), | |
decoration: BoxDecoration( | |
color: b3, | |
borderRadius: BorderRadius.circular(24), | |
), | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
AppLocalizations.of(context)!.from_short_form, | |
style: Theme.of(context).textTheme.bodyMediumBlack.copyWith(color: b1DDark), | |
), | |
const SizedBox(width: 2), | |
Text( | |
getIt<CurrencyFormatter>().formatPrice(widget.model.price, withSymbol: false), | |
style: Theme.of(context).textTheme.bodyMediumBlack.copyWith(color: white), | |
), | |
const SizedBox(width: 2), | |
Text( | |
context.read<BookingFlowCubit>().state.currencySymbol, | |
style: Theme.of(context).textTheme.bodyMediumBlack.copyWith(color: white), | |
), | |
], | |
), | |
), | |
SizedBox( | |
width: 8, | |
height: 17, | |
child: Stack( | |
children: [ | |
Positioned( | |
top: 0, | |
right: 0, | |
child: SizedBox( | |
width: 4, | |
height: 12, | |
child: CustomPaint(painter: _CurvePainter(true)), | |
), | |
), | |
Positioned( | |
top: 0, | |
left: 0, | |
child: SizedBox( | |
width: 4, | |
height: 12, | |
child: CustomPaint(painter: _CurvePainter(false)), | |
), | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: Container(width: 2, height: 12, color: b3), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: ClipOval(child: Container(width: 7, height: 7, color: b3)), | |
), | |
], | |
), | |
) | |
], | |
), | |
), | |
); | |
} | |
class _CurvePainter extends CustomPainter { | |
_CurvePainter(this._isRight); | |
final bool _isRight; | |
@override | |
void paint(final Canvas canvas, final Size size) { | |
final paint = Paint(); | |
paint.color = b3; | |
paint.style = PaintingStyle.fill; | |
final path = Path(); | |
if (_isRight) { | |
path.moveTo(4, 0); | |
path.quadraticBezierTo(-1, 5, 3, 12); | |
path.lineTo(0, 12); | |
path.lineTo(0, 0); | |
} else { | |
path.moveTo(0, 0); | |
path.quadraticBezierTo(5, 5, 1, 12); | |
path.lineTo(4, 12); | |
path.lineTo(4, 0); | |
} | |
canvas.drawPath(path, paint); | |
} | |
@override | |
bool shouldRepaint(final CustomPainter oldDelegate) => true; | |
} | |
class _LoadingTabBar extends StatelessWidget { | |
const _LoadingTabBar({final Key? key}) : super(key: key); | |
@override | |
Widget build(final BuildContext context) => Container( | |
color: white, | |
height: _tabBarHeight, | |
child: Row( | |
children: [ | |
for (var i = 0; i < 3; i++) | |
Expanded( | |
child: Container( | |
color: white, | |
padding: const EdgeInsets.only(bottom: 12, left: 16, right: 16), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.end, | |
children: [ | |
Shimmer.fromColors( | |
baseColor: white, | |
highlightColor: g2, | |
child: Container(height: 16, color: white), | |
), | |
const SizedBox(height: 4), | |
Shimmer.fromColors( | |
baseColor: white, | |
highlightColor: g2, | |
child: Container(height: 16, color: white), | |
), | |
], | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
class _CollapsedTabBar extends StatelessWidget { | |
const _CollapsedTabBar({final Key? key, required this.flightsState}) : super(key: key); | |
final FlightsState flightsState; | |
@override | |
Widget build(final BuildContext context) { | |
if (flightsState.tabsInfo == null) return const _LoadingTabBar(); | |
return Row( | |
children: List.generate( | |
flightsState.tabsInfoList.length, | |
(final index) { | |
final model = flightsState.tabsInfoList[index]; | |
final selected = flightsState.selectedDate == model.date; | |
return GestureDetector( | |
onTap: () { | |
if (selected) { | |
return; | |
} | |
if (flightsState.isInboundSelected) { | |
context.read<BookingFlowCubit>().resetSelectedInboundFlightPage(); | |
context.read<BookingFlowCubit>().loadFlights(inboundDate: model.date); | |
} else { | |
context.read<BookingFlowCubit>().resetSelectedOutboundFlightPage(); | |
context.read<BookingFlowCubit>().loadFlights(outboundDate: model.date); | |
} | |
}, | |
child: Container( | |
width: _selectedItemWidth(context), | |
height: _tabBarHeight, | |
alignment: Alignment.bottomCenter, | |
color: selected ? b1LLight : white, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
AnimatedDefaultTextStyle( | |
duration: _animationDuration, | |
style: TextStyle( | |
fontSize: 14, | |
fontWeight: selected ? FontWeight.w900 : FontWeight.w500, | |
color: selected ? b2 : secondary2, | |
), | |
child: Text(DateUtils.getDayMonth(model.date)), | |
), | |
const SizedBox(height: 2), | |
Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
AnimatedDefaultTextStyle( | |
duration: _animationDuration, | |
style: TextStyle( | |
fontSize: 14, | |
fontWeight: FontWeight.w900, | |
color: selected ? b2 : secondary2, | |
), | |
child: Text(getIt<CurrencyFormatter>().formatPrice(model.price, withSymbol: false)), | |
), | |
const SizedBox(width: 2), | |
AnimatedDefaultTextStyle( | |
duration: _animationDuration, | |
style: TextStyle( | |
fontSize: 14, | |
fontWeight: FontWeight.w500, | |
color: selected ? b2 : secondary2, | |
), | |
child: Text(context.read<BookingFlowCubit>().state.currencySymbol), | |
), | |
], | |
), | |
const SizedBox(height: 12), | |
if (selected) | |
Container( | |
width: _selectedItemWidth(context) - 32, | |
height: _indicatorHeight, | |
decoration: const BoxDecoration( | |
color: accent3, | |
borderRadius: BorderRadius.only( | |
topLeft: Radius.circular(24), | |
topRight: Radius.circular(24), | |
), | |
), | |
) | |
else | |
const SizedBox(height: 3), | |
], | |
), | |
), | |
); | |
}, | |
), | |
); | |
} | |
} | |
class _PageViewItemWidget extends StatelessWidget { | |
const _PageViewItemWidget({ | |
final Key? key, | |
required this.origin, | |
required this.destination, | |
required this.onPressed, | |
}) : super(key: key); | |
final ExtendedAirportEntity origin; | |
final ExtendedAirportEntity destination; | |
final VoidCallback onPressed; | |
@override | |
Widget build(final BuildContext context) => GestureDetector( | |
onTap: onPressed, | |
child: Container( | |
height: _pageViewItemHeight, | |
padding: const EdgeInsets.fromLTRB(24, 0, 24, 0), | |
margin: const EdgeInsets.fromLTRB(6, 0, 6, 16), | |
alignment: Alignment.center, | |
decoration: const BoxDecoration( | |
color: white, | |
borderRadius: BorderRadius.all(Radius.circular(8)), | |
boxShadow: [BoxShadow(color: defaultCardShadow, blurRadius: 16, offset: Offset(0, 6))], | |
), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Row( | |
children: [ | |
Text( | |
origin.name, | |
style: Theme.of(context).textTheme.titleMedium!.copyWith(color: g5TextGray), | |
), | |
const Spacer(), | |
Text( | |
destination.name, | |
style: Theme.of(context).textTheme.titleMedium!.copyWith(color: g5TextGray), | |
), | |
], | |
), | |
const SizedBox(height: 8), | |
Row( | |
children: [ | |
Text( | |
origin.code, | |
style: Theme.of(context).textTheme.headlineSmallBlack.copyWith( | |
color: b3, | |
fontSize: 22, | |
height: 27.5 / 22, | |
), | |
), | |
const SizedBox(width: 16), | |
Expanded( | |
child: Stack( | |
alignment: Alignment.center, | |
children: [ | |
Container( | |
height: 0.8, | |
decoration: const BoxDecoration( | |
gradient: LinearGradient( | |
colors: [ | |
flightCardGradient1, | |
flightCardGradient2, | |
flightCardGradient2, | |
flightCardGradient1, | |
], | |
stops: [0.0, 0.51, 0.55, 1.0], | |
), | |
), | |
), | |
SvgPicture.asset(icPlaneRight24), | |
], | |
), | |
), | |
const SizedBox(width: 16), | |
Text( | |
destination.code, | |
style: Theme.of(context).textTheme.headlineSmallBlack.copyWith( | |
color: b3, | |
fontSize: 22, | |
height: 27.5 / 22, | |
), | |
), | |
], | |
), | |
], | |
), | |
), | |
); | |
} | |
class _CustomFlexibleSpaceBar extends StatelessWidget { | |
const _CustomFlexibleSpaceBar({ | |
final Key? key, | |
required this.pinnedHeader, | |
required this.pinnedPageView, | |
required this.collapsedTabBar, | |
required this.expandedListView, | |
required this.onExpanded, | |
}) : super(key: key); | |
final Widget pinnedHeader; | |
final Widget pinnedPageView; | |
final Widget collapsedTabBar; | |
final Widget expandedListView; | |
final VoidCallback onExpanded; | |
@override | |
Widget build(final BuildContext context) => ColoredBox( | |
color: b2, | |
child: SafeArea( | |
child: LayoutBuilder( | |
builder: (final BuildContext context, final BoxConstraints constraints) { | |
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!; | |
final deltaExtent = settings.maxExtent - settings.minExtent; | |
final tempCurrentExtent = settings.currentExtent - settings.minExtent; | |
double collapsedOpacity; | |
var collapsedCurrentExtent = tempCurrentExtent; | |
if (collapsedCurrentExtent > (deltaExtent / 2)) { | |
collapsedOpacity = 0; | |
} else { | |
collapsedCurrentExtent = (deltaExtent / 2) - collapsedCurrentExtent; | |
collapsedOpacity = (collapsedCurrentExtent / deltaExtent) * 2; | |
} | |
double expandedOpacity; | |
var expandedCurrentExtent = tempCurrentExtent; | |
if (expandedCurrentExtent < (deltaExtent / 2)) { | |
expandedOpacity = 0; | |
} else { | |
expandedCurrentExtent = expandedCurrentExtent - (deltaExtent / 2); | |
expandedOpacity = (expandedCurrentExtent / deltaExtent) * 2; | |
} | |
if (expandedOpacity >= 0.67) { | |
onExpanded(); | |
} | |
return Stack( | |
children: [ | |
Column( | |
children: [ | |
pinnedHeader, | |
Expanded( | |
child: ColoredBox( | |
color: white, | |
child: Stack( | |
children: [ | |
Positioned.fill( | |
child: AnimatedOpacity( | |
duration: _animationDuration, | |
opacity: expandedOpacity, | |
child: expandedListView, | |
), | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: AnimatedSwitcher( | |
duration: _animationDuration, | |
transitionBuilder: (final child, final animation) => Opacity( | |
opacity: collapsedOpacity, | |
child: child, | |
), | |
child: collapsedOpacity == 0 ? const SizedBox.shrink() : collapsedTabBar, | |
), | |
), | |
], | |
), | |
), | |
), | |
], | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: Padding( | |
padding: const EdgeInsets.only(top: 64), | |
child: pinnedPageView, | |
), | |
), | |
], | |
); | |
}, | |
), | |
), | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment