I have a Map<String, dynamic>, where dynamic is a dynamic tree of Strings. I need to show in a ListView (or another Widget that allow to show a Tree structure) the Strings and their children. Each String is a collapsable item (if has children) of the ListView. For example:
Text
text
text
text
text
text
text
text
text
this is the code of an example of the tree:
void main() {
Map<String, dynamic> joinTrees(int level, int maxLevel,
List<List<String>> trees, Map<String, dynamic> tmpMap) {
if (maxLevel < 0) return {};
List<String> nodes = trees.map((tree) => tree[level]).toSet().toList();
print(nodes);
for (String node in nodes) {
List<List<String>> childrenBranchs =
trees.where((tree) => tree[level] == node).toList();
if (childrenBranchs.length == 1 &&
childrenBranchs[0][childrenBranchs[0].length - 1] == node) {
print("leaf: " + node);
tmpMap[node] = null;
} else {
Map<String, dynamic> childrenTree = joinTrees(
level + 1, maxLevel, childrenBranchs, new Map<String, dynamic>());
print("node: " + node);
tmpMap[node] = childrenTree;
}
}
return tmpMap;
}
List<List<String>> trees = [
["A", "A11", "A21"],
["A", "A12"],
["A", "A11", "A22"],
["B", "B11", "B21"],
["C"]
];
int maxLength = trees
.map((tree) => tree.length)
.toList()
.reduce((curr, next) => curr > next ? curr : next);
int maxLevel = maxLength - 1;
print(trees);
print(maxLength);
Map<String, dynamic> joinedTree =
joinTrees(0, maxLevel, trees, new Map<String, dynamic>());
print(joinedTree);
}
this is de ouput of joinedTree:
{
A:
{
A11:
{
A21: null,
A22: null
},
A12: null
},
B:
{
B11:
{
B21: null
}
},
C: null
}
if your tree look like this, detail Entry class please reference full code
final List<Entry> data = <Entry>[
Entry(
'A',
<Entry>[
Entry(
'A11',
<Entry>[
Entry('A21'),
Entry('A22'),
],
),
Entry('A12'),
],
),
Entry(
'B',
<Entry>[
Entry(
'B11',
<Entry>[
Entry('B21'),
],
),
],
),
Entry(
'C',
),
];
inside ListView.builder, build item with
Widget _buildTiles(Entry root) {
if (root.children.isEmpty) {
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: ListTile(title: Text(root.title)),
);
}
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: ExpansionTile(
key: PageStorageKey<Entry>(root),
title: Text(root.title),
children: root.children.map(_buildTiles).toList(),
),
);
}
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: ExpansionTileSample(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class ExpansionTileSample extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('ExpansionTile'),
),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) =>
EntryItem(data[index]),
itemCount: data.length,
),
),
);
}
}
// One entry in the multilevel list displayed by this app.
class Entry {
Entry(this.title, [this.children = const <Entry>[]]);
final String title;
final List<Entry> children;
}
// The entire multilevel list displayed by this app.
final List<Entry> data = <Entry>[
Entry(
'A',
<Entry>[
Entry(
'A11',
<Entry>[
Entry('A21'),
Entry('A22'),
],
),
Entry('A12'),
],
),
Entry(
'B',
<Entry>[
Entry(
'B11',
<Entry>[
Entry('B21'),
],
),
],
),
Entry(
'C',
),
];
// Displays one Entry. If the entry has children then it's displayed
// with an ExpansionTile.
class EntryItem extends StatelessWidget {
const EntryItem(this.entry);
final Entry entry;
Widget _buildTiles(Entry root) {
if (root.children.isEmpty) {
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: ListTile(title: Text(root.title)),
);
}
return Padding(
padding: const EdgeInsets.only(left: 10.0),
child: ExpansionTile(
key: PageStorageKey<Entry>(root),
title: Text(root.title),
children: root.children.map(_buildTiles).toList(),
),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(entry);
}
}
Related
I am quite new to app development and I had a few questions about Flutter I was hoping someone could help me with!
First, I am trying to code an intro-slide part in my code. I have found this code online (https://flutterawesome.com/simple-and-configurable-app-introduction-slider-for-flutter/) and when I tried executing it, using my own images, only the background color seems to print. When I remove the background colors, it is just a white screen. Is my pageImage part correct? I saved an assert folder everywhere, so I'm unsure if that is the problem. I have included my code at the end.
Thank you for your time!
class _MyHomePageState extends State<MyHomePage> {
List<Slide> slides = new List();
#override
void initState() {
super.initState();
slides.add(
new Slide(
title: "ERASER",
description: "Allow miles wound place the leave had. To sitting subject no improve studied limited",
pathImage: "assets/images/1.png",
backgroundColor: Colors.pink[200],
),
);
slides.add(
new Slide(
title: "PENCIL",
description: "Ye indulgence unreserved connection alteration appearance",
pathImage: "assets/images/1.png",
backgroundColor: Colors.blue[200],
),
);
slides.add(
new Slide(
title: "RULER",
description:
"Much evil soon high in hope do view. Out may few northward believing attempted. Yet timed being songs marry one defer men our. Although finished blessing do of",
pathImage: "assets/images/3.jpg",
),
);
}
void onDonePress() {
// TODO: go to next screen
}
void onSkipPress() {
// TODO: go to next screen
}
#override
Widget build(BuildContext context) {
return new IntroSlider(
slides: this.slides,
onDonePress: this.onDonePress,
onSkipPress: this.onSkipPress,
);
}
}
**Solution: edit assets in pubspec page
Edit:
On the left (the orange part) is how I want the blue image to appear: No scrolling and fills the whole page. However, I tried to make my image (on the right) fill the page by editing the width and height and I started having to scroll where there is the pink background below and above the image (I assume it is because it keeps having to center the image).
Is there any way to make my image my background so it is like the picture on the left? I understand the orange color background is the background color, but hopefully, by comparing the two it makes sense. Thank you!
I create new intro widget. Here is the code.
import 'package:flutter/material.dart';
class MyIntroView extends StatefulWidget {
final List<Widget> pages;
final VoidCallback onIntroCompleted;
const MyIntroView({
Key key,
#required this.pages,
#required this.onIntroCompleted,
}) : assert(pages != null),
assert(onIntroCompleted != null),
super(key: key);
#override
_MyIntroViewState createState() => _MyIntroViewState();
}
class _MyIntroViewState extends State<MyIntroView> {
PageController _pageController;
int _currentPage = 0;
#override
void initState() {
_pageController = PageController(
initialPage: _currentPage,
);
super.initState();
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
NotificationListener<ScrollEndNotification>(
onNotification: (x) {
setState(() {
_currentPage = _pageController.page.round();
});
return false;
},
child: PageView(
children: widget.pages,
controller: _pageController,
),
),
Align(
alignment: Alignment.bottomCenter,
child: _buildBottomButtons(),
),
],
);
}
bool get _isFinalPage => _currentPage == widget.pages.length - 1;
Widget _buildBottomButtons() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Opacity(
opacity: _isFinalPage ? 0.0 : 1.0,
child: _buildButton("SKIP", _gotoLastPage),
),
_buildNavIndicator(),
_isFinalPage
? _buildButton("DONE", widget.onIntroCompleted)
: _buildButton("NEXT", _gotoNextPage),
],
),
);
}
Widget _buildButton(String title, VoidCallback callback) {
return FlatButton(
child: Text(
title,
style: TextStyle(color: Colors.white),
),
onPressed: callback,
);
}
void _gotoLastPage() {
_pageController.animateToPage(
widget.pages.length - 1,
duration: const Duration(milliseconds: 600),
curve: Curves.ease,
);
}
void _gotoNextPage() {
_pageController.nextPage(
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
Widget _buildNavIndicator() {
final indicatorList = <Widget>[];
for (int i = 0; i < widget.pages.length; i++)
indicatorList.add(_buildIndicator(i == _currentPage));
return Row(children: indicatorList);
}
Widget _buildIndicator(bool isActive) {
return Padding(
padding: const EdgeInsets.all(5.0),
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? Colors.white : Colors.white30,
),
child: SizedBox(width: 8, height: 8),
),
);
}
}
Usage:
import 'package:flutter/material.dart';
import 'package:flutter_app_test3/my_intro_view.dart';
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MyIntroView(
pages: <Widget>[
Image.asset("assets/images/1.png", fit: BoxFit.cover),
Image.asset("assets/images/2.png", fit: BoxFit.cover),
Image.asset("assets/images/3.jpg", fit: BoxFit.cover),
],
onIntroCompleted: () {
print("Into is Completed");
//To the navigation stuff here
},
);
}
}
Ask me if you have any doubts in the comment
just try wrapping your Widget into Scaffold Widget and return
#override
Widget build(BuildContext context) {
return Scaffold(body:IntroSlider(
slides: this.slides,
onDonePress: this.onDonePress,
onSkipPress: this.onSkipPress,
));
}
I was facing the same issue and I fixed it by setting fit:Boxfit.fill for the image.
i have a screen that build using MaterialApp, DefaultTabController, Scaffold and TabBarView.
in this screen, i have body content that retreive a list of element from sqllite using StreamBuilder. i get exact 100 elements ("finite list") to be shown using ListView.
my question, using ListView.builder, How we can jump to certain index when this screen opened ?
my main screen:
...
ScrollController controller = ScrollController();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner : false,
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
backgroundColor: Pigment.fromString(UIData.primaryColor),
elevation: 0,
centerTitle: true,
title: Text(translations.text("quran").toUpperCase()),
bottom: TabBar(
tabs: [
Text("Tab1"),
Text("Tab2"),
Text("Tab3")
],
),
leading: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: InkWell(
child: SizedBox(child: Image.asset("assets/images/home.png"), height: 10, width: 1,),
onTap: () => Navigator.of(context).pop(),
)
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Testing Index Jump',
child: Text("GO"),
),
body:
TabBarView(
children: [
Stack(
children: <Widget>[
MyDraggableScrollBar.create(
scrollController: controller,
context: context,
heightScrollThumb: 25,
child: ListView(
controller: controller,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(30, 15, 30, 8),
child: Container(
alignment: Alignment.center,
height: 30,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: TextField(
style: TextStyle(color: Colors.green),
decoration: new InputDecoration(
contentPadding: EdgeInsets.all(5),
border: InputBorder.none,
filled: true,
hintStyle: new TextStyle(color: Colors.green, fontSize: 14),
prefixIcon: Icon(FontAwesomeIcons.search,color: Colors.green,size: 17,),
hintText: translations.text("search-quran"),
fillColor: Colors.grey[300],
prefixStyle: TextStyle(color: Colors.green)
),
onChanged: (val) => quranBloc.searchSurah(val),
),
)
)
),
//surah list
streamBuilderQuranSurah(context)
],
)
) // MyDraggableScrollBar
],
),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
)
)));
}
Widget streamBuilderQuranSurah(BuildContext ctx){
return StreamBuilder(
stream: quranBloc.chapterStream ,
builder: (BuildContext context, AsyncSnapshot<ChaptersModel> snapshot){
if(snapshot.hasData){
return ListView.builder(
controller: controller,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount:(snapshot.data.chapters?.length ?? 0),
itemBuilder: (BuildContext context, int index) {
var chapter =
snapshot.data.chapters?.elementAt(index);
return chapterDataCell(chapter);
},
);
}
else{
return SurahItemShimmer();
}
},
);
}
...
class MyDraggableScrollBar.dart :
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
class MyDraggableScrollBar {
static Widget create({
#required BuildContext context,
#required ScrollController scrollController,
#required double heightScrollThumb,
#required Widget child,
}) {
return DraggableScrollbar(
alwaysVisibleScrollThumb: true,
scrollbarTimeToFade: Duration(seconds: 3),
controller: scrollController,
heightScrollThumb: heightScrollThumb,
backgroundColor: Colors.green,
scrollThumbBuilder: (
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text labelText,
BoxConstraints labelConstraints,
}) {
return InkWell(
onTap: () {},
child: Container(
height: height,
width: 7,
color: backgroundColor,
),
);
},
child: child,
);
}
}
i have tried find other solutions but seems not working, for example indexed_list_view that only support infinite list
and it seems flutter still not have feature for this, see this issue
Any Idea ?
You can use https://pub.dev/packages/scrollable_positioned_list. You can pass the initial index to the widget.
ScrollablePositionedList.builder(
initialScrollIndex: 12, //you can pass the desired index here//
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
General Solution:
To store anything which can be represented as a number/string/list of strings, Flutter provides a powerful easy-to-use plugin which stores the values needed to be stored along with a key. So the next time you need you'll need to retrieve or even update that value all that you'll need is that key.
To get started, add the shared_preferences plugin to the pubspec.yaml file,
dependencies:
flutter:
sdk: flutter
shared_preferences: "<newest version>"
Run flutter pub get from the terminal or if your using IntelliJ just click on Packages get(You'll find it somewhere around the top-right corner of your screen while viewing the pubspec.yaml file)
Once the above command is successfully executed, import the below file in your main.dart or concerned file.
import 'package:shared_preferences/shared_preferences.dart';
Now just attach a ScrollController to your ListView.builder() widget and make sure that the final/last offset is stored along with a specific key using shared_preferences whenever the user leaves the app in any way and is set when the initState of your concerned widget is called.
In order to know to detect changes in the state of our app and to act with accordance to it, we'll be inheriting WidgetsBindingObserver to our class.
Steps to follow:
Extend the WidgetsBindingObserver class along with the State class of your StatefulWidget.
Define a async function resumeController() as a function member of the above class.
Future<void> resumeController() async{
_sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
else _sharedPreferences.setDouble("scroll-offset-0", 0);
setState((){});
return _sharedPreferences;
});
Declare two variables one to store and pass the scrollcontroller and the other to store and use the instance of SharedPreferences.
ScrollController _scrollController;
SharedPreferences _sharedPreferences;
Call resumeController() and pass your class to the addObserver method of the instance object in WidgetsBinding class.
resumeController();
WidgetsBinding.instance.addObserver(this);
Simply paste this code in the class definition (outside other member functions)
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
_sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
super.didChangeAppLifecycleState(state);
}
Pass the ScrollController() to the concerned Scrollable.
Working Example:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver{
//[...]
ScrollController _scrollController;
SharedPreferences _sharedPreferences;
Future<void> resumeController() async{
_sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
else _sharedPreferences.setDouble("scroll-offset-0", 0);
setState((){});
return _sharedPreferences;
});
}
#override
void initState() {
resumeController();
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
_sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
super.didChangeAppLifecycleState(state);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text("Smart Scroll View"),
),
body: ListView.builder(
itemCount: 50,
controller: _scrollController,
itemBuilder: (c,i)=>
Padding(
padding: EdgeInsets.symmetric(horizontal: 24,vertical: 16),
child: Text((i+1).toString()),
),
),
),
);
}
}
Solution without knowing the size of your widgets
the Solution I found without knowing the size of your widget is displaying a reverse 'sublist' from the index to the end, then scroll to the top of your 'sublist' and reset the entire list. As it is a reverse list the item will be add at the top of the list and you will stay at your position (the index).
the problem is that you can't use a listView.builder because you will need to change the size of the list
example
class _ListViewIndexState extends State<ListViewIndex> {
ScrollController _scrollController;
List<Widget> _displayedList;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_displayedList = widget.items.sublist(0, widget.items.length - widget.index);
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
//here the sublist is already build
completeList();
});
}
}
completeList() {
//to go to the last item(in first position)
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
//reset the list to the full list
setState(() {
_displayedList = widget.items;
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView(
controller: _scrollController,
reverse: true,
children: _displayedList,
),
]
);
}
}
The https://pub.dev/packages/indexed_list_view package could maybe help you out for this. Use something like this:
IndexedListView.builder(
controller: indexScrollController,
itemBuilder: itemBuilder
);
indexScrollController.jumpToIndex(10000);
I'll present another approach, which supports list lazy loading unlike #Shinbly 's method, and also support tiles in list to resize without recalculating the correct offset of the ListView nor saving any persistent information like "#Nephew of Stackoverflow" does.
The essential key to this approach is to utilize CustomScrollView, the CustomScrollView.center property.
Here's an example based on the example code from Flutter document (widgets.CustomScrollView.2):
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
List<int> top = [];
List<int> bottom = [0];
List<int> test = List.generate(10, (i) => -5 + i);
bool positionSwitcher = true;
#override
Widget build(BuildContext context) {
positionSwitcher = !positionSwitcher;
final jumpIndex = positionSwitcher ? 1 : 9;
Key centerKey = ValueKey('bottom-sliver-list');
return Scaffold(
appBar: AppBar(
title: const Text('Press Jump!! to jump between'),
leading: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
top.add(-top.length - 1);
bottom.add(bottom.length);
});
},
),
),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(
child: Text('Jump!!'),
onPressed: () => setState(() {}),
),
Text(positionSwitcher ? 'At top' : 'At bottom'),
],
),
Expanded(
child: CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
final index = jumpIndex - 1 - i;
return Container(
alignment: Alignment.center,
color: Colors.blue[200 + test[index] % 4 * 100],
height: 100 + test[index] % 4 * 20.0,
child: Text('Item: ${test[index]}'),
);
},
childCount: jumpIndex,
),
),
SliverList(
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
final index = i + jumpIndex;
return Container(
alignment: Alignment.center,
color: i == 0
? Colors.red
: Colors.blue[200 + test[index] % 4 * 100],
height: 100 + test[index] % 4 * 20.0,
child: Text('Item: ${test[index]}'),
);
},
childCount: test.length - jumpIndex,
),
),
],
),
)
],
),
);
}
}
Explanation:
We use single list as data source for both SliverList
During each rebuild, we use center key to reposition the second SliverList inside ViewPort
Carefully manage the conversion from SliverList index to data source list index
Notice how the scroll view build the first SliverList by passing an index starting from bottom of this SliverList (i.e. index 0 suggests last item in the first list sliver)
Give the CustomeScrollView a proper key to decide whether to "re-position" or not
Working Example:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class ScrollToIndexDemo extends StatefulWidget {
const ScrollToIndexDemo({Key? key}) : super(key: key);
#override
_ScrollToIndexDemoState createState() => _ScrollToIndexDemoState();
}
class _ScrollToIndexDemoState extends State<ScrollToIndexDemo> {
late AutoScrollController controller = AutoScrollController();
var rng = Random();
ValueNotifier<int> scrollIndex = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ValueListenableBuilder(
valueListenable: scrollIndex,
builder: (context, index, child) {
return Text('Scroll Demo - $index');
},
),
),
body: ListView.builder(
itemCount: 100,
controller: controller,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(8),
child: AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
highlightColor: Colors.black.withOpacity(0.1),
child: Container(
padding: EdgeInsets.all(10),
alignment: Alignment.center,
color: Colors.grey[300],
height: 100,
child: Text(
'index: $index',
style: TextStyle(color: Colors.black),
),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
scrollIndex.value = rng.nextInt(100);
await controller.scrollToIndex(scrollIndex.value, preferPosition: AutoScrollPosition.begin);
},
tooltip: 'Increment',
child: Center(
child: Text(
'Next',
textAlign: TextAlign.center,
),
),
),
);
}
}
You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity
Create and use instance of ScrollController normally.
ScrollController scrollController = ScrollController();
ListView _buildListView() {
return ListView.builder(
controller: scrollController,
...
);
}
Create an instance of ListObserverController pass it to ListViewObserver
ListObserverController observerController = ListObserverController(controller: scrollController);
ListViewObserver(
controller: observerController,
child: _buildListView(),
...
)
Now you can scroll to the specified index position
// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)
// Jump to the specified index position with animation.
observerController.animateTo(
index: 1,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
I'm trying to implement a scrollable background image (parallax).
Like in a home screen launcher.
An example:
In Evie launcher:
this video
I've tried using AnimatedBuilder mentioned here in the docs like this.
I'm using a ValueNotifier<double> as the listener for the animation of the AnimatedBuilder Widget.
The complete code is this
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PageView Scrolling',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
ValueNotifier<double> _notifier;
double _prevnotifier;
double getOffset(){
if (_notifier.value == 0 && _prevnotifier != null){
return _prevnotifier;
}
return _notifier.value;
}
#override
void dispose() {
_notifier?.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_notifier = ValueNotifier<double>(0);
_prevnotifier = _notifier.value;
_notifier.addListener(
(){
print('object ${_notifier.value}');
if (_notifier.value != 0)
_prevnotifier = _notifier.value;
}
);
}
#override
Widget build(BuildContext context) {
print("Size is ${MediaQuery.of(context).size}");
return Scaffold(
body: Stack(
children: <Widget>[
AnimatedBuilder(
animation: _notifier,
builder: (context, _) {
return Transform.translate(
offset: Offset(-getOffset() * 60, 0),
child: Image.network(
"https://w.wallhaven.cc/full/r2/wallhaven-r276qj.png",
height: MediaQuery.of(context).size.height,
fit: BoxFit.fitHeight
),
);
},
),
NotifyingPageView(
notifier: _notifier,
),
],
),
);
}
}
class NotifyingPageView extends StatefulWidget {
final ValueNotifier<double> notifier;
const NotifyingPageView({Key key, this.notifier}) : super(key: key);
#override
_NotifyingPageViewState createState() => _NotifyingPageViewState();
}
class _NotifyingPageViewState extends State<NotifyingPageView> {
int _previousPage;
PageController _pageController;
void _onScroll() {
// Consider the page changed when the end of the scroll is reached
// Using onPageChanged callback from PageView causes the page to change when
// the half of the next card hits the center of the viewport, which is not
// what I want
if (_pageController.page.toInt() == _pageController.page) {
_previousPage = _pageController.page.toInt();
}
widget.notifier?.value = _pageController.page - _previousPage;
}
#override
void initState() {
_pageController = PageController(
initialPage: 0,
viewportFraction: 0.9,
)..addListener(_onScroll);
_previousPage = _pageController.initialPage;
super.initState();
}
List<Widget> _pages = List.generate(
10,
(index) {
return Container(
height: 10,
alignment: Alignment.center,
color: Colors.transparent,
child: Text(
"Card number $index",
style: TextStyle(
color: Colors.teal,
fontWeight: FontWeight.bold,
fontSize: 25,
),
),
);
},
);
#override
Widget build(BuildContext context) {
return PageView(
children: _pages,
controller: _pageController,
);
}
}
The image can be found here
Now I have two issues:
The image when using fit: BoxFit.fitHeight is not overflowing fully. It's currently like this
Because the value will become zero when the animation is done it's snapping like this:
this video
I tried storing the value just before the _notifier.value becomes zero and use it when it returns zero but it resulted in that weird transition that I've shown you in that above video.
What do you suggest can be done to make something like a scrollable wallpaper in flutter?
Something like this
Design
This is not as trivial as I thought it would be.
TLDR; Github read the comments.
I used a ValueNotifier<double> like I mentioned to control the scroll.
Then instead of Transform.translate I used an OverflowBox with its alignment property. Which is computed based on the notifier.value before rendering.
And to display the image in fullscreen mode:
I used AspectRatio with a child DecoratedBox whose decoration is a BoxDecoration with its image as an ImageProvider.
All the code can be found here on github. (Read the comments)
And this issue on github has slightly detailed info and a less complicated alternate implementation by Antonello Galipò
I've created an animated Floating Action Button (FAB) menu:
Main FAB which spawns other FABs when clicked.
It is based on several online tutorials.
Works fine besides one major issue:
The spawned FABs don't trigger an onPressed event.
Seems like there's a problem with the combination of transform animation and the stack widget (which the FABs are children of).
Works fine if I replace the stack with a row...
Seems like the issue was addressed here, but without a proper solution:
FloatingActionButton onPressed not triggering
Here's the complete code for that FAB menu.
Just supply it a list with buttons data, similar to this one:
List<Map<String, dynamic>> _buttonsData = [
{'color': Colors.green ,'icon': Icons.stop},
{'color': Colors.blue ,'icon': Icons.subway},
{'color': Colors.green ,'icon': Icons.add},];
FancyFab2(_buttonsData, null)
The code:
import 'package:flutter/material.dart';
class FancyFab extends StatefulWidget {
FancyFab(this._buttonsData, this._onSelected);
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
#override
createState() => FancyFabState(_buttonsData, _onSelected);
}
class FancyFabState extends State<FancyFab> with SingleTickerProviderStateMixin {
final List<Map<String, dynamic>> _buttonsData;
final ValueChanged<int> _onSelected;
// state vars
AnimationController _controller;
Animation <double> _transform;
bool _isOpened = false;
FancyFabState(this._buttonsData, this._onSelected);
#override
void initState() {
// call base
super.initState();
// _controller
_controller = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this
);
_transform = Tween<double>(
begin: 0.0,
end: -64.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn
),
);
}
#override
void dispose() {
// controller
_controller.dispose();
// call base
super.dispose();
}
#override
Widget build(BuildContext context) {
return _buildFab(_controller);
}
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Stack(
children: List.generate(_buttonsData.length, (index) => _buildStackWidget(context, index))
);
}
);
}
Widget _buildStackWidget(BuildContext context, int index) {
Map<String, dynamic> buttonData = _buttonsData[index];
if (index == _buttonsData.length - 1)
return _buildMenuButton(index, buttonData);
else
return _buildMenuItem(index, buttonData);
}
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
);
}
void _toggle() {
print('toggle');
_isOpened = !_isOpened;
if (true == _isOpened)
_controller.forward();
else
_controller.reverse();
}
}
That's because you are moving the items outside your Stack so you can't hitTest on those positions.
I modified a few lines of your code to make it works :
Add constraints to your Stack (you can use SizedBox also) , I'm using container to set a different color of background.
Widget _buildFab(AnimationController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Container(
width: MediaQuery.of(context).size.width,
height: 100,
color: Colors.blueGrey,
child: Stack(
children: List.generate(_buttonsData.length,
(index) => _buildStackWidget(context, index))),
);
});
}
Center your items
Widget _buildMenuItem(int index, Map<String, dynamic> buttonData) {
return Center(
child: Transform.translate(
offset: Offset((1 + index) * _transform.value, 0.0),
child: FloatingActionButton(
heroTag: 100 + index,
backgroundColor: buttonData['color'],
//onPressed: () => _onSelected(index),
onPressed: () => print('click'),
child: Icon(buttonData['icon']),
),
),
);
}
Widget _buildMenuButton(int index, Map<String, dynamic> buttonData) {
return Center(
child: FloatingActionButton(
heroTag: 200,
backgroundColor: buttonData['color'],
onPressed: _toggle,
child: Icon(buttonData['icon']),
),
);
}
I've made a widget that takes a list of children, and a List<double> of gaps, which displays the children with the respective gap between them. I've made it so passing a new list of gaps causes the widget to animate from the old gap to the new gaps (changing number of gaps not supported).
What's the best way to handle implicity animating the gaps?
This is the kind of behaviour I'm looking for:
(source: gfycat.com)
To avoid unneeded repetition you can move the tween logic to a custom widget.
You can also fuse List<Widget> children with List<double> gaps with a custom Gap widget.
Ultimately you can keep using ListView via separated constructor and use our custom Gap as separators.
Taking all of this into consideration, in the end your Gap widget is simply an AnimatedContainer with a custom height:
class Gap extends StatelessWidget {
final double gap;
const Gap(this.gap, {Key key})
: assert(gap >= .0),
super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOut,
height: gap,
);
}
}
And you can then use it using the following:
ListView.separated(
itemCount: 42,
addAutomaticKeepAlives: true,
itemBuilder: (context, index) {
return RaisedButton(onPressed: null, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
return Gap(10.0);
},
),
The addAutomaticKeppAlives: true here is used to ensure that items leaving then reappearing don't have their animation reset. But it is not a necessity.
Here's a full example with dynamically changing gap size:
class Home extends StatefulWidget {
#override
HomeState createState() {
return new HomeState();
}
}
class HomeState extends State<Home> {
final rand = Random.secure();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {});
},
child: Text("Reroll random gaps"),
),
Expanded(
child: ListView.separated(
addAutomaticKeepAlives: true,
itemCount: 42,
itemBuilder: (context, index) {
print("Bar");
return RaisedButton(onPressed: () {}, child: Text("Foo $index"));
},
separatorBuilder: (context, index) {
print("Foo $index");
return Gap(rand.nextDouble() * 10.0);
},
),
),
],
),
);
}
}
This was my solution, but my code is pretty messy. In particular, I'm not sure having a seperate list for the animations, tweens, controllers and curves (which is what I'm doing now) is the best way to do things. Also doing List<int>.generate(widget.gaps.length, (i) => i).forEach in the build function seems wrong, but the usual for (var i; i<x; i++) doesn't seem very dart-y either.
Is there a better way to handle these two issues?
class GappedList extends StatefulWidget {
final List<Widget> children;
final List<double> gaps;
GappedList({#required this.children, #required this.gaps}) :
assert(children != null),
assert(children.length > 0),
assert(gaps != null),
assert (gaps.length >= children.length - 1);
#override
GappedListState createState() {
return new GappedListState();
}
}
class GappedListState extends State<GappedList> with TickerProviderStateMixin{
List<Animation> _animations = [];
List<AnimationController> _controllers = [];
List<CurvedAnimation> _curves = [];
List<Tween<double>> _tweens;
#override
void initState() {
super.initState();
_tweens = widget.gaps.map((g) => Tween(
begin: g ?? 0.0,
end: g ?? 0.0,
)).toList();
_tweens.forEach((t) {
_controllers.add(AnimationController(
value: 1.0,
vsync: this,
duration: Duration(seconds: 1),
));
_curves.add(CurvedAnimation(parent: _controllers.last, curve: Curves.ease));
_animations.add(t.animate(_curves.last));
});
}
#override
void dispose() {
_controllers.forEach((c) => c.dispose());
super.dispose();
}
#override
void didUpdateWidget(GappedList oldWidget) {
super.didUpdateWidget(oldWidget);
assert(oldWidget.gaps.length == widget.gaps.length);
List<Tween<double>> oldTweens = _tweens;
List<int>.generate(widget.gaps.length, (i) => i).forEach(
(i) {
_tweens[i] = Tween<double>(
begin: oldTweens[i].evaluate(_curves[i]),
end: widget.gaps[i] ?? 0.0,
);
_animations[i] = _tweens[i].animate(_curves[i]);
if (_tweens[i].begin != _tweens[i].end) {
_controllers[i].forward(from: 0.0);
}
}
);
}
#override
Widget build(BuildContext context) {
List<Widget> list = [];
List<int>.generate(widget.children.length, (i) => i).forEach(
(i) {
list.add(widget.children[i]);
if (widget.children[i] != widget.children.last) {
list.add(
AnimatedBuilder(
animation: _animations[i],
builder: (context, _) => ConstrainedBox(
constraints: BoxConstraints.tightForFinite(
height: _animations[i].value,
),
),
)
);
}
}
);
return ListView(
primary: true,
shrinkWrap: true,
children: list,
);
}
}