Related
I am making a music player using Flutter and 'audio player' and I want to have 'previous' and 'next' button to play previous and next music.
Currently, I have sepearated classes for each music and when I connected them with 'Navigator.push' to show previous and next music easily.
But problem is for example, when I click B music in the music list and, in B music player page, there are previous and next buttons.
Previous button leads to A music page and next button leads to C music page. When I navigate pages with those buttons, route, for example, can be something like music list->B->C->B->A->B.
And then if I click 'back' button on the app bar which was created because of stateful widget itself, it doesn't go back to music list but goes all pages I accessed back one by one like B->A->B->C->B->music list.
I want it to go to music list directly.
So I am wondering whether I need to put all musics in one class not all sepearated classes or I need to changes the 'navigation' function.
But I'm not sure what solution actually works.
Could someone help me with this? Thank you
Below is the code of one music
class Music1 extends StatefulWidget {
const Music1({Key? key}) : super(key: key);
#override
State<Music1> createState() => _Music1State();
}
class _Music1State extends State<Music1> {
//setting the project url
String img_cover_url =
"https://i.pinimg.com/736x/a7/a9/cb/a7a9cbcefc58f5b677d8c480cf4ddc5d.jpg";
late AudioPlayer advancedPlayer;
Duration _duration = new Duration();
Duration _position = new Duration();
bool isPlaying = false;
bool isPaused = false;
bool isLoop = false;
void initPlayer() async {
await advancedPlayer.setSource(AssetSource("forest.mp3"));
advancedPlayer.onDurationChanged.listen((d) {setState(() {
_duration = d;
}); });
advancedPlayer.onPositionChanged.listen((p) {setState(() {
_position = p;
}); });
}
//init the player
#override
void initState() {
// TODO: implement initState
super.initState();
advancedPlayer = AudioPlayer();
initPlayer();
}
#override
void changeToSecond(int second) {
Duration newDuration = Duration(seconds: second);
advancedPlayer.seek(newDuration);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Musics for Kids", style: TextStyle(fontFamily: "Itim",fontSize: 25)),),
body: Stack(
children: [
Container(
constraints: const BoxConstraints.expand(),
height: 300.0,
width: 300.0,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/forest.jpg"),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 28, sigmaY: 28),
child: Container(
color: Colors.black.withOpacity(0.6),
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//setting the music cover
ClipRRect(
borderRadius: BorderRadius.circular(30.0),
child: Image.asset(
"assets/forest.jpg",
width: 250.0,
),
),
const SizedBox(
height: 10.0,
),
const Text(
"Forest",
style: TextStyle(
color: Colors.white, fontSize: 36, letterSpacing: 6, fontFamily: "Itim"),
),
//Setting the seekbar
const SizedBox(
height: 30.0, //50
),
Row( //problem part - previous and next buttons
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute<void>(builder: (BuildContext context) {
return const Music0();
})
);
},
onTap: () => Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context)=> MusicList(),
),
(route) => true,
),
child: Text("Previous", style: TextStyle(color: Colors.white)),
),
SizedBox(width: 200),
GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute<void>(builder: (BuildContext context) {
return const Music2();
})
);
},
child: Text("Next", style: TextStyle(color: Colors.white)),
),
],
),
const SizedBox(
height: 20.0, //50
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_position.toString().split(".")[0],
style: const TextStyle(
color: Colors.white,
),
),
Slider.adaptive(
onChanged: (value){},
min: 0.0,
max: _duration.inSeconds.toDouble(),
value:_position.inSeconds.toDouble(),
onChangeEnd: (double value){
setState((){
changeToSecond(value.toInt());
value = value;
});
advancedPlayer.pause();
advancedPlayer.seek(Duration(seconds: value.toInt()));
advancedPlayer.resume();
},
activeColor: Colors.white,
),
Text(
_duration.toString().split(".")[0],
style: const TextStyle(
color: Colors.white,
),
),
],
),
const SizedBox(
height: 60.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(60.0),
color: Colors.black87,
border: Border.all(color: Colors.white38),
),
width: 50.0,
height: 50.0,
child: InkWell(
onTapDown: (details) {
advancedPlayer.setPlaybackRate(0.5);
},
onTapUp: (details) {
advancedPlayer.setPlaybackRate(1);
},
child: const Center(
child: Icon(
Icons.fast_rewind_rounded,
color: Colors.white,
),
),
),
),
const SizedBox(
width: 60.0,
),
Container(
width: 50.0,
height: 50.0,
decoration:
BoxDecoration(
borderRadius: BorderRadius.circular(60.0),
color: Colors.black87,
border: Border.all(color: Colors.pink),
),
child: InkWell(
onTap: () async{
if (isPlaying) {
await advancedPlayer.pause();
setState((){
isPlaying = false;
});
} else {
await advancedPlayer.resume();
setState((){
isPlaying = true;
});
}
},
child: Icon(
isPlaying ? Icons.pause: Icons.play_arrow,
color: Colors.white,
),
)
),
const SizedBox(
width: 60.0,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(60.0),
color: Colors.black87,
border: Border.all(color: Colors.white38),
),
width: 50.0,
height: 50.0,
child: InkWell(
onTapDown: (details) {
advancedPlayer.setPlaybackRate(2);
},
onTapUp: (details) {
advancedPlayer.setPlaybackRate(1);
},
child: const Center(
child: Icon(
Icons.fast_forward_rounded,
color: Colors.white,
),
),
),
),
],
)
],
),
],
),
);
}
}
EDIT: if you don't want the option I gave below, you could do this, and it will pop the screens until you reach the right screen:
Navigator.popUntil(context, ModalRoute.withName('/example'));
An alternative:
I think I didn't uderstand very well, but do you really need to each music be a different screen? Because you could make only one Music class, and just one screen for the player, and just move between the musics.
You could just setup a list of musics, and functions to move between one from another. Without having to make many different screens for each.
Example of Music Class:
class Music {
String imgCoverUrl;
Duration duration;
String assetSource;
Music({
required this.imgCoverUrl,
required this.duration,
required this.assetSource,
});
}
Example of the screen with functions:
class MusicPlayerScreen extends StatefulWidget {
const MusicPlayerScreen({super.key});
#override
State<MusicPlayerScreen> createState() => _MusicPlayerScreenState();
}
class _MusicPlayerScreenState extends State<MusicPlayerScreen> {
// variable to keep track of which music is playing right now
int selectedMusicIndex = 0;
// list of your musics, so you can move between one and another
List<Music> myMusics = [
Music(imgCoverUrl: 'cover_1.png', duration: Duration(seconds: 10), assetSource: 'myFolder/song1.mp3'),
Music(imgCoverUrl: 'cover_2.png', duration: Duration(seconds: 10), assetSource: 'myFolder/song2.mp3'),
Music(imgCoverUrl: 'cover_2.png', duration: Duration(seconds: 10), assetSource: 'myFolder/song3.mp3'),
];
// function to move to the next music, verifying if have any music after the actual one
void nextMusic() {
if (selectedMusicIndex + 1 < myMusics.length) {
setState(() {
selectMusicIndex++;
});
}
}
// function to move to the previous music, verifying if isn't the first music
void previousMusic() {
if (selectMusic != 0) {
setState(() {
selectMusicIndex--;
});
}
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Then you just use the following the get each music attributes:
myMusics[selectedMusicIndex].imgCoverUrl;
myMusics[selectedMusicIndex].duration;
myMusics[selectedMusicIndex].assetSource;
I made a list of class OriginDestination, i.e. _allCities in my file. I then assigned all values in filteredCities and cities in initState.
Then I made a function runFilter which would take keyword from TextField and filter the results accordingly and save them to resultCities. Then I am using resultCities to display the information in ListView.builder. But the problem is, the list is not filtering according to the keyword i am searching.
Also, it would be appreciated if you can suggest a better way of using parameter cities, i.e. I don't think that passing the cities as parameter through state's constructor is a good practice.
Here is the code -
import 'package:flutter/material.dart';
import 'package:passenger_flutter_app/models/new_city.dart';
import 'package:passenger_flutter_app/models/origin_destination.dart';
import 'package:passenger_flutter_app/utils/colors.dart';
class SelectionScreen extends StatefulWidget {
List<OriginDestination>? cities;
SelectionScreen({this.cities});
#override
_SelectionScreenState createState() => _SelectionScreenState(cities);
}
class _SelectionScreenState extends State<SelectionScreen> {
final List<OriginDestination>? _allCities;
_SelectionScreenState(this._allCities);
bool originSelected=false;
List<OriginDestination>? resultCities = [];
List<OriginDestination>? filteredCities = [];
void getCitiesFromResponse() {
/*for(var city in _allCities!) {
cities!.add(city.origin!);
}*/
filteredCities=_allCities;
resultCities=_allCities;
}
#override
initState() {
// at the beginning, all users are shown
getCitiesFromResponse();
super.initState();
}
void _runFilter(String enteredKeyword) {
if (enteredKeyword.isEmpty) {
// if the search field is empty or only contains white-space, we'll display all users
filteredCities = _allCities;
} else {
filteredCities = _allCities!
.where((city) =>
city.origin!.name!.toLowerCase().contains(enteredKeyword.toLowerCase()))
.toList();
// we use the toLowerCase() method to make it case-insensitive
}
#override
void setState() {
resultCities=filteredCities;
}
}
#override
Widget build(BuildContext context) {
var originSelected;
return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xffEEEDEF),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Column(
children: [
Container(
width: MediaQuery.of(context).size.width*0.8,
),
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
color: Colors.orange,
onPressed: () {
Navigator.pop(context);
},
),
Column(
children: [
originSelected==true ? Container(
child: Text(''),
) :
Container(
width: MediaQuery.of(context).size.width * 0.85,
height: 50.0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
const BorderRadius.all(Radius.circular(5.0)),
border: Border.all(color: colorAccent)),
child: TextField(
decoration: InputDecoration(
hintText: "Enter Origin",
border: InputBorder.none,
contentPadding: const EdgeInsets.only(left: 10.0),
hintStyle: TextStyle(
fontSize: 15.0,
color: Colors.grey[500],
),
),
onChanged: (value) {
_runFilter(value);
},
),
),
],
),
],
),
],
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width * 0.04, top: 3.0),
child: Text(
'Popular Searches:',
style: TextStyle(
color: popUpLightTextColor,
fontSize: MediaQuery.of(context).size.width * 0.035),
),
),
),
Expanded(
child: ListView.builder(
itemCount: resultCities!.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.only(
left: 18.0, top: index==0 ? 29.0 : 15.0, bottom: 15.0),
child: InkWell(
onTap: () {
print(resultCities?[index].origin!.name);
/*setState(() {
widget.city=filteredCities[index]['city'];
print("Changed to - ");
//print(widget.city);
Navigator.pop(context);
});*/
},
child: Text(
resultCities?[index].origin!.name??"No name",
style: const TextStyle(
color: darkText,
fontSize: 15.0,
fontWeight: FontWeight.normal,
),
),
),
);
},
),
),
],
),
),
);
}
}
seems like you defined the setState function instead of calling it, so instead of:
#override
void setState() {
resultCities=filteredCities;
}
write:
setState(() {
resultCities=filteredCities;
});
Why are you overriding the setState. You also pass the call back as argument in the setState.
You should be call setState on a trigger, like a gesture or button:
setState(() => resultCities = filteredCities);
Sorry in advance as the issue might be something very small.
So i have got this file
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../utils/authentication.dart';
import '../utils/stated.dart';
import 'user_info_screen.dart';
class CategoryScreen extends StatefulWidget {
const CategoryScreen({Key? key, required User user, required Stated stated, required int intel})
: _user = user, _stated = stated, _intel = intel,
super(key: key);
final User _user;
final Stated _stated;
final int _intel;
#override
_CategoryScreenState createState() => _CategoryScreenState();
}
class _CategoryScreenState extends State<CategoryScreen> {
late User _user;
late Stated _stated;
late int _intel;
bool loaded = false;
List<Widget> rows = [];
int limit = 2;
var temporal = {};
#override
void initState() {
_user = widget._user;
_stated = widget._stated;
_intel = widget._intel;
super.initState();
}
#override
Widget build(BuildContext context) {
//print(_user.photoURL);
if (!loaded) {
print(rows.toString());
setState(() {
// Rows ==============================================================================================================
FirebaseFirestore.instance.collection('listings').where('category', isEqualTo: _stated.CATEGORIES[_intel]).orderBy('views', descending: true).limit(limit).get().then((value) {
rows.add(
Padding(
padding: EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0, left: 16.0),
child: Text(
_stated.CATEGORIES[_intel],
style: TextStyle(
fontSize: 30,
color: Colors.white,
letterSpacing: 2,
),
),
)
);
value.docs.forEach((element) {
if (element.data()['reviewed'] == false) {
rows.add(
Container(
height: 30.0,
)
);
rows.add(
OutlineButton(
onPressed: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DisplayScreen(
user: _user,
stated: _stated,
intel: element.id,
),
),
);
},
child: Column(
children: <Widget> [
Container(
height: 175.0,
width: 200,
child: Image.network(element.data()['image']['0']),
),
Container(
height: 75.0,
width: 200,
child: Padding(
padding: EdgeInsets.only(top: 10.0, bottom: 10.0, right: 16.0, left: 16.0),
child: Center(
child: Text(
element.data()['title'],
style: TextStyle(
fontSize: 15,
color: Colors.white,
letterSpacing: 2,
),
),
),
),
),
],
)
)
);
}
});
}).then((e) {
loaded = true;
print(rows.toString());
if (rows.length == limit) {
rows.add(
Container(
height: 30.0,
)
);
rows.add(
OutlineButton(
onPressed: () async {
},
child: Column(
children: <Widget> [
Container(
height: 250.0,
width: 200,
child: Padding(
padding: EdgeInsets.only(top: 10.0, bottom: 10.0, right: 16.0, left: 16.0),
child: Center(
child: Text(
'Load More',
style: TextStyle(
fontSize: 15,
color: Colors.white,
letterSpacing: 2,
),
),
),
),
),
],
)
)
);
}
});
});
// End of setState =============================================================
}
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.black,
actions: <Widget>[
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.black),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
),
onPressed: () async {
User? user =
await Authentication.signInWithGoogle(context: context);
if (user != null) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => UserInfoScreen(
user: user,
),
),
);
}
},
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
child: _user.photoURL != null
? ClipOval(
child: Material(
color: Colors.black.withOpacity(0.3),
child: Image.network(
(_user.photoURL!).replaceAll("=s96-c", "=s1000-c"),
height: 50.0,
),
),
)
: ClipOval(
child: Material(
color: Colors.black.withOpacity(0.3),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Icon(
Icons.person,
size: 50,
color: Colors.grey,
),
),
),
),
),
),
],
),
body: Center(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.all(10.0),
children: rows,
),
)
);
}
}
The page loads fine without errors, however the concept of the page is that is the "Category" page for my app, the idea is that once rerouted to this page i create a listview to not leave the screen blank, and run a Firebase call to load the items, currently is set to a limit of 2 items(for testing), and i create card like containers that hold the data of the items.
Issue is that all the items in the list are stored in 'rows', and this variable is updated as i check by printing it on the console, however the page on the screen does not update.
I do believe its a common or basic thing that i am forgetting/missing because there are no errors, or probably how i am setting the list, but every time i search for this issue online no body has had this issue.
How do i make the list on screen show?
PS: as android studio is, if I press CTRL-S to save the code it also hot-reloads the simulator, and for a split second in the simulator the list shows up.
There is a lot of business logic inside the build method that blocks your UI rendering until you get the result from Firestore - this is bad and definitely not a way to go.
You should start by learning more about FutureBuilder or any state management solution on retrieving data and building the UI based on state changes. I would recommend starting from this Codelab (or any other Firestore + Flutter tutorial) just to get the idea of how to work with Firestore in Flutter: https://firebase.google.com/codelabs/firebase-get-to-know-flutter
Currently I'm using flutter package 'Reorderables' to show a reorderable listview which contains several images.These images can be deleted from listview through a button , everything works fine. But the listview rebuild everytime when I delete an image. I'm using a class called 'ReorderableListviewManager' with ChangeNotifier to update images and Provider.of<ReorderableListviewManager>(context) to get latest images . The problem now is that using Provider.of<ReorderableListviewManager>(context) makes build() called everytime I delete an image , so the listview rebuid. I koow I
can use consumer to only rebuild part of widget tree, but it seems like that there's no place to put consumer in children of this Listview. Is there a way to rebuild only image but not whole ReorderableListview ? Thanks very much!
Below is my code:
class NotePicturesEditScreen extends StatefulWidget {
final List<Page> notePictures;
final NotePicturesEditBloc bloc;
NotePicturesEditScreen({#required this.notePictures, #required this.bloc});
static Widget create(BuildContext context, List<Page> notePictures) {
return Provider<NotePicturesEditBloc>(
create: (context) => NotePicturesEditBloc(),
child: Consumer<NotePicturesEditBloc>(
builder: (context, bloc, _) =>
ChangeNotifierProvider<ReorderableListviewManager>(
create: (context) => ReorderableListviewManager(),
child: NotePicturesEditScreen(
bloc: bloc,
notePictures: notePictures,
),
)),
dispose: (context, bloc) => bloc.dispose(),
);
}
#override
_NotePicturesEditScreenState createState() => _NotePicturesEditScreenState();
}
class _NotePicturesEditScreenState extends State<NotePicturesEditScreen> {
PreloadPageController _pageController;
ScrollController _reorderableScrollController;
List<Page> notePicturesCopy;
int longPressIndex;
List<double> smallImagesWidth;
double scrollOffset = 0;
_reorderableScrollListener() {
scrollOffset = _reorderableScrollController.offset;
}
#override
void initState() {
Provider.of<ReorderableListviewManager>(context, listen: false)
.notePictures = widget.notePictures;
notePicturesCopy = widget.notePictures;
_reorderableScrollController = ScrollController();
_pageController = PreloadPageController();
_reorderableScrollController.addListener(_reorderableScrollListener);
Provider.of<ReorderableListviewManager>(context, listen: false)
.getSmallImagesWidth(notePicturesCopy, context)
.then((imagesWidth) {
smallImagesWidth = imagesWidth;
});
super.initState();
}
#override
void dispose() {
_pageController.dispose();
_reorderableScrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
ReorderableListviewManager reorderableManager =
Provider.of<ReorderableListviewManager>(context, listen: false);
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
shape: Border(bottom: BorderSide(color: Colors.black12)),
iconTheme: IconThemeData(color: Colors.black87),
elevation: 0,
automaticallyImplyLeading: false,
titleSpacing: 0,
centerTitle: true,
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
child: IconButton(
padding: EdgeInsets.only(left: 20, right: 12),
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.close),
),
),
Text('編輯',
style: TextStyle(color: Colors.black87, fontSize: 18))
],
),
actions: <Widget>[
FlatButton(
onPressed: () {},
child: Text(
'下一步',
),
)
],
),
backgroundColor: Color(0xffeeeeee),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Spacer(),
StreamBuilder<List<Page>>(
initialData: widget.notePictures,
stream: widget.bloc.notePicturesStream,
builder: (context, snapshot) {
notePicturesCopy = snapshot.data;
return Container(
margin: EdgeInsets.symmetric(horizontal: 20),
height: MediaQuery.of(context).size.height * 0.65,
child: PreloadPageView.builder(
preloadPagesCount: snapshot.data.length,
controller: _pageController,
itemCount: snapshot.data.length,
onPageChanged: (index) {
reorderableManager.updateCurrentIndex(index);
reorderableManager.scrollToCenter(
smallImagesWidth,
index,
scrollOffset,
_reorderableScrollController,
context);
},
itemBuilder: (context, index) {
return Container(
child: Image.memory(
File.fromUri(
snapshot.data[index].polygon.isNotEmpty
? snapshot.data[index]
.documentPreviewImageFileUri
: snapshot.data[index]
.originalPreviewImageFileUri)
.readAsBytesSync(),
gaplessPlayback: true,
alignment: Alignment.center,
),
);
}),
);
},
),
Spacer(),
Container(
height: MediaQuery.of(context).size.height * 0.1,
margin: EdgeInsets.symmetric(horizontal: 20),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ReorderableRow(
scrollController: _reorderableScrollController,
buildDraggableFeedback: (context, constraints, __) =>
Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: Image.memory(File.fromUri(
notePicturesCopy[longPressIndex]
.polygon
.isNotEmpty
? notePicturesCopy[longPressIndex]
.documentPreviewImageFileUri
: notePicturesCopy[longPressIndex]
.originalPreviewImageFileUri)
.readAsBytesSync()),
),
onReorder: (oldIndex, newIndex) async {
List<Page> result = await widget.bloc.reorderPictures(
oldIndex,
newIndex,
reorderableManager.notePictures);
_pageController.jumpToPage(newIndex);
reorderableManager.updateNotePictures(result);
reorderableManager
.getSmallImagesWidth(result, context)
.then((imagesWidth) {
smallImagesWidth = imagesWidth;
});
},
footer: Container(
width: 32,
height: 32,
margin: EdgeInsets.only(left: 16),
child: SizedBox(
child: FloatingActionButton(
backgroundColor: Colors.white,
elevation: 1,
disabledElevation: 0,
highlightElevation: 1,
child: Icon(Icons.add, color: Colors.blueAccent),
onPressed: notePicturesCopy.length >= 20
? () {
Scaffold.of(context)
.showSnackBar(SnackBar(
content: Text('筆記上限為20頁 !'),
));
}
: () async {
List<Page> notePictures =
await widget.bloc.addPicture(
reorderableManager.notePictures);
List<double> imagesWidth =
await reorderableManager
.getSmallImagesWidth(
notePictures, context);
smallImagesWidth = imagesWidth;
reorderableManager.updateCurrentIndex(
notePictures.length - 1);
reorderableManager
.updateNotePictures(notePictures);
_pageController
.jumpToPage(notePictures.length - 1);
},
),
),
),
children: Provider.of<ReorderableListviewManager>(
context)
.notePictures
.asMap()
.map((index, page) {
return MapEntry(
index,
Consumer<ReorderableListviewManager>(
key: ValueKey('value$index'),
builder: (context, manager, _) =>
GestureDetector(
onTapDown: (_) {
longPressIndex = index;
},
onTap: () {
reorderableManager.scrollToCenter(
smallImagesWidth,
index,
scrollOffset,
_reorderableScrollController,
context);
_pageController.jumpToPage(index);
},
child: Container(
margin: EdgeInsets.only(
left: index == 0 ? 0 : 12),
decoration: BoxDecoration(
border: Border.all(
width: 1.5,
color: index ==
manager
.getCurrentIndex
? Colors.blueAccent
: Colors.transparent)),
child: index + 1 <=
manager.notePictures.length
? Image.memory(
File.fromUri(manager
.notePictures[
index]
.polygon
.isNotEmpty
? manager
.notePictures[
index]
.documentPreviewImageFileUri
: manager
.notePictures[
index]
.originalPreviewImageFileUri)
.readAsBytesSync(),
gaplessPlayback: true,
)
: null),
),
));
})
.values
.toList()),
)),
Spacer(),
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black12))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
onPressed: () async => await widget.bloc
.cropNotePicture(reorderableManager.notePictures,
_pageController.page.round())
.then((notePictures) {
reorderableManager.updateNotePictures(notePictures);
reorderableManager
.getSmallImagesWidth(notePictures, context)
.then((imagesWidth) {
smallImagesWidth = imagesWidth;
});
}),
child: Column(
children: <Widget>[
Icon(
Icons.crop,
color: Colors.blueAccent,
),
Container(
margin: EdgeInsets.only(top: 1),
child: Text(
'裁切',
style: TextStyle(color: Colors.blueAccent),
),
)
],
),
),
FlatButton(
onPressed: () {
int deleteIndex = _pageController.page.round();
widget.bloc
.deletePicture(
reorderableManager.notePictures, deleteIndex)
.then((notePictures) {
if (deleteIndex == notePictures.length) {
reorderableManager
.updateCurrentIndex(notePictures.length - 1);
}
reorderableManager.updateNotePictures(notePictures);
reorderableManager
.getSmallImagesWidth(notePictures, context)
.then((imagesWidth) {
smallImagesWidth = imagesWidth;
});
if (reorderableManager.notePictures.length == 0) {
Navigator.pop(context);
}
});
},
child: Column(
children: <Widget>[
Icon(
Icons.delete_outline,
color: Colors.blueAccent,
),
Container(
margin: EdgeInsets.only(top: 1),
child: Text(
'刪除',
style: TextStyle(color: Colors.blueAccent),
),
),
],
),
)
],
),
)
],
)),
);
}
}
You can't prevent a rebuild on your ReorderableListView widget because it will be rebuild every time there's an update on the Provider. What you can do here is to keep track the current index of all visible ListView items. When new data should be displayed coming from the Provider, you can retain the current indices of previous ListView items, and add the newly added items at the end of the list, or wherever you like.
Somedays ago I decided to choose an Ui for an app from Pinterest to practice building apps with Flutter but I'm stuck with the Slider which shows an "more" and "delete" button on horizontal drag. Picture on the right.
I don't have enough knowledge to use Gestures combined with Animations to create something like this in flutter. Thats why I hope that someone of you can make an example for everyone like me that we can understand how to implement something like this in a ListView.builder.
(Source)
An gif example from the macOS mail App:
I created a package for doing this kind of layout: flutter_slidable (Thanks Rémi Rousselet for the based idea)
With this package it's easier to create contextual actions for a list item. For example if you want to create the kind of animation you described:
You will use this code:
new Slidable(
delegate: new SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
child: new Container(
color: Colors.white,
child: new ListTile(
leading: new CircleAvatar(
backgroundColor: Colors.indigoAccent,
child: new Text('$3'),
foregroundColor: Colors.white,
),
title: new Text('Tile n°$3'),
subtitle: new Text('SlidableDrawerDelegate'),
),
),
actions: <Widget>[
new IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
new IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
new IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
new IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
There's already a widget for this kind of gesture. It's called Dismissible.
You can find it here. https://docs.flutter.io/flutter/widgets/Dismissible-class.html
EDIT
If you need the exact same transtion, you'd probably have to implement if yourself.
I made a basic example. You'd probably want to tweak the animation a bit, but it's working at least.
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
double rating = 3.5;
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView(
children: ListTile
.divideTiles(
context: context,
tiles: new List.generate(42, (index) {
return new SlideMenu(
child: new ListTile(
title: new Container(child: new Text("Drag me")),
),
menuItems: <Widget>[
new Container(
child: new IconButton(
icon: new Icon(Icons.delete),
),
),
new Container(
child: new IconButton(
icon: new Icon(Icons.info),
),
),
],
);
}),
)
.toList(),
),
);
}
}
class SlideMenu extends StatefulWidget {
final Widget child;
final List<Widget> menuItems;
SlideMenu({this.child, this.menuItems});
#override
_SlideMenuState createState() => new _SlideMenuState();
}
class _SlideMenuState extends State<SlideMenu> with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
initState() {
super.initState();
_controller = new AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final animation = new Tween(
begin: const Offset(0.0, 0.0),
end: const Offset(-0.2, 0.0)
).animate(new CurveTween(curve: Curves.decelerate).animate(_controller));
return new GestureDetector(
onHorizontalDragUpdate: (data) {
// we can access context.size here
setState(() {
_controller.value -= data.primaryDelta / context.size.width;
});
},
onHorizontalDragEnd: (data) {
if (data.primaryVelocity > 2500)
_controller.animateTo(.0); //close menu on fast swipe in the right direction
else if (_controller.value >= .5 || data.primaryVelocity < -2500) // fully open if dragged a lot to left or on fast swipe to left
_controller.animateTo(1.0);
else // close if none of above
_controller.animateTo(.0);
},
child: new Stack(
children: <Widget>[
new SlideTransition(position: animation, child: widget.child),
new Positioned.fill(
child: new LayoutBuilder(
builder: (context, constraint) {
return new AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return new Stack(
children: <Widget>[
new Positioned(
right: .0,
top: .0,
bottom: .0,
width: constraint.maxWidth * animation.value.dx * -1,
child: new Container(
color: Colors.black26,
child: new Row(
children: widget.menuItems.map((child) {
return new Expanded(
child: child,
);
}).toList(),
),
),
),
],
);
},
);
},
),
)
],
),
);
}
}
EDIT
Flutter no longer allows type Animation<FractionalOffset> in SlideTransition animation property. According to this post https://groups.google.com/forum/#!topic/flutter-dev/fmr-C9xK5t4 it should be replaced with AlignmentTween but this also doesn't work. Instead, according to this issue: https://github.com/flutter/flutter/issues/13812 replacing it instead with a raw Tween and directly creating Offset object works instead. Unfortunately, the code is much less clear.
Updated Code with Null Safety: Flutter: 2.x
Firstly you need to add the flutter_slidable package in your project and add below code then Let's enjoy...
Slidable(
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.25,
child: Container(
color: Colors.white,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.indigoAccent,
child: Text('$3'),
foregroundColor: Colors.white,
),
title: Text('Tile n°$3'),
subtitle: Text('SlidableDrawerDelegate'),
),
),
actions: <Widget>[
IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
I have a task that needs the same swipeable menu actions I tried answeres of Romain Rastel and Rémi Rousselet. but I have complex widget tree. the issue with that slideable solutions is they go on other widgets(to left widgets of listview). I found a batter solution here someone wrote a nice article medium and GitHub sample is here.
I look at a lot of articles and answers, and find #Rémi Rousselet answer the best fitted to use without third party libraries.
Just put some improvements to #Rémi's code to make it usable in modern SDK without errors and null safety.
Also I smooth a little bit movement, to make the speed of buttons appeared the same as finger movement.
And I put some comments into the code:
import 'package:flutter/material.dart';
class SlidebleList extends StatefulWidget {
const SlidebleList({Key? key}) : super(key: key);
#override
State<SlidebleList> createState() => _SlidebleListState();
}
class _SlidebleListState extends State<SlidebleList> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: ListTile.divideTiles(
context: context,
tiles: List.generate(42, (index) {
return SlideMenu(
menuItems: <Widget>[
Container(
color: Colors.black12,
child: IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {},
),
),
Container(
color: Colors.red,
child: IconButton(
color: Colors.white,
icon: const Icon(Icons.delete),
onPressed: () {},
),
),
],
child: const ListTile(
title: Text("Just drag me"),
),
);
}),
).toList(),
),
);
}
}
class SlideMenu extends StatefulWidget {
final Widget child;
final List<Widget> menuItems;
const SlideMenu({Key? key,
required this.child, required this.menuItems
}) : super(key: key);
#override
State<SlideMenu> createState() => _SlideMenuState();
}
class _SlideMenuState extends State<SlideMenu> with SingleTickerProviderStateMixin {
late AnimationController _controller;
#override
initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
//Here the end field will determine the size of buttons which will appear after sliding
//If you need to appear them at the beginning, you need to change to "+" Offset coordinates (0.2, 0.0)
final animation =
Tween(begin: const Offset(0.0, 0.0),
end: const Offset(-0.2, 0.0))
.animate(CurveTween(curve: Curves.decelerate).animate(_controller));
return GestureDetector(
onHorizontalDragUpdate: (data) {
// we can access context.size here
setState(() {
//Here we set value of Animation controller depending on our finger move in horizontal axis
//If you want to slide to the right, change "-" to "+"
_controller.value -= (data.primaryDelta! / (context.size!.width*0.2));
});
},
onHorizontalDragEnd: (data) {
//To change slide direction, change to data.primaryVelocity! < -1500
if (data.primaryVelocity! > 1500)
_controller.animateTo(.0); //close menu on fast swipe in the right direction
//To change slide direction, change to data.primaryVelocity! > 1500
else if (_controller.value >= .5 || data.primaryVelocity! < -1500)
_controller.animateTo(1.0); // fully open if dragged a lot to left or on fast swipe to left
else // close if none of above
_controller.animateTo(.0);
},
child: LayoutBuilder(builder: (context, constraint) {
return Stack(
children: [
SlideTransition(
position: animation,
child: widget.child,
),
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
//To change slide direction to right, replace the right parameter with left:
return Positioned(
right: .0,
top: .0,
bottom: .0,
width: constraint.maxWidth * animation.value.dx * -1,
child: Row(
children: widget.menuItems.map((child) {
return Expanded(
child: child,
);
}).toList(),
),
);
})
],
);
})
);
}
}
i had the same problem and and as the accepted answer suggests, i used flutter_slidable
but i needed a custom look for the actions and also i wanted them to be vertically aligned not horizontal.
i noticed that actionPane() can take a list of widgets as children not only
SlidableAction.
so i was able to make my custom actions,and wanted to share the code and results with you here.
this is the layout
this is the code i used :
ListView.builder(
itemBuilder: (context, index) {
return Slidable(
startActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.25,
// A pane can dismiss the Slidable.
// All actions are defined in the children parameter.
children: [
Expanded(
flex: 1,
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
Expanded(
child: InkWell(
child: Container(
width: double.infinity,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.edit,
color:
Colors.deepPurple),
Text(
LocalizationKeys.edit.tr,
style: TextStyle(
color:
Colors.deepPurple,
fontSize: 16),
),
],
),
),
onTap: () {},
),
),
Container(
height: 1,
color: Colors.deepPurple,
),
Expanded(
child: InkWell(
child: Container(
width: double.infinity,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.delete,
color: Colors.red),
Text(
LocalizationKeys
.app_delete.tr,
style: TextStyle(
color: Colors.red,
fontSize: 16),
),
],
),
),
onTap: () {},
),
),
],
),
),
),
]),
child: Card(
margin: EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 16),
Text(_lecturesViewModel
.lectures.value[index].centerName),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_lecturesViewModel
.lectures.value[index].classLevel),
Text(_lecturesViewModel
.lectures.value[index].material),
],
),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.location_pin),
Text(_lecturesViewModel
.lectures.value[index].city),
Text(_lecturesViewModel
.lectures.value[index].area),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Icon(Icons.calendar_today),
Text(_lecturesViewModel
.lectures.value[index].day),
],
),
Container(
height: 1,
width: 60,
color: Colors.black,
),
Column(
children: [
Icon(Icons.punch_clock),
Text(_lecturesViewModel
.lectures.value[index].time),
],
),
Container(
height: 1,
width: 60,
color: Colors.black,
),
Column(
children: [
Icon(Icons.money),
Text(
"${_lecturesViewModel.lectures.value[index].price.toString()}ج "),
],
)
]),
SizedBox(height: 16),
]),
),
);
},
itemCount: _lecturesViewModel.lectures.length,
physics: BouncingScrollPhysics(),
)