feat : wave bottom sheet :)

This commit is contained in:
2025-04-29 16:11:13 +03:30
parent caa3884242
commit bb4eccd01d
3 changed files with 174 additions and 381 deletions

View File

@@ -12,37 +12,40 @@ class RootPage extends GetView<RootLogic> {
return Scaffold(
body: Stack(
children: [
// سایر محتواها (مثلا صفحات اصلی)
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 90,
color: AppColor.blueNormal,
clipBehavior: Clip.none,
padding: const EdgeInsets.all(10),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Positioned(
top: -43,
child: Container(
width: 110,
height: 110,
clipBehavior: Clip.none,
decoration: ShapeDecoration(
color:Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(200),
side: BorderSide(width: 2, color: Colors.white),
),
),
),
),
WavePageView(),
],
),
child: WaveBottomNavigation(
items: [
WaveBottomNavigationItem(
title: 'خانه',
icon:Assets.vecMapSvg,
),
WaveBottomNavigationItem(
title: 'عملیات',
icon:Assets.vecUserSvg,
),
WaveBottomNavigationItem(
title: 'افزودن',
icon:Assets.vecAddSvg,
),
WaveBottomNavigationItem(
title: 'آمار',
icon:Assets.vecDiagramSvg,
), WaveBottomNavigationItem(
title: 'تماس',
icon:Assets.vecCallSvg,
), WaveBottomNavigationItem(
title: 'مکان ',
icon:Assets.vecGpsSvg,
), WaveBottomNavigationItem(
title: 'تاریخ',
icon:Assets.vecCalendarSvg,
),
],
onPageChanged: (index) {
controller.changePage(index);
},
),
),
],
@@ -50,343 +53,3 @@ class RootPage extends GetView<RootLogic> {
);
}
}
class BottomNavigation1ItemTST extends StatelessWidget {
final String icon;
final String label;
final bool isSelected;
final Function() onTap;
const BottomNavigation1ItemTST({
super.key,
required this.icon,
required this.label,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 80,
height: 130,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.bottomCenter,
children: [
AnimatedPositioned(
duration: const Duration(milliseconds: 400),
width: 80,
height: 80,
bottom: isSelected ? 50 : 0,
child: InkWell(
splashColor: Colors.transparent,
onTap: onTap,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: isSelected ? AppColor.greenNormal : Colors.transparent,
borderRadius: BorderRadius.circular(40),
border:
isSelected
? Border.all(width: 2, color: Colors.white)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
vecWidget(icon, width: 32, height: 32, color: Colors.white),
const SizedBox(height: 7),
Text(
label,
style: AppFonts.yekan14.copyWith(color: Colors.white),
),
],
),
),
),
),
],
),
);
}
}
class ScrollWithCenterScaling extends StatefulWidget {
const ScrollWithCenterScaling({super.key});
@override
State<ScrollWithCenterScaling> createState() =>
_ScrollWithCenterScalingState();
}
class _ScrollWithCenterScalingState extends State<ScrollWithCenterScaling> {
final PageController _pageController = PageController(viewportFraction: 0.3);
double currentPage = 0.0;
@override
void initState() {
super.initState();
_pageController.addListener(() {
setState(() {
currentPage = _pageController.page ?? 0.0;
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
double _calculateScale(int index) {
final double distance = (currentPage - index).abs();
if (distance >= 1) {
return 0.7;
} else {
return 1.3;
}
}
bool _isSelected(int index) {
return (currentPage - index).abs() < 0.5; // دقیق وسط بودن رو بگیره
}
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: _pageController,
itemCount: 50,
clipBehavior: Clip.none,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final scale = _calculateScale(index);
final selected = _isSelected(index);
return Transform.scale(
scale: scale,
child: AnimatedPadding(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
padding: EdgeInsets.only(bottom: selected ? 20 : 0),
child: InkWell(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.map, color: Colors.white),
const SizedBox(height: 8),
Text(
'Label $index',
style: TextStyle(
color: Colors.white,
fontSize: selected ? 18 : 0,
fontWeight:
selected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
},
);
}
}
class ScrollWithCenterRotation2 extends StatefulWidget {
const ScrollWithCenterRotation2({super.key});
@override
State<ScrollWithCenterRotation2> createState() =>
_ScrollWithCenterRotationState2();
}
class _ScrollWithCenterRotationState2 extends State<ScrollWithCenterRotation2> {
final PageController _pageController = PageController(viewportFraction: 0.3);
double currentPage = 0.0;
@override
void initState() {
super.initState();
_pageController.addListener(() {
setState(() {
currentPage = _pageController.page ?? 0.0;
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
double _calculateScale(int index) {
final double distance = (currentPage - index).abs();
if (distance >= 1) {
return 0.7;
} else {
return 1.2;
}
}
double _calculateRotationY(int index) {
return (currentPage - index) * 0.5;
}
bool _isSelected(int index) {
return (currentPage - index).abs() < 0.5;
}
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: _pageController,
itemCount: 50,
clipBehavior: Clip.none,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final scale = _calculateScale(index);
final rotationY = _calculateRotationY(index);
final selected = _isSelected(index);
return AnimatedContainer(
clipBehavior: Clip.none,
height: 80,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
margin: EdgeInsets.only(
bottom: selected ? 90 : 20, // وسطی بالاتر میاد
),
child: Transform(
transform:
Matrix4.identity()
..scale(scale)
..setEntry(3, 2, 0.001),
alignment: Alignment.center,
child: InkWell(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: selected ? 70 : 60,
height: selected ? 70 : 60,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
boxShadow:
selected
? [
BoxShadow(
color: Colors.amber,
blurRadius: 20,
spreadRadius: 5,
),
]
: [],
),
alignment: Alignment.center,
child: Icon(
Icons.map,
color: Colors.white,
size: selected ? 32 : 24,
),
),
const SizedBox(height: 8),
Text(
'Label $index',
style: TextStyle(
color: Colors.white,
fontSize: selected ? 18 : 14,
fontWeight:
selected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
),
);
},
);
}
}
class WavePageView extends StatefulWidget {
@override
_WavePageViewState createState() => _WavePageViewState();
}
class _WavePageViewState extends State<WavePageView> {
final PageController _controller = PageController(viewportFraction: 0.3);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double _calculateScale(double currentPage,int index) {
final double distance = (currentPage - index).abs();
if (distance >= 1) {
return 0.7;
} else {
return 1.7;
}
}
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: _controller,
clipBehavior: Clip.none,
scrollDirection: Axis.horizontal,
itemCount: 10,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
double value = 0.0;
final scale = _calculateScale(_controller.page??0,index);
value = index - (_controller.page ?? 0);
value = (value).clamp(-1, 1);
eLog("index = $index \n value = $value");
double offset = value * 30;
if (value.abs() < 0.1 || value.abs() > 0.1) {
offset = -5 * (1 - value.abs() * 5);
}
return Transform.scale(
scale: scale,
child: Transform.translate(
offset: Offset(0, offset),
child: Column(
children: [
Icon(Icons.map, color: value.toInt() == 0 ? AppColor.greenNormal:Colors.white, size: 32),
Visibility(
visible: value.toInt() == 0,
child: Text(
'بازرسی',
style: AppFonts.yekan10.copyWith(color: AppColor.greenNormal)
),
),
],
),
),
);
},
);
},
);
}
}

View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
class WaveBottomNavigationItem {
final String title;
final String icon;
WaveBottomNavigationItem({required this.title, required this.icon});
}
class WaveBottomNavigation extends StatefulWidget {
const WaveBottomNavigation({
super.key,
required this.items,
required this.onPageChanged,
});
final List<WaveBottomNavigationItem> items;
final Function(int) onPageChanged;
@override
State<WaveBottomNavigation> createState() => _WaveBottomNavigationState();
}
class _WaveBottomNavigationState extends State<WaveBottomNavigation> {
final PageController _controller = PageController(viewportFraction: 0.3);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double _calculateScale(double currentPage, int index) {
final double distance = (currentPage - index).abs();
if (distance >= 1) {
return 0.9;
} else {
return 1.50;
}
}
@override
Widget build(BuildContext context) {
return Container(
height: 68,
color: AppColor.blueNormal,
clipBehavior: Clip.none,
padding: const EdgeInsets.all(10),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [backgroundWidget(), waveScrolledLists()],
),
);
}
Widget backgroundWidget() {
return Positioned(
top: -43,
child: Container(
width: 90,
height: 90,
clipBehavior: Clip.none,
decoration: ShapeDecoration(
color: AppColor.greenNormal,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(200),
side: BorderSide(width: 5, color: Colors.white),
),
),
),
);
}
Widget waveScrolledLists() {
return PageView.builder(
controller: _controller,
clipBehavior: Clip.none,
onPageChanged: widget.onPageChanged,
scrollDirection: Axis.horizontal,
itemCount: widget.items.length,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final WaveBottomNavigationItem item = widget.items[index];
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
double value = 0.0;
final scale = _calculateScale(_controller.page ?? 0, index);
value = index - (_controller.page ?? 0);
value = (value).clamp(-1, 1);
double offset = value * 30;
if (value.abs() < 0.2 || value.abs() > 0.2) {
offset = -7 * (1 - value.abs() * 2);
}
return Transform.scale(
scale: scale,
child: Transform.translate(
offset: Offset(0, offset),
child: Column(
children: [
Tooltip(
message: item.title,
child: vecWidget(
item.icon,
color: Colors.white,
width: 32,
height: 32,
),
),
/* Visibility(
visible: (_controller.page ?? 0) == index,
child: Text(
item.title,
style: AppFonts.yekan10.copyWith(color: Colors.white),
),
),*/
],
),
),
);
},
),
);
},
);
}
}

View File

@@ -1,18 +1,16 @@
export 'vec_widget.dart';
export 'app_bar/r_app_bar.dart';
export 'bottom_navigation/bottom_navigation_1.dart';
export 'bottom_navigation/wave_bottom_navigation.dart';
export 'buttons/elevated.dart';
export 'buttons/outline_elevated.dart';
export 'buttons/outline_elevated_icon.dart';
export 'buttons/text_button.dart';
export 'captcha/captcha_widget.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet_controller.dart';
export 'buttons/outline_elevated_icon.dart';
export 'buttons/outline_elevated.dart';
export 'buttons/text_button.dart';
export 'app_bar/r_app_bar.dart';
export 'captcha/captcha_widget.dart';
export 'buttons/elevated.dart';
export 'inputs/r_input.dart';
export 'tabs/new_tab.dart';
export 'tabs/tab.dart';
export 'pagination/pagination_from_until.dart';
export 'pagination/show_more.dart';
export 'tabs/new_tab.dart';
export 'tabs/tab.dart';
export 'vec_widget.dart';