I have this design in flutter. I want it so that when the user scrolls down the exercise list, at the bottom of the exercise list (ALWAYS THERE – Liked fixed in place a certain height above the bottom of the container), is the start button with the expandableFAB. The container surrounding the start button with the FAB should be semi-transparent and also have a border radius. Thank you so much for your help in advance!
My code:
import 'package:flutter/material.dart';
import 'package:workout_app/Screens/Components/Widgets/footer.dart';
class workout_page extends StatefulWidget {
@override
_WorkoutPageState createState() => _WorkoutPageState();
}
class _WorkoutPageState extends State<workout_page>
with TickerProviderStateMixin {
late TabController _tabController;
late PageController _pageController;
List<Map<String, dynamic>> workoutMap = [
{
'name': 'My First Workout',
'exercises': [
{
'name': 'Bicep Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Preacher Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Bicep Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Preacher Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Bicep Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Preacher Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Bicep Curl',
'musclesworked': ['bicep', 'tricep']
},
{
'name': 'Preacher Curl',
'musclesworked': ['bicep', 'tricep']
}
]
},
];
@override
void initState() {
super.initState();
_tabController = TabController(length: workoutMap.length + 1, vsync: this);
_pageController = PageController();
}
TextEditingController _newTabController = TextEditingController();
void _addTab() {
setState(() {
String newTabName = _newTabController.text;
Map<String, dynamic> newTab = {
'name': newTabName,
'exercises': [],
};
workoutMap.add(newTab);
_newTabController.clear();
_tabController = TabController(
length: workoutMap.length + (workoutMap.length < 4 ? 1 : 0),
vsync: this,
);
});
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Container(
decoration: BoxDecoration(color: Color.fromARGB(255, 67, 67, 67)),
child: DefaultTabController(
length: workoutMap.length + (workoutMap.length < 4 ? 1 : 0),
child: Scaffold(
backgroundColor: Color.fromRGBO(79, 79, 79, 1),
body: Stack(
children: [
CustomScrollView(
physics: NeverScrollableScrollPhysics(),
slivers: [
SliverAppBar(
expandedHeight: 30,
titleSpacing: 15,
pinned: true,
elevation: 0,
backgroundColor: Color.fromARGB(255, 19, 19, 19),
centerTitle: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0),
child: Column(children: [
TabBar(
controller: _tabController,
onTap: (index) {
if (index == _tabController.length - 1 &&
workoutMap.length < 4) {
_tabController
.animateTo(_tabController.previousIndex);
} else {
_tabController.animateTo(index);
_pageController.jumpToPage(index);
}
},
tabs: [
...workoutMap.map(
(tab) => Tab(text: tab['name'] as String)),
if (workoutMap.length < 4)
Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Add Tab'),
content: TextField(
controller: _newTabController,
decoration: InputDecoration(
hintText: 'Enter tab name',
),
),
actions: [
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: Text('Add'),
onPressed: () {
_addTab();
Navigator.pop(context);
},
),
],
),
);
},
),
),
],
),
])),
),
SliverToBoxAdapter(
child: Container(
height: size.height * .6,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
if (index == _tabController.length - 1 &&
workoutMap.length < 4) {
_pageController.jumpToPage(_tabController.index);
} else {
_tabController.animateTo(index);
}
},
children: [
...workoutMap.map(
(tab) => Container(
width: size.width,
height: size.height * .6,
decoration: BoxDecoration(
color: Color.fromARGB(255, 41, 41, 41)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
tab['exercises'].length == 0
? Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons
.sentiment_very_dissatisfied,
size: size.width *
.3, // You can adjust the size as per your need
color: Colors
.white, // You can adjust the color as per your need
),
SizedBox(height: 10),
Text(
'This folder is empty. Add some workouts',
style: TextStyle(
color: Colors.white),
textAlign: TextAlign.center,
),
],
)
: Expanded(
child: ListView.builder(
itemCount:
tab['exercises'].length,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.only(
bottom: 10.0,
left: 10,
right: 10),
decoration: BoxDecoration(
color: Colors.black,
borderRadius:
BorderRadius
.circular(
10.0)),
child: Column(children: [
Container(
height:
size.height * .15,
decoration:
BoxDecoration(
color: Color.fromARGB(
255, 94, 94, 94),
borderRadius:
BorderRadius
.circular(
10.0),
),
child: Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Container(
width:
size.width *
.4,
child:
ClipRRect(
borderRadius:
BorderRadius
.circular(
10.0),
child: Image
.network(
'https://i.giphy.com/media/14kdiJUblbWBXy/giphy.gif', // Replace with your GIF URL
),
),
),
),
Positioned(
top: 10,
left: size.width *
.4 +
20,
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start, // Add this line
children: [
Text(
tab['exercises']
[
index]
[
'name'],
style: TextStyle(
color: Colors
.black,
fontSize:
20,
fontWeight:
FontWeight.bold),
),
Text(
(tab['exercises'][index]['musclesworked']
as List)
.join(
', '), // join the list into a string
style: TextStyle(
fontSize:
16), // adjust style as needed
)
]),
),
],
),
),
SizedBox(height: 10),
Row(children: [
Spacer(),
Text(
'3 Sets', // Replace with your sets, reps, and weight data
style: TextStyle(
color:
Colors.white),
textAlign:
TextAlign.center,
),
Spacer(),
Text(
'4 reps', // Replace with your sets, reps, and weight data
style: TextStyle(
color:
Colors.white),
textAlign:
TextAlign.center,
),
Spacer(),
Text(
'30 kg', // Replace with your sets, reps, and weight data
style: TextStyle(
color:
Colors.white),
textAlign:
TextAlign.center,
),
Spacer()
]),
SizedBox(height: 10),
]));
},
),
),
SizedBox(height: 10),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Spacer(),
Container(
decoration: BoxDecoration(
color:
Color.fromARGB(255, 0, 255, 8),
borderRadius:
BorderRadius.circular(10.0),
),
height: size.height * .08,
width: size.width * .5,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: <Widget>[
Text('Start',
style:
TextStyle(fontSize: 30)),
],
),
),
SizedBox(width: 20),
ExpandableFab(),
Spacer(),
],
),
SizedBox(height: 10),
])),
)
],
),
))
],
),
Footer(tab: 'Workout'),
],
),
),
),
);
}
}
class CustomScrollPhysics extends ScrollPhysics {
final TabController tabController;
CustomScrollPhysics({required this.tabController, ScrollPhysics? parent})
: super(parent: parent);
@override
CustomScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomScrollPhysics(tabController: tabController, parent: ancestor);
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if ((tabController.index == tabController.length - 2 && offset > 0)) {
return 0.0;
}
return offset;
}
}
class ExpandableFab extends StatefulWidget {
@override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animation =
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleExpanded() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_animationController.forward();
} else {
_animationController.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (_isExpanded) _buildOptionButton(Icons.dangerous, 'Dangerous'),
if (_isExpanded) _buildOptionButton(Icons.delete, 'Delete'),
if (_isExpanded) _buildOptionButton(Icons.edit, 'Edit'),
FloatingActionButton(
heroTag: 'Blah',
onPressed: _toggleExpanded,
tooltip: _isExpanded ? 'Close' : 'Open',
backgroundColor: Color.fromARGB(255, 100, 100, 100),
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.rotate(
angle: _animation.value * 0.5 * 3.1415,
child: Icon(
_isExpanded ? Icons.close : Icons.add,
),
);
},
),
),
],
);
}
Widget _buildOptionButton(IconData iconData, String tooltip) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: FloatingActionButton(
backgroundColor: Color.fromARGB(255, 100, 100, 100),
heroTag: tooltip,
onPressed: () {},
tooltip: tooltip,
child: Icon(iconData),
),
);
}
}
2
Answers
try
customize withOpacity(xx) to your liking
You can change opacity by change value of 255 from below.