使列表可重新排序

huangapple go评论60阅读模式
英文:

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 回调用于使用包装 ListTileContainer 构建列表项,每个项目都分配了一个 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 &gt; oldIndex) {
newIndex -= 1;
}
final item = tab[&#39;exercises&#39;].removeAt(oldIndex);
tab[&#39;exercises&#39;].insert(newIndex, item);
});
},
itemCount: tab[&#39;exercises&#39;].length,
itemBuilder: (BuildContext context, int index) {
final exercise = tab[&#39;exercises&#39;][index];
return Container(
key: ValueKey(exercise),
margin: EdgeInsets.all(5.0),
child: CardListTile(
title: Text(exercise[&#39;name&#39;]),
subtitle: Text(&#39;Muscles Worked: ${exercise[&#39;musclesworked&#39;].join(&#39;, &#39;)}&#39;),
),
);
},
),

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: &#39;Workout&#39;),
],
),

Hopefully this will work now

huangapple
  • 本文由 发表于 2023年5月28日 08:21:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/76349516.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定