Flutter AnimatedList and AnimatedController - flutter

One of them should open and close. But when pressed, animatedcontainers all open. How can I fix?
I placed the AnimatedContainer in AnimatedList. All of them don't look nice when opened.
The list slips when they all open.
I set the height and width in two setstate. But I cannot adjust this situation.
It should be very simple but I couldn't see it.
I think it will be useful if edited.
Thank you. For read.
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double _width = 0;
double _height = 0;
int _a=0;
final GlobalKey<AnimatedListState> _key =GlobalKey();
double _state(){
setState(() {
_width=25;
_height=20;
});
}
double _state2(){
setState(() {
_width=0;
_height=0;
});
}
List<String> _controll1=[
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: AnimatedList(
key: _key,
initialItemCount: _controll1.length,
itemBuilder: (context, index, animation){
return _buildItem(_controll1[index], animation,index);
}
),
);
}
Widget _buildItem(String item, Animation animation, int index){
return SizeTransition(
sizeFactor: animation,
child: Container(
width: 300,
child: Card(
elevation: 2,
child: Column(
children: [
Container(
child: RaisedButton(
color: Colors.black,
child: Text("${index}. button",style: TextStyle(color: Colors.white),),
onPressed: (){
for(;;) {
if (_a == 0) {
_state();
_a = 1;
return;
}
if (_a == 1) {
_state2();
_a = 0;
return;
}
}
},
),
),
AnimatedContainer(duration: Duration(milliseconds: 5),
width: _width,
height: _height,
color: Colors.black,
),
],
)
),
),
);
}
}

From what i understood, you need not use any animation at all.
And what you missed was just that, you were passing same size to all the widgets, so when you click one all would change.
I tried to recreate your use case and achieved it without any animation
class MyApp2 extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp2> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> _controll1 = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
List<Size> _size;
#override
void initState() {
_size = [
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: ListView.builder(
itemCount: _controll1.length,
itemBuilder: (context, index) {
return _buildItem(_controll1[index], index);
}),
);
}
Widget _buildItem(String item, int index) {
return Container(
width: 300,
child: Card(
elevation: 2,
child: Column(
children: [
Container(
child: RaisedButton(
color: Colors.black,
child: Text(
"${index}. button",
style: TextStyle(color: Colors.white),
),
onPressed: () {
if (_size[index] == Size(0, 0)) {
setState(() {
_size[index] = Size(25, 30);
});
} else if (_size[index] == Size(25, 30)) {
setState(() {
_size[index] = Size(0, 0);
});
}
},
),
),
Container(
width: _size[index].width,
height: _size[index].height,
color: Colors.black,
),
],
)),
);
}
}

I wanted to do this. Your code has been very useful Nitesh. Thank you. I did.
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double _width = 0;
double _height = 0;
int _a = 0;
var duration = Duration(seconds: 1);
final GlobalKey<AnimatedListState> _key = GlobalKey();
int _c = 0;
int _b = 0;
List<String> _controll1 = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
#override
void initState() {
// TODO: implement initState
_a = 0;
_b = 0;
_width = 0;
_height = 0;
_c = 1;
duration = Duration(seconds: 1);
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: AnimatedList(
controller: ScrollController(initialScrollOffset: 5),
scrollDirection: Axis.vertical,
key: _key,
initialItemCount: _controll1.length,
itemBuilder: (context, index, animation) {
return _buildItem(_controll1[index], animation, index);
}),
),
);
}
Widget _buildItem(String item, Animation animation, int index) {
return SizeTransition(
sizeFactor: animation,
child: Center(
child: Column(
children: [
Center(
child: RaisedButton(
color: Colors.black,
onPressed: () {
print(index);
if(_a==0){
setState(() {
duration=Duration(seconds: 2);
_b=index;
_width=80.0;
_height=30.0;
_a=1;
_c=index;
});
}else if(_a==1&&_c==index){
setState(() {
Duration(seconds: 2);
_b=index;
_width=0;
_height=0;
_a=0;
});
}else if(_a==1&&_c!=index){
setState(() {
Duration(seconds: 2);
_b=index;
_width=80;
_height=30;
_a=1;
_c=index;
});
}
},
child: Text(
"${index}. button",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedContainer(
duration: duration,
width: _b!=index?0:_width,
height: _b!=index?0:_height,
curve: Curves.fastOutSlowIn,
decoration: BoxDecoration(
color: Colors.black,
),
child: RaisedButton(
child: Text("Delete"),
onPressed: (){
setState(() {
_removeItem(index);
_b=index;
duration=Duration(seconds: 0);
_width=0;
_height=0;
_a=0;
});
},
),
),
],
),
),
);
}
void _removeItem(int i) {
String removeItem = _controll1.removeAt(i);
AnimatedListRemovedItemBuilder builder = (context, animation) {
return _buildItem(removeItem, animation, i);
};
_key.currentState.removeItem(i, builder);
}
}

Related

StreamBuilder not updating widget

i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.

Triggering page controller animation from ViewModel

i'm trying to implement a View with a PageView widget, that is being controlled by the ViewModel.
In my view i have buttons that trigger the following method in the ViewModel:
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
currentIndex.add(_index);
}
}
currentIndex is just a sink of my _currentIndexController, and outputCurrentIndex is a stream of the controller:
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
it looks like the value is added to the stream successfully, but i can't get it to trigger changing pages, i've set up this listener in the initState() method of the view:
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
but it is not triggered for some reason.. what am i doing wrong?
here is a full code of my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose(){
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
}
#override
Widget build(BuildContext context) {
List<Widget> pagesList = [
const SexPage(),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return MultiProvider(
providers: [
StreamProvider.value(value: _viewModel.outputProgress, initialData: 0.25),
StreamProvider.value(value: _viewModel.outputCurrentIndex, initialData: 0),
],
child: Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
),
);
}
}
class AppBarWidget extends StatefulWidget {
const AppBarWidget({
Key? key,
}) : super(key: key);
#override
State<AppBarWidget> createState() => _AppBarWidgetState();
}
class _AppBarWidgetState extends State<AppBarWidget> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: Progresso(
progress: Provider.of<double>(context),
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
),
),
),
),
Expanded(
flex: 1,
child: Provider.of<int>(context) > 1
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
),
],
);
}
}
and my ViewModel:
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
void dispose() {
_progressBarController.close();
_currentIndexController.close();
_isBackEnabled.close();
}
#override
void start() {
// TODO: implement start
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Sink get progress => _progressBarController.sink;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
}
abstract class RegisterViewModelInputs {
register();
increaseProgress();
decreaseProgress();
nextPage();
previousPage();
setIsBackEnabled(int index);
Sink get currentIndex;
Sink get isBackEnabled;
Sink get progress;
}
abstract class RegisterViewModelOutputs {
Stream<int> get outputCurrentIndex;
Stream<double> get outputProgress;
Stream<bool> get outputIsBackEnabled;
}
EDIT: I have moved the stream listener to the _AppBarWidgetState build method, and it seems to work now, but i don't fully understand why it hasn't worked before..
is it because the PageController wasn't assigned to a view yet? where is the correct place for the listener? it doesn't makes sense to me for it to be in a child widget.

Flutter Scroll view to focused widget on a column

I'm developing an app for Android TV, and use DPAD navigation.
I have multiple widgets inside a column. when i navigate to a widget which is outside the view, the widget/view is not moving to reflect the selected widget.
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatelessWidget(),
),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4!,
child: ChangeNotifierProvider<SampleNotifier>(
create: (context) => SampleNotifier(), child: const CardHolder()),
);
}
}
class CardHolder extends StatefulWidget {
const CardHolder({Key? key}) : super(key: key);
#override
_CardHolderState createState() => _CardHolderState();
}
class _CardHolderState extends State<CardHolder> {
late FocusNode _focusNode;
late FocusAttachment _focusAttachment;
#override
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: "traversal_node");
_focusAttachment = _focusNode.attach(context, onKey: _handleKeyPress);
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
_focusAttachment.reparent();
return Focus(
focusNode: _focusNode,
autofocus: true,
onKey: _handleKeyPress,
child: Consumer<SampleNotifier>(
builder: (context, models, child) {
int listSize = Provider.of<SampleNotifier>(context).listSize;
return SingleChildScrollView(
child: SampleRow(cat: "Test", models: models.modelList),
);
},
),
);
}
KeyEventResult _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print("t:FocusNode: ${node.debugLabel} event: ${event.logicalKey}");
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
Provider.of<SampleNotifier>(context, listen: false).moveRight();
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
Provider.of<SampleNotifier>(context, listen: false).moveLeft();
return KeyEventResult.handled;
}
}
// debugDumpFocusTree();
return KeyEventResult.ignored;
}
}
class SampleCard extends StatefulWidget {
final int number;
final SampleModel model;
final bool focused;
const SampleCard(
{required this.number,
required this.focused,
required this.model,
Key? key})
: super(key: key);
#override
_SampleCardState createState() => _SampleCardState();
}
class _SampleCardState extends State<SampleCard> {
late Color _color;
#override
void initState() {
super.initState();
_color = Colors.red.shade900;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: widget.focused
? Container(
width: 150,
height: 300,
color: Colors.white,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
)
: Container(
width: 150,
height: 300,
color: Colors.black,
child: Center(
child: Text(
"${widget.model.text} ${widget.model.num}",
style: TextStyle(color: _color),
),
),
),
);
}
}
class SampleRow extends StatelessWidget {
final String cat;
final List<SampleModel> models;
SampleRow({Key? key, required this.cat, required this.models}) : super(key: key);
#override
Widget build(BuildContext context) {
final int selectedIndex =
Provider.of<SampleNotifier>(context).selectedIndex;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16, bottom: 8),
),
models.isNotEmpty
? SizedBox(
height: 200,
child: ListView.custom(
padding: const EdgeInsets.all(8),
scrollDirection: Axis.horizontal,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SampleCard(
focused: index == selectedIndex,
model: models[index],
number: index,
),
),
childCount: models.length,
findChildIndexCallback: _findChildIndex,
),
),
)
: SizedBox(
height: 200,
child: Container(
color: Colors.teal,
),
)
],
);
}
int _findChildIndex(Key key) => models.indexWhere((model) =>
"$cat-${model.text}_${model.num}" == (key as ValueKey<String>).value);
}
class SampleNotifier extends ChangeNotifier {
final List<SampleModel> _models = [
SampleModel(0, "zero"),
SampleModel(1, "one"),
SampleModel(2, "two"),
SampleModel(3, "three"),
SampleModel(4, "four"),
SampleModel(5, "five"),
SampleModel(6, "six"),
SampleModel(7, "seven"),
SampleModel(8, "eight"),
SampleModel(9, "nine"),
SampleModel(10, "ten")
];
int _selectedIndex = 0;
List<SampleModel> get modelList => _models;
int get selectedIndex => _selectedIndex;
int get listSize => _models.length;
void moveRight() {
if (_selectedIndex < _models.length - 1) {
_selectedIndex = _selectedIndex + 1;
}
notifyListeners();
}
void moveLeft() {
if (_selectedIndex > 0) {
_selectedIndex = _selectedIndex - 1;
}
notifyListeners();
}
}
class SampleModel {
int num;
String text;
SampleModel(this.num, this.text);
}
I need a way to move/scroll the widget into view. Is there any way to do this, using the DPAD navigation on android tv
Here is the gist
You could use the scrollable_positioned_list package.
Instead of a ListView.custom which scrolls based on pixels, this widgets its based on index:
final ItemScrollController itemScrollController = ItemScrollController();
ScrollablePositionedList.builder(
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
So you could maintain an index of the current scroll position and on DPAD press just :
itemScrollController.jumpTo(index: currentItem);
setState((){currentItem++;})

How to create sticky shop button animation in Flutter?

How can I create this sticky buy button animation of Adidas app in Flutter. I have tried to use a scroll controller to listen for the position of user and then use an animated container but it is of no use since I have to define my scroll controller in my initstate while the height of my containers are relative to my device's height.
here is the link of video for the animation:
https://drive.google.com/file/d/1TzIUBr6abRQI87xAVu4NOPG67aftzceK/view?usp=sharing
this is what the widget tree looks like:
Scaffold(appbar,FAB,_body),
_body= SingleChildSrollView child:Column[Container(child:Listview)
,Container(child:PageView(children:[GridView])
,Container
,Container(this is where the shop button should be, the one that replaces the FAB)
,GridView,])
Output:
void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
ScrollController _controller = ScrollController();
double _boxHeight = 200, _screenHeight;
int _itemIndex = 5;
bool _itemVisibility = true;
#override
void initState() {
super.initState();
double offsetEnd;
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderBox box = context.findRenderObject();
_screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
offsetEnd = ((_itemIndex + 1) - (_screenHeight / _boxHeight)) * _boxHeight;
});
_controller.addListener(() {
if (_controller.position.pixels >= offsetEnd) {
if (_itemVisibility) setState(() => _itemVisibility = false);
} else {
if (!_itemVisibility) setState(() => _itemVisibility = true);
}
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView.builder(
controller: _controller,
itemCount: 8,
itemBuilder: (context, index) {
return _buildBox(
index: index,
color: index == _itemIndex ? Colors.cyan : Colors.blue[((index + 1) * 100) % 900],
);
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Visibility(
visible: _itemVisibility,
child: _buildBox(index: _itemIndex, color: Colors.cyan),
),
),
],
);
}
Widget _buildBox({int index, Color color}) {
return Container(
height: _boxHeight,
color: color,
alignment: Alignment.center,
child: Text(
"${index}",
style: TextStyle(fontSize: 52, fontWeight: FontWeight.bold),
),
);
}
}
Another answer (variable height boxes)
void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
ScrollController _controller = ScrollController();
double _screenHeight, _hRatings = 350, _hSize = 120, _hWidth = 130, _hComfort = 140, _hQuality = 150, _hBuy = 130, _hQuestions = 400;
bool _itemVisibility = true;
#override
void initState() {
super.initState();
double offsetEnd;
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderBox box = context.findRenderObject();
_screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
offsetEnd = (_hRatings + _hSize + _hWidth + _hComfort + _hQuality + _hBuy) - _screenHeight;
});
_controller.addListener(() {
if (_controller.position.pixels >= offsetEnd) {
if (_itemVisibility) setState(() => _itemVisibility = false);
} else {
if (!_itemVisibility) setState(() => _itemVisibility = true);
}
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView(
controller: _controller,
children: <Widget>[
_buildBox(_hRatings, "Ratings box", Colors.blue[200]),
_buildBox(_hSize, "Size box", Colors.blue[300]),
_buildBox(_hWidth, "Width box", Colors.blue[400]),
_buildBox(_hComfort, "Comfort box", Colors.blue[500]),
_buildBox(_hQuality, "Quality box", Colors.blue[600]),
_buildBox(_hBuy, "Buy box", Colors.orange[700]),
_buildBox(_hQuestions, "Questions part", Colors.blue[800]),
],
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Visibility(
visible: _itemVisibility,
child: _buildBox(_hBuy, "Buy box", Colors.orange[700]),
),
),
],
);
}
Widget _buildBox(double height, String text, Color color) {
return Container(
height: height,
color: color,
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
fontSize: 32,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
);
}
}
I would show a floatingActionButton when the embedded button is not visible. Here's a solution based on this thread : How to know if a widget is visible within a viewport?
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
GlobalKey<State> key = new GlobalKey();
double fabOpacity = 1.0;
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Scrolling."),
),
body: NotificationListener<ScrollNotification>(
child: new ListView(
itemExtent: 100.0,
children: [
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
new MyObservableWidget(key: key),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder()
],
),
onNotification: (ScrollNotification scroll) {
var currentContext = key.currentContext;
if (currentContext == null) return false;
var renderObject = currentContext.findRenderObject();
RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
var offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0);
var offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0);
if (offsetToRevealBottom.offset > scroll.metrics.pixels ||
scroll.metrics.pixels > offsetToRevealTop.offset) {
if (fabOpacity != 1.0) {
setState(() {
fabOpacity = 1.0;
});
}
} else {
if (fabOpacity == 1.0) {
setState(() {
fabOpacity = 0.0;
});
}
}
return false;
},
),
floatingActionButton: new Opacity(
opacity: fabOpacity,
child: Align(
alignment: Alignment.bottomCenter,
child: new FloatingActionButton.extended(
label: Text('sticky buy button'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
onPressed: () {
print("YAY");
},
),
),
),
),
);
}
}
class MyObservableWidget extends StatefulWidget {
const MyObservableWidget({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => new MyObservableWidgetState();
}
class MyObservableWidgetState extends State<MyObservableWidget> {
#override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: () {
},
color: Colors.lightGreenAccent,
child: Text('This is my buy button', style: TextStyle(color: Colors.blue),),
);
}
}
class ContainerWithBorder extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(border: new Border.all(), color: Colors.grey),
);
}
}

How to access list item when built with builder?

In the following Flutter app, I'm trying to show a LinearProgressIndicator in each card only when that card is counting. The the correct progression is printed to the console, but I can't figure out how to access "stepProgress" variable from the LinearProgressIndicator widget to update the view.
The cards are being built with a builder because they will change based on the input List (Array) of Maps (Objects).
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
final key = new GlobalKey<_MyHomePageState>();
List<Widget> cards = [];
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
canvasColor: Colors.grey[350],
),
home: MyHomePage(title: 'Title', key: key),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List _sequence = [];
double stepProgress = 0.0;
#override
initState() {
super.initState();
setState(() => _sequence = [
{'iterations': 1, 'time': 10},
{'iterations': 3, 'time': 7},
{'iterations': 2, 'time': 5},
]);
setState(() {
cards = getRun();
});
_countdown(_sequence, null);
}
getRun() {
List<Widget> runCards = [];
for (var group in _sequence) {
runCards.add(_buildCard(CardModel(
iterationsInGroup: group['iterations'],
timeEach: group['time'],
)));
}
return runCards;
}
void _countdown(seq, iters) async {
if (seq.length > 0) {
int i = iters == null ? seq[0]['iterations'] : iters;
if (i > 0) {
int duration = seq[0]["time"];
Timer.periodic(Duration(seconds: 1), (timer) {
if (timer.tick < duration) {
setState(() {
stepProgress = timer.tick / duration;
});
print('Iteration $i: ${timer.tick} / $duration = $stepProgress');
} else {
print('Finished iteration $i');
timer.cancel();
i = i - 1;
if (i > 0) {
_countdown(seq, i); // Next iteration
} else {
print('Finished group ${seq.length}');
timer.cancel();
if (seq.length > 1) {
_countdown(seq.sublist(1), null); // Next group
} else {
print('Done');
}
}
}
});
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'Header',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: ListView(
children: cards,
padding: const EdgeInsets.all(8.0),
),
),
],
),
);
}
}
Widget _buildCard(CardModel card) {
List<Widget> columnData = <Widget>[];
columnData.add(
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
card.timeEach.toString() +
' seconds ' +
card.iterationsInGroup.toString() +
' times',
style: TextStyle(fontSize: 22.0),
),
),
true //key.currentState.activeStep == card.cardStep //TODO: This doesn't work
? LinearProgressIndicator(
value: key.currentState.stepProgress,
)
: Container(width: 0.0, height: 0.0),
],
),
);
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Column(children: columnData),
),
);
}
class CardModel {
final int iterationsInGroup;
final int timeEach;
CardModel({
this.iterationsInGroup,
this.timeEach,
});
}
I modified your code a little, this is a little messy therefore I recommend you the following:
Create a StatefulWidget for your ChildView (Like my code below).
Keep the progress logic into the ChildView, it would be easy to change the status in that way.
I had to keep track the Globalkey in order to refresh the changes into the child view, but if you handle the logic into each child, you don't need the GlobalKey.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
final key = new GlobalKey<_MyHomePageState>();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
canvasColor: Colors.grey[350],
),
home: MyHomePage(title: 'Title', key: key),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List _sequence = [];
List<ChildView> runCards = [];
#override
initState() {
super.initState();
setState(() => _sequence = [
{'iterations': 1, 'time': 10, 'progress': 0.0},
{'iterations': 3, 'time': 7, 'progress': 0.0},
{'iterations': 2, 'time': 5, 'progress': 0.0},
]);
getRun();
_countdown(_sequence, null);
}
getRun() {
for (var group in _sequence) {
var cardModel = CardModel(
iterationsInGroup: group['iterations'],
timeEach: group['time'],
progress: group['progress'],
);
runCards.add(new ChildView(cardModel,new GlobalKey<_ChildViewState>()));
}
setState(() {
});
return runCards;
}
void _countdown(seq, iters) async {
if (seq.length > 0) {
int i = iters == null ? seq[0]['iterations'] : iters;
if (i > 0) {
int duration = seq[0]["time"];
Timer.periodic(Duration(seconds: 1), (timer) {
if (timer.tick <= duration) {
var childView = runCards[i-1];
double stepProgress = 0.0;
stepProgress = timer.tick / duration;
childView.key.currentState.updateProgress(stepProgress);
print('Iteration $i: ${timer.tick} / $duration = $stepProgress');
} else {
print('Finished iteration $i');
timer.cancel();
i = i - 1;
if (i > 0) {
_countdown(seq, i); // Next iteration
} else {
print('Finished group ${seq.length}');
timer.cancel();
if (seq.length > 1) {
_countdown(seq.sublist(1), null); // Next group
} else {
print('Done');
}
}
}
});
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'Header',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: ListView(
children: runCards,
padding: const EdgeInsets.all(8.0),
),
),
],
),
);
}
}
class CardModel {
final int iterationsInGroup;
final int timeEach;
double progress;
CardModel({
this.iterationsInGroup,
this.timeEach,
this.progress,
});
}
class ChildView extends StatefulWidget {
final CardModel card;
final GlobalKey<_ChildViewState> key;
ChildView(this.card, this.key) : super(key: key);
#override
_ChildViewState createState() => _ChildViewState();
}
class _ChildViewState extends State<ChildView> {
void updateProgress(double progress){
setState(() {
widget.card.progress = progress;
});
}
#override
Widget build(BuildContext context) {
List<Widget> columnData = <Widget>[];
columnData.add(
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.card.timeEach.toString() +
' seconds ' +
widget.card.iterationsInGroup.toString() +
' times',
style: TextStyle(fontSize: 22.0),
),
),
widget.card.progress < 1 //key.currentState.activeStep == card.cardStep //TODO: This doesn't work
? LinearProgressIndicator(
value: widget.card.progress,
)
: Container( child: new Text("Completed"),),
],
),
);
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Column(children: columnData),
),
);
}
}