英文:
Make list reorderable
问题
这是我的完整代码:
import 'dart:collection';
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;
TextEditingController _newTabController = TextEditingController();
late var currentPageIndx;
List<String> plans = [];
late String firstplan;
List<Map<String, dynamic>> workoutMap = [
{
'plan_name': 'My First Plan',
'content': [
{
'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']
}
]
}
]
},
{
'plan_name': 'My Second Plan',
'content': [
{
'name': 'My Second Workout',
'exercises': [
{
'name': 'POOPY 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();
for (var i = 0; i < workoutMap.length; i++) {
plans.add(workoutMap[i]['plan_name']);
}
firstplan = plans[0];
currentPageIndx = 1;
_tabController = TabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
vsync: this,
);
_pageController = PageController();
}
void _addTab() {
setState(() {
String newTabName = _newTabController.text;
Map<String, Object> newTab = {
'name': newTabName,
'exercises': [],
};
workoutMap[currentPageIndx]['content'].add(newTab);
_newTabController.clear();
_tabController = TabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
vsync: this,
);
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Container(
child: Material(
color: Colors.green,
child: DefaultTabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Color.fromRGBO(79, 79, 79, 1),
body: Stack(
children: [
CustomScrollView(
slivers: [
SliverAppBar(
titleSpacing: 15,
pinned: true,
elevation: 0,
backgroundColor: Color.fromARGB(255, 17, 17, 17),
centerTitle: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(40),
child: Column(
children: [
Container(
color: Color.fromARGB(255, 17, 17, 17),
child: Row(
children: [
Spacer(),
Container(
padding: EdgeInsets.symmetric(
horizontal:
10.0),
decoration: BoxDecoration(
color: Color.fromARGB(255, 35, 35, 35),
borderRadius: BorderRadius.circular(
3.0),
),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: size.width * .5,
maxHeight: 35),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: firstplan,
dropdownColor:
Color.fromARGB(255, 35, 35, 35),
isExpanded:
true,
items: plans
.map(
(item) =>
DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: TextStyle(
fontSize: 15,
color: Colors.white,
),
),
),
)
.toList(),
onChanged: (item) {
setState(() {
firstplan = item!;
currentPageIndx =
plans.indexOf(firstplan);
_tabController = TabController(
length: workoutMap[
currentPageIndx]
['content']
.length +
(workoutMap[currentPageIndx]
['content']
.length < 4
? 1
: 0),
vsync: this,
);
});
},
),
),
),
),
SizedBox(width: 10),
Transform.translate(
offset: Offset(0, .3),
child: ElevatedButton(
onPressed: () => print('
<details>
<summary>英文:</summary>
I have a list of widgets (exercises) in my screen. I want to make it so that the exercises are reorderable, so I can drag and drop them around and the order changes.
This is my COMPLETE code:
import 'dart:collection';
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;
TextEditingController _newTabController = TextEditingController();
late var currentPageIndx;
List<String> plans = [];
late String firstplan;
List<Map<String, dynamic>> workoutMap = [
{
'plan_name': 'My First Plan',
'content': [
{
'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']
}
]
}
]
},
{
'plan_name': 'My Second Plan',
'content': [
{
'name': 'My Second Workout',
'exercises': [
{
'name': 'POOPY 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();
for (var i = 0; i < workoutMap.length; i++) {
plans.add(workoutMap[i]['plan_name']);
}
firstplan = plans[0];
currentPageIndx = 1;
_tabController = TabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
vsync: this,
);
_pageController = PageController();
}
void _addTab() {
setState(() {
String newTabName = _newTabController.text;
Map<String, Object> newTab = {
'name': newTabName,
'exercises': [],
};
workoutMap[currentPageIndx]['content'].add(newTab);
_newTabController.clear();
_tabController = TabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
vsync: this,
);
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Container(
child: Material(
color: Colors.green,
child: DefaultTabController(
length: workoutMap[currentPageIndx]['content'].length +
(workoutMap[currentPageIndx]['content'].length < 4 ? 1 : 0),
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Color.fromRGBO(79, 79, 79, 1),
body: Stack(
children: [
CustomScrollView(
slivers: [
SliverAppBar(
titleSpacing: 15,
pinned: true,
elevation: 0,
backgroundColor: Color.fromARGB(255, 17, 17, 17),
centerTitle: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(40),
child: Column(
children: [
Container(
color: Color.fromARGB(255, 17, 17, 17),
child: Row(
children: [
Spacer(),
Container(
padding: EdgeInsets.symmetric(
horizontal:
10.0), // 1. Padding on the left and right sides
decoration: BoxDecoration(
color: Color.fromARGB(255, 35, 35, 35),
borderRadius: BorderRadius.circular(
3.0), // 2. Circular edges
),
child: ConstrainedBox(
// To3make the entire container smaller
constraints: BoxConstraints(
maxWidth: size.width * .5,
maxHeight: 35),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: firstplan,
dropdownColor:
Color.fromARGB(255, 35, 35, 35),
isExpanded:
true, // To make sure dropdown appears below
items: plans
.map(
(item) =>
DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: TextStyle(
fontSize: 15,
color: Colors.white,
),
),
),
)
.toList(),
onChanged: (item) {
setState(() {
firstplan = item!;
currentPageIndx =
plans.indexOf(firstplan);
_tabController = TabController(
length: workoutMap[
currentPageIndx]
['content']
.length +
(workoutMap[currentPageIndx]
['content']
.length <
4
? 1
: 0),
vsync: this,
);
});
},
),
),
),
),
SizedBox(width: 10),
Transform.translate(
offset: Offset(0, .3),
child: ElevatedButton(
onPressed: () => print('premium'),
style: ElevatedButton.styleFrom(
primary:
Color.fromARGB(255, 35, 35, 35),
),
child: Text('Go Premium'),
),
),
Spacer()
],
),
),
Container(
decoration: BoxDecoration(
color: Color.fromRGBO(60, 60, 60, 1),
),
child: TabBar(
indicatorColor: Colors.grey,
controller: _tabController,
onTap: (index) {
if (index == _tabController.length - 1 &&
workoutMap[currentPageIndx]['content']
.length <
4) {
_tabController.animateTo(
_tabController.previousIndex);
} else {
_tabController.animateTo(index);
_pageController.jumpToPage(index);
}
},
tabs: [
...workoutMap[currentPageIndx]['content'].map(
(tab) => Tab(
text: tab['name'] as String,
),
),
if (workoutMap[currentPageIndx]['content']
.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: SizedBox(
height: size.height - 170,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
if (index == _tabController.length - 1 &&
workoutMap[currentPageIndx]['content'].length <
4) {
_pageController.jumpToPage(_tabController.index);
} else {
_tabController.animateTo(index);
}
},
children: [
...workoutMap[currentPageIndx]['content'].map(
(tab) => Container(
child: tab['exercises'].length == 0
? Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.sentiment_very_dissatisfied,
size: size.width * 0.3,
color: Colors.white,
),
SizedBox(height: 10),
Text(
'This folder is empty. Add some workouts',
style: TextStyle(
color: Colors.white),
textAlign: TextAlign.center,
),
SizedBox(height: size.height * .1),
],
)
: Container(
height: size.height, // specify height
child: ReorderableListView.builder(
onReorder:
(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = tab['exercises']
.removeAt(oldIndex);
tab['exercises']
.insert(newIndex, item);
});
},
itemCount: tab['exercises'].length,
itemBuilder: (BuildContext context,
int index) {
final exercise =
tab['exercises'][index];
return Container(
key: ValueKey(exercise),
margin: EdgeInsets.all(5.0),
decoration: BoxDecoration(
color: Colors.black,
borderRadius:
BorderRadius.circular(
10.0),
),
child: Column(
children: [
Container(
height: size.height * .15,
decoration: BoxDecoration(
color: Color.fromARGB(
255, 45, 45, 45),
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,
children: [
Text(
exercise[
'name'],
style: TextStyle(
color: Color.fromARGB(
255,
255,
255,
255),
fontSize:
20,
fontWeight:
FontWeight
.bold),
),
Text(
'Muscles Worked: \n' +
(exercise['musclesworked']
as List)
.join(
', '), // join the list into a string
style: TextStyle(
fontSize:
16,
color: Colors
.white), // 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),
],
),
);
},
))),
),
],
),
),
),
],
),
Footer(tab: 'Workout'),
//if (dropdownVisible)
Positioned(
bottom: 60,
left: 0,
right: 0,
child: Column(children: [
Transform.translate(
offset: Offset(0, 0),
child: Center(
child: Container(
width: size.width * 0.8,
child: Row(
children: [
Spacer(),
Container(
width: 30,
height: 30,
child: ElevatedButton(
onPressed: () => {print('clicked')},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(1000.0),
),
primary: Color.fromARGB(255, 49, 188, 10),
),
child: Align(
alignment: Alignment.center,
child: Text(
'+',
style: TextStyle(
fontSize: 25, color: Colors.white),
),
),
),
),
],
),
),
),
),
])),
],
),
),
),
),
);
}
}
</details>
# 答案1
**得分**: 0
请确保您拥有所需的 "components"。请尝试以下操作:
- 使用 `ReorderableListView.builder` 代替在列表上使用 map。
- 使用每个 ListTile 或 Container 的 key 属性使它们可以重新排序。每个项目的 key 应该是唯一的。
以下是一些示例:
```dart
ReorderableListView.builder(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = tab['exercises'].removeAt(oldIndex);
tab['exercises'].insert(newIndex, item);
});
},
itemCount: tab['exercises'].length,
itemBuilder: (BuildContext context, int index) {
final exercise = tab['exercises'][index];
return Container(
key: ValueKey(exercise),
margin: EdgeInsets.all(5.0),
child: CardListTile(
title: Text(exercise['name']),
subtitle: Text('Muscles Worked: ${exercise['musclesworked'].join(', ')}'),
),
);
},
),
itemBuilder
回调用于使用包装 ListTile
的 Container
构建列表项,每个项目都分配了一个 ValueKey
以进行重新排序。
除了使用 builder,您还应该:
将 ReorderableListView 移出 CustomScrollView,并使用 Expanded 封装 ReorderableListView,以允许其占据父容器中剩余的可用空间。
body: Stack(
children: [
CustomScrollView(
slivers: [
SliverAppBar(
// AppBar 配置
),
SliverToBoxAdapter(
child: SizedBox(
height: size.height - 170,
child: PageView(
// PageView 配置
),
),
),
],
),
Positioned(
top: size.height,
left: 0,
right: 0,
child: Transform.translate(
offset: Offset(0, -120),
child: Center(
child: Container(
// 控制面板配置
),
),
),
),
Footer(tab: 'Workout'),
],
),
希望现在可以正常工作。
英文:
You need to make sure you have the needed "components". Go ahead and try this please:
-
Use
ReorderableListView.builder
instead of using map on the list. -
Use the key property of each ListTile or Container to make them
reorderable. The key should be unique for each item.
Some example below:
ReorderableListView.builder(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = tab['exercises'].removeAt(oldIndex);
tab['exercises'].insert(newIndex, item);
});
},
itemCount: tab['exercises'].length,
itemBuilder: (BuildContext context, int index) {
final exercise = tab['exercises'][index];
return Container(
key: ValueKey(exercise),
margin: EdgeInsets.all(5.0),
child: CardListTile(
title: Text(exercise['name']),
subtitle: Text('Muscles Worked: ${exercise['musclesworked'].join(', ')}'),
),
);
},
),
The itemBuilder
callback is used to build the list items with a Container
wrapping the ListTile
, and each item is assigned a ValueKey
for reordering.
Other than using builder you should:
Move the ReorderableListView outside the CustomScrollView and wrap the ReorderableListView with a Expanded widget to allow it to take up the remaining available space in the parent container
body: Stack(
children: [
CustomScrollView(
slivers: [
SliverAppBar(
// AppBar configs
),
SliverToBoxAdapter(
child: SizedBox(
height: size.height - 170,
child: PageView(
// PageView configs
),
),
),
],
),
Positioned(
top: size.height,
left: 0,
right: 0,
child: Transform.translate(
offset: Offset(0, -120),
child: Center(
child: Container(
// Control panel configs
),
),
),
),
Footer(tab: 'Workout'),
],
),
Hopefully this will work now
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论