How to make ListWheelScrollView horizontal - android

I Am trying to have a horizontal ListView Widget that magnifies the center items. I tried using the normal ListView but I couldn't get the center items to magnify. Then while searching the flutter docs I came across ListWheelScrollView but unfortunately, it doesn't seem to support horizontal children layout. So basically am looking to create a horizontal ListView with center items magnification. I'd appreciate it if anyone can at least point me in the right direction. Thanks

Edit:
I have published package based on this.
pub.dev/packages/list_wheel_scroll_view_x
Here's my workaround.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ListWheelScrollViewX extends StatelessWidget {
final Widget Function(BuildContext, int) builder;
final Axis scrollDirection;
final FixedExtentScrollController controller;
final double itemExtent;
final double diameterRatio;
final void Function(int) onSelectedItemChanged;
const ListWheelScrollViewX({
Key key,
#required this.builder,
#required this.itemExtent,
this.controller,
this.onSelectedItemChanged,
this.scrollDirection = Axis.vertical,
this.diameterRatio = 100000,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return RotatedBox(
quarterTurns: scrollDirection == Axis.horizontal ? 3 : 0,
child: ListWheelScrollView.useDelegate(
onSelectedItemChanged: onSelectedItemChanged,
controller: controller,
itemExtent: itemExtent,
diameterRatio: diameterRatio,
physics: FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
return RotatedBox(
quarterTurns: scrollDirection == Axis.horizontal ? 1 : 0,
child: builder(context, index),
);
},
),
),
);
}
}

You can do this using the built-in ListWheelScrollView by pairing it with the Rotated Box. This is kind of a hack which works.
RotatedBox(
quarterTurns: -1,
child: ListWheelScrollView(
onSelectedItemChanged: (x) {
setState(() {
selected = x;
});
},
controller: _scrollController,
children: List.generate(
itemCount,
(x) => RotatedBox(
quarterTurns: 1,
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
width: x == selected ? 60 : 50,
height: x == selected ? 60 : 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: x == selected ? Colors.red : Colors.grey,
shape: BoxShape.circle),
child: Text('$x')))),
itemExtent: itemWidth,
)),
);
Full source Code here
I recommend using this approach until this issue is resolved
Heres the output

copy this code and used it
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ListWheelScrollViewX extends StatelessWidget {
final Axis scrollDirection;
final List<Widget>? children;
final ScrollController? controller;
final ScrollPhysics? physics;
final double diameterRatio;
final double perspective;
final double offAxisFraction;
final bool useMagnifier;
final double magnification;
final double overAndUnderCenterOpacity;
final double itemExtent;
final double squeeze;
final ValueChanged<int>? onSelectedItemChanged;
final bool renderChildrenOutsideViewport;
final ListWheelChildDelegate? childDelegate;
final Clip clipBehavior;
const ListWheelScrollViewX({
Key? key,
this.scrollDirection = Axis.vertical,
this.controller,
this.physics,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
this.overAndUnderCenterOpacity = 1.0,
required this.itemExtent,
this.squeeze = 1.0,
this.onSelectedItemChanged,
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
required this.children,
}) : childDelegate = null,
super(key: key);
const ListWheelScrollViewX.useDelegate({
Key? key,
this.scrollDirection = Axis.vertical,
this.controller,
this.physics,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
this.overAndUnderCenterOpacity = 1.0,
required this.itemExtent,
this.squeeze = 1.0,
this.onSelectedItemChanged,
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
required this.childDelegate,
}) : children = null,
super(key: key);
#override
Widget build(BuildContext context) {
final _childDelegate = children != null
? ListWheelChildListDelegate(
children: children!.map((child) {
return RotatedBox(
quarterTurns: scrollDirection == Axis.horizontal ? 1 : 0,
child: child,
);
}).toList())
: ListWheelChildBuilderDelegate(
builder: (context, index) {
return RotatedBox(
quarterTurns: scrollDirection == Axis.horizontal ? 1 : 0,
child: childDelegate!.build(context, index),
);
},
);
return RotatedBox(
quarterTurns: scrollDirection == Axis.horizontal ? 3 : 0,
child: ListWheelScrollView.useDelegate(
controller: controller,
physics: FixedExtentScrollPhysics(),
diameterRatio: diameterRatio,
perspective: perspective,
offAxisFraction: offAxisFraction,
useMagnifier: useMagnifier,
magnification: magnification,
overAndUnderCenterOpacity: overAndUnderCenterOpacity,
itemExtent: itemExtent,
squeeze: squeeze,
onSelectedItemChanged: onSelectedItemChanged,
renderChildrenOutsideViewport: renderChildrenOutsideViewport,
clipBehavior: clipBehavior,
childDelegate: _childDelegate,
),
);
}
}
Example
ListWheelScrollViewX(
scrollDirection: Axis.horizontal,
itemExtent: 120,
children:...
),
Reference
list_wheel_scroll_view_x
changes:
convert to null safety

You can use this flutter package https://pub.dev/packages/carousel_slider.
It also has a very helpful description and few samples to see how it looks. And it's compatible with dart 2.0 too.

You can make this work with the help of ListView and PageView along with NotificationListener.
Below is my code for the same-
import 'dart:math';
import 'package:flutter/material.dart';
const SCALE_FRACTION = 0.9;
const FULL_SCALE = 1.0;
final PAGER_HEIGHT = SizeConfig.screenHeight*0.32;
const PAGER_WIDTH = double.infinity;
class PaymentWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _PaymentState();
}
class _PaymentState extends State<PaymentWidget> {
double viewPortFraction = 0.9;
int currentPage = 1;
double page = 2.0;
PageController pageController;
final List<String> _cardsImages = ['image/path1', 'image/path2',
'image/path3', 'image/path4'];
#override
void initState() {
pageController = PageController(
initialPage: currentPage, viewportFraction: viewPortFraction);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: null,
body: _creditCardsList()
);
}
Widget _creditCardsList() {
return ListView(
shrinkWrap: true,
children: <Widget>[
Container(
height: PAGER_HEIGHT,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
setState(() {
page = pageController.page;
});
}
},
child: PageView.builder(
onPageChanged: (pos) {
setState(() {
currentPage = pos;
});
},
physics: BouncingScrollPhysics(),
controller: pageController,
itemCount: _cardsImages.length,
itemBuilder: (context, index) {
final scale =
max(SCALE_FRACTION, (FULL_SCALE - (index - page).abs()) + viewPortFraction);
return CreditCardTile(
_cardsImages[index], scale);
},
),
),
),
],
);
}
Widget CreditCardTile(String image, double scale) {
return Align(
alignment: Alignment.bottomCenter,
child:Container(
height: PAGER_HEIGHT * scale,
width: PAGER_WIDTH * scale,
child: Card(
elevation: 5,
shadowColor: constColors.blueWhiteShade,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)
),
clipBehavior: Clip.antiAlias,
child: Image.asset(
image,
fit: BoxFit.cover,
),
)
) ,
);
}

Add 2 RotatedBox as Follows
StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance
.collection('Categories')
.doc('EcomCat')
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
final DocumentSnapshot document = snapshot.data!;
final Map<String, dynamic> documentData =
document.data() as Map<String, dynamic>;
final List Category = documentData['category'];
return RotatedBox(quarterTurns: 1,
child: ListWheelScrollView.useDelegate(magnification: 200,
itemExtent: 180,
childDelegate: ListWheelChildBuilderDelegate(
childCount: Category.length,
builder: (context,index){
return Row(
children: [
RotatedBox(quarterTurns: -1,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(10),
shadowColor: Colors.black,
primary: Colors.teal,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(18))),
onPressed: () {
setState(() {
categoryVal =
Category[index].toString();
});
},
child: Text(
Category[index].toString(),
style: themeData.textTheme.headline4,
),
),
),
],
);
},
)),
);
},
),

Transform.rotate(
angle: -math.pi / 2,
child: ListWheelScrollView()
}
use Transform.rotate

Related

Is there a way to have a "User" place a button widget on an interactive viewer. (Like a Floorplan Builder)

I am building an app that allows the user to build their gyms floorplan and place buttons where the machines would be.
I am currently using an Interactive Viewer to display the floorplan. I need the functionality to create a button widget where the "User" long-presses.
Here is my Current Code with the onLongPressEnd class.
Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Center(
child: GestureDetector(
onLongPressEnd: ((details) => {
//Code to create button goes here
}),
child: InteractiveViewer(
minScale: 1,
maxScale: 2,
child: Stack(
children: [
Image.asset(
'Assets/Lift View Images/room_layout_1.png'),
);
Someone on the internet has posted an almost identical problem to this and created a whole solution, the problem is since his solution, the InteractiveViewer widget has been added so all of his code is obsolete and cannot be copied to a newer version.
https://medium.com/#alexander.arendar/dragging-zooming-and-placing-object-on-indoor-map-with-flutter-67667ef415ec
In conclusion I need the functionality for the user to create pre defined widgets by pressing on the page.
Full Code https://github.com/CokeMango/mapview_iteration_1.git
I tried for hours to understand this documents solution, https://medium.com/#alexander.arendar/dragging-zooming-and-placing-object-on-indoor-map-with-flutter-67667ef415ec but never figured out how to implement it with the Interactive Viewer widget. And being fairly new to Flutter I couldn't replicate it exactly.
I also searched online for a while and at most I found what I already had which was a zoomable scrollable image viewer with no functionality.
here you can see two approaches: FloorPlanWithFixedButtons adds "buttons" with a fixed size (no matter what the current zoom factor is used by InteractiveViewer), while FloorPlanWithScaledButtons adds "buttons" directly to InteractiveViewer so they are automatically scaled when you zoom-in / zoom-out
if you need draggable "buttons" check FloorPlanWithScaledDraggableButtons widget
class ChipEntry {
ChipEntry({
required this.offset,
required this.label,
});
Offset offset;
final String label;
}
FloorPlanWithFixedButtons
class FloorPlanWithFixedButtons extends StatefulWidget {
#override
State<FloorPlanWithFixedButtons> createState() => _FloorPlanWithFixedButtonsState();
}
class _FloorPlanWithFixedButtonsState extends State<FloorPlanWithFixedButtons> with TickerProviderStateMixin {
int animatedIndex = -1;
late final controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 250));
final chips = <ChipEntry>[];
final transformationController = TransformationController();
int labelNumber = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('1) long press on the floor plan below to add a new button\n'),
),
Expanded(
child: ClipRect(
child: GestureDetector(
onLongPressStart: _addButton,
child: Stack(
children: [
InteractiveViewer(
minScale: 1,
maxScale: 5,
constrained: false,
transformationController: transformationController,
// https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Sample_Floorplan.jpg/640px-Sample_Floorplan.jpg
child: Image.asset('images/640px-Sample_Floorplan.jpg'),
),
CustomMultiChildLayout(
delegate: FloorPlanDelegate(
chips: chips,
transformationController: transformationController,
),
children: [
for (int index = 0; index < chips.length; index++)
LayoutId(id: index, child: _button(index)),
],
),
],
),
),
),
),
],
);
}
Widget _button(int index) {
final button = Chip(
backgroundColor: Colors.orange,
side: const BorderSide(width: 1, color: Colors.black12),
elevation: 4,
onDeleted: () async {
setState(() {
animatedIndex = index;
});
await controller.reverse(from: 1.0);
setState(() {
chips.removeAt(index);
animatedIndex = -1;
});
},
label: InkWell(
onTap: () => print('button |${chips[index].label}| at index $index pressed'),
child: Text(chips[index].label),
),
);
return index == animatedIndex? ScaleTransition(scale: controller, child: button) : button;
}
void _addButton(LongPressStartDetails details) async {
setState(() {
animatedIndex = chips.length;
final chipEntry = ChipEntry(
offset: transformationController.toScene(details.localPosition),
label: 'btn #$labelNumber'
);
chips.add(chipEntry);
labelNumber++;
});
await controller.forward(from: 0.0);
animatedIndex = -1;
}
}
class FloorPlanDelegate extends MultiChildLayoutDelegate {
FloorPlanDelegate({
required this.chips,
required this.transformationController,
}) : super(relayout: transformationController); // NOTE: this is very important
final List<ChipEntry> chips;
final TransformationController transformationController;
#override
void performLayout(ui.Size size) {
// print('performLayout $size');
int id = 0;
final constraints = BoxConstraints.loose(size);
final matrix = transformationController.value;
for (final chip in chips) {
final size = layoutChild(id, constraints);
final offset = MatrixUtils.transformPoint(matrix, chip.offset) - size.center(Offset.zero);
positionChild(id, offset);
id++;
}
}
#override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => false;
}
FloorPlanWithScaledButtons
class FloorPlanWithScaledButtons extends StatefulWidget {
#override
State<FloorPlanWithScaledButtons> createState() => _FloorPlanWithScaledButtonsState();
}
class _FloorPlanWithScaledButtonsState extends State<FloorPlanWithScaledButtons> with TickerProviderStateMixin {
int animatedIndex = -1;
late final controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 250));
final chips = <ChipEntry>[];
final transformationController = TransformationController();
int labelNumber = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('1) long press on the floor plan below to add a new button\n'),
),
Expanded(
child: ClipRect(
child: GestureDetector(
onLongPressStart: _addButton,
child: InteractiveViewer(
minScale: 1,
maxScale: 5,
constrained: false,
transformationController: transformationController,
child: Stack(
children: [
// https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Sample_Floorplan.jpg/640px-Sample_Floorplan.jpg
Image.asset('images/640px-Sample_Floorplan.jpg'),
...chips.mapIndexed(_positionedButton),
],
),
),
),
),
),
],
);
}
Widget _positionedButton(int index, ChipEntry chip) {
final child = Chip(
backgroundColor: Colors.orange,
side: const BorderSide(width: 1, color: Colors.black12),
elevation: 4,
onDeleted: () async {
setState(() {
animatedIndex = index;
});
await controller.reverse(from: 1.0);
setState(() {
chips.removeAt(index);
animatedIndex = -1;
});
},
label: InkWell(
onTap: () => print('button |${chip.label}| at index $index pressed'),
child: Text(chip.label),
),
);
return Positioned(
left: chip.offset.dx,
top: chip.offset.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: index == animatedIndex? ScaleTransition(scale: controller, child: child) : child,
),
);
}
void _addButton(LongPressStartDetails details) async {
setState(() {
animatedIndex = chips.length;
final chipEntry = ChipEntry(
offset: transformationController.toScene(details.localPosition),
label: 'btn #$labelNumber'
);
chips.add(chipEntry);
labelNumber++;
});
await controller.forward(from: 0.0);
animatedIndex = -1;
}
}
FloorPlanWithScaledDraggableButtons
class FloorPlanWithScaledDraggableButtons extends StatefulWidget {
#override
State<FloorPlanWithScaledDraggableButtons> createState() => _FloorPlanWithScaledDraggableButtonsState();
}
class _FloorPlanWithScaledDraggableButtonsState extends State<FloorPlanWithScaledDraggableButtons> {
final chips = <ChipEntry>[];
final transformationController = TransformationController();
int labelNumber = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('1) long press on the floor plan below to add a new button\n'
'2) long press on the added button to drag it'),
),
Expanded(
child: ClipRect(
child: GestureDetector(
onLongPressStart: _addButton,
child: InteractiveViewer(
minScale: 1,
maxScale: 5,
constrained: false,
transformationController: transformationController,
child: Stack(
children: [
// https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/Sample_Floorplan.jpg/640px-Sample_Floorplan.jpg
Image.asset('images/640px-Sample_Floorplan.jpg'),
...chips.mapIndexed(_button),
],
),
),
),
),
),
],
);
}
Widget _button(int index, ChipEntry chip) {
return DraggableChip(
chip: chip,
onTap: () => print('button |${chip.label}| at index $index pressed'),
onDrag: (delta) => setState(() => chip.offset += _scaled(delta)),
onDeleted: () => setState(() => chips.removeAt(index)),
);
}
Offset _scaled(Offset delta) {
return delta / transformationController.value.getMaxScaleOnAxis();
}
void _addButton(LongPressStartDetails details) {
setState(() {
final chipEntry = ChipEntry(
offset: transformationController.toScene(details.localPosition),
label: 'btn #$labelNumber'
);
chips.add(chipEntry);
labelNumber++;
});
}
}
class DraggableChip extends StatefulWidget {
const DraggableChip({
Key? key,
required this.chip,
this.onTap,
this.onDrag,
this.onDeleted,
}) : super(key: key);
final ChipEntry chip;
final VoidCallback? onTap;
final Function(Offset)? onDrag;
final VoidCallback? onDeleted;
#override
State<DraggableChip> createState() => _DraggableChipState();
}
class _DraggableChipState extends State<DraggableChip> with SingleTickerProviderStateMixin {
late final controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 250));
bool drag = false;
Offset position = Offset.zero;
double scale = 0;
#override
void initState() {
super.initState();
controller.forward();
}
#override
void didUpdateWidget(covariant DraggableChip oldWidget) {
super.didUpdateWidget(oldWidget);
scale = controller.value = 1;
}
#override
Widget build(BuildContext context) {
final child = RawChip(
selected: drag,
showCheckmark: false,
selectedColor: Colors.teal,
backgroundColor: Colors.orange,
side: const BorderSide(width: 1, color: Colors.black12),
elevation: 4,
onDeleted: () async {
await controller.reverse();
widget.onDeleted?.call();
},
label: GestureDetector(
onLongPressStart: (d) => setState(() {
drag = true;
position = d.globalPosition;
}),
onLongPressMoveUpdate: (d) {
widget.onDrag?.call(d.globalPosition - position);
position = d.globalPosition;
},
onLongPressEnd: (d) => setState(() => drag = false),
child: InkWell(
onTap: widget.onTap,
child: Text(widget.chip.label),
),
),
);
return Positioned(
left: widget.chip.offset.dx,
top: widget.chip.offset.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: ScaleTransition(
scale: controller,
child: child,
),
),
);
}
}

UI does not get updated when using `Notifier`

I basically have a four-page sample app that I'm just getting started with, and I developed a custom bottom navigation bar. However, when I switch pages (using PageView and Riverpod) or tap on the BlurBottomNav buttons, it changes pages but the BlurBottomNav UI doesn't get updated. I am using flutter_riverpod: ^2.1.1
What I have tried-
I have tried to convert NavButton from stateless to consumer and get ref.watch there
I have tried StateNotifier instead of Notifier
Update: Same Code works when using ChangeNotifier with ChangeNotifierProvider but it doesn't with Notifier
Here are snippets
pageview_widget.dart
import 'package:demo/src/constants/nav_items.dart';
import 'package:demo/src/features/categories/presentation/categories_page.dart';
import 'package:demo/src/features/favourites/presentation/fav_page.dart';
import 'package:demo/src/features/pageview/data/pageview_service.dart';
import 'package:demo/src/features/pageview/presentation/bottom_nav/bottom_nav.dart';
import 'package:demo/src/features/settings/presentation/settings_page.dart';
import 'package:demo/src/features/wallpaper/presentation/wall_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PageViewWidget extends StatefulWidget {
const PageViewWidget({super.key});
#override
State<PageViewWidget> createState() => _PageViewWidgetState();
}
class _PageViewWidgetState extends State<PageViewWidget> {
final pageController = PageController(initialPage: 0);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Breezy'),
),
body: Consumer(
builder: (context, ref, child) {
final pageviewService = ref.watch(pageviewServiceProvider.notifier);
final pageviewServiceRead = ref.watch(pageviewServiceProvider);
return Stack(
children: [
PageView.builder(
itemCount: 4,
controller: pageController,
onPageChanged: (int index) {
pageviewService.onPageChanged(index);
// pageviewService.changeShowBNB(true);
},
itemBuilder: (context, index) {
switch (index) {
case 0:
return const WallpaperList();
case 1:
return const CategoriesPage();
case 2:
return const FavouritesPage();
case 3:
return const SettingsPage();
default:
// Should never get hit.
return CircularProgressIndicator(
color: Theme.of(context).primaryColor,
);
}
}),
Positioned.fill(
bottom: 20,
child: SafeArea(
child: BlurBottomNav(
onItemSelected: (int value) {
pageviewService.onTapByBnb(value, pageController);
},
selectedIndex: pageviewServiceRead.currentindex,
items: navItems,
),
),
),
],
);
},
));
}
}
here is pageview_service.dart
import 'package:demo/src/features/pageview/domain/pageview_navigation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PageViewService extends Notifier<PageViewNavigation> {
#override
PageViewNavigation build() {
return PageViewNavigation(
currentindex: 0, changedByBNB: false, showBNB: true);
}
void changeShowBNB(bool value) {
state.showBNB = value;
}
void changeIndex(int index) {
state.currentindex = index;
}
void _changeBNBBool(bool value) {
state.changedByBNB = value;
}
void onPageChanged(int index) {
if ((index != state.currentindex) && (!state.changedByBNB)) {
changeIndex(index);
} else {
_changeBNBBool(false);
}
}
void onTapByBnb(int index, PageController pageController) {
if (index != state.currentindex) {
if (pageController.hasClients) {
_changeBNBBool(true);
changeIndex(index);
pageController.animateToPage(
index,
curve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 350),
);
}
}
}
}
final pageviewServiceProvider =
NotifierProvider<PageViewService, PageViewNavigation>(PageViewService.new);
here is bottom_nav.dart
import 'dart:ui';
import 'package:demo/src/features/pageview/domain/nav_button.dart';
import 'package:demo/src/features/pageview/presentation/bottom_nav/nav_buttons.dart';
import 'package:flutter/material.dart';
class BlurBottomNav extends StatelessWidget {
final List<BottomNavBarItem> items;
final ValueChanged<int> onItemSelected;
final int selectedIndex;
const BlurBottomNav({
super.key,
required this.items,
required this.onItemSelected,
required this.selectedIndex,
});
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.65,
height: 57,
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
height: 100,
color: Colors.red,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items.map((item) {
var index = items.indexOf(item);
return GestureDetector(
onTap: () => onItemSelected(index),
child: NavButton(
item: item,
isSelected: index == selectedIndex,
),
);
}).toList(),
),
),
),
),
),
),
);
}
}
here is bottom_nav_button.dart
import 'dart:ui';
import 'package:demo/src/features/pageview/domain/nav_button.dart';
import 'package:flutter/material.dart';
class NavButton extends StatelessWidget {
final BottomNavBarItem item;
final bool isSelected;
const NavButton({
super.key,
required this.item,
required this.isSelected,
});
#override
Widget build(BuildContext context) {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(15),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 100,
width: 43,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.purple,
width: 2,
),
color: isSelected ? item.activeIconColor : Colors.white,
),
child: item.icon,
),
),
),
],
);
}
}
and lastly here is BottomNavBarItem
import 'package:flutter/material.dart';
class BottomNavBarItem {
BottomNavBarItem({
required this.icon,
required this.activeIconColor,
});
final Widget icon;
final Color activeIconColor;
}
I haven't used flutter in a while, so please be gentle with me
Keep in mind that you can't use mutable state like this in your Notifier subclasses:
state.showBNB = value;
Instead it should be:
state = /* new state */
If you have a custom state class, make sure all properties are final and add a copyWith method.

How to update child StateFulWidget value using parent stateful widget

I have two separate widgets. I want to update the child widget textFormField value when I click on the button in the parent widget.
I have provided the code below. How can I do this without getX or Provider in flutter? I looked for a solution to this problem but did not find a solution for this kind of problem.
Parent Widget
FutureBuilder(
future: SupervisorAttendanceServices.getAttendancesDetailsList(
widget.attendanceId),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
var data = snapshot.data['labour'];
return ListView.builder(
itemCount: data.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return LabourAttendanceWidget(
workerId: data[index]['worker_id'],
masterAttendanceId: widget.attendanceId,
name: data[index]['worker_name'],
wages: data[index]['attendance_worker_wages'],
isPrensent: data[index]
['attendance_worker_presense']
.toString());
});
} else if (snapshot.hasError) {
return const Center(
child: Text("Something went wrong !"),
);
} else {
return const Center(child: LinearProgressIndicator());
}
},
),
CHILD WIDGET
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:get/get.dart';
import 'package:site_management/supervisors/screens/supervisor_attendance/controller/labour_attendance_controller.dart';
import 'package:site_management/supervisors/supervisor_services/supervisor_attendance_services.dart';
class LabourAttendanceWidget extends StatefulWidget {
const LabourAttendanceWidget({
Key? key,
required this.name,
required this.wages,
required this.isPrensent,
required this.workerId,
required this.masterAttendanceId,
}) : super(key: key);
final int workerId;
final int masterAttendanceId;
final String name;
final String wages;
final String isPrensent;
#override
State<LabourAttendanceWidget> createState() => _LabourAttendanceWidgetState();
}
class _LabourAttendanceWidgetState extends State<LabourAttendanceWidget> {
final TextEditingController _wagesController = TextEditingController();
String _character = "";
Timer? searchOnStoppedTyping;
LabourAttendanceController attendanceController =
Get.put(LabourAttendanceController());
_onChangeHandler(value) {
const duration = Duration(
milliseconds:
800); // set the duration that you want call search() after that.
if (searchOnStoppedTyping != null) {
setState(() => searchOnStoppedTyping?.cancel()); // clear timer
}
setState(() =>
searchOnStoppedTyping = Timer(duration, () => submitWages(value)));
}
submitWages(value) {
SupervisorAttendanceServices.storeWorkerWages(
widget.workerId, value, widget.masterAttendanceId);
}
#override
void initState() {
super.initState();
_character = widget.isPrensent;
_wagesController.text = widget.wages;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Card(
child: Column(children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
width: 10,
height: 50,
),
const Icon(FeatherIcons.user),
const SizedBox(
width: 20,
),
Text(
widget.name,
style: const TextStyle(fontSize: 18),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
SizedBox(
width: 150,
height: 60,
child: TextFormField(
controller: _wagesController,
onChanged: _onChangeHandler,
decoration: const InputDecoration(
// border: OutlineInputBorder(),
hintText: "Wages",
prefixIcon: Icon(
Icons.wallet,
color: Colors.blue,
)),
)),
Row(
children: [
Radio(
value: "P",
groupValue: _character,
fillColor:
MaterialStateColor.resolveWith((states) => Colors.green),
onChanged: (selectedValue) {
setState(() {
_character = selectedValue.toString();
SupervisorAttendanceServices.changeAttendance(
widget.workerId,
_character,
widget.masterAttendanceId)
.then((response) {
if (response == 1) {
return null;
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Row(
children: const [
Icon(FeatherIcons.home),
SizedBox(
width: 10,
),
Text("Something went wrong !"),
],
),
),
// sb
);
}
});
});
attendanceController
.getAttendanceCount(widget.masterAttendanceId);
},
),
const Text("P"),
Radio(
value: "A",
fillColor:
MaterialStateColor.resolveWith((states) => Colors.red),
groupValue: _character,
onChanged: (selectedValue) {
setState(() {
_wagesController.text = "0";
_onChangeHandler("0");
_character = selectedValue.toString();
SupervisorAttendanceServices.changeAttendance(
widget.workerId,
_character,
widget.masterAttendanceId);
});
attendanceController
.getAttendanceCount(widget.masterAttendanceId);
}),
const Text("A"),
],
)
],
)
]),
);
}
}
First change your LabourAttendanceWidget to this:
class LabourAttendanceWidget extends StatefulWidget {
const LabourAttendanceWidget({
Key? key,
required this.name,
required this.wages,
required this.isPrensent,
required this.workerId,
required this.masterAttendanceId,
this.someString,
}) : super(key: key);
final int workerId;
final int masterAttendanceId;
final String name;
final String wages;
final String isPrensent;
final String someString;
#override
State<LabourAttendanceWidget> createState() => _LabourAttendanceWidgetState();
}
then in LabourAttendanceWidget's initState do this:
#override
void initState() {
super.initState();
_character = widget.isPrensent;
_wagesController.text = widget.someString ?? widget.wages;
setState(() {});
}
and in your parent widget first define this variable out of build method:
String? _value;
then do this:
return LabourAttendanceWidget(
workerId: data[index]['worker_id'],
masterAttendanceId: widget.attendanceId,
name: data[index]['worker_name'],
wages: data[index]['attendance_worker_wages'],
someString: _value,
isPrensent: data[index]
['attendance_worker_presense']
.toString());
then fill _value when came back from pop up and then call setstate.

Implement a function to create dropdown butttons in flutter

im new to dart programming i want to implement a function to create dropdown buttons.
Contitons are:
-Dropdown button value should be taken as a variable.
Example function call:
buildDropdownField(dropdownHeader:"tank 1", dropdownValue:_tank2)
expected output of this function is to create a dropdown button and store its value into _tank2 so i can use it later on. So far ive tried different methods but i cant seem to get it right.
Heres my final function which does not display the value on screen but saves it tho the value given.
Widget buildDropdownField(
{required String dropdownHeader,
required String? dropdownValue,
required VoidCallback? OnChanged(Value)}) {
return Column(
children: <Widget>[
Text(dropdownHeader),
const SizedBox(
height: 10,
),
StatefulBuilder(
builder: (_, setDropState) {
return DropdownButton<String>(
value: dropdownValue,
icon: const Icon(Icons.arrow_downward),
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (value){
OnChanged(value);
},
items: <String>['-', 'Geçti', 'Kaldı', 'Belirsiz']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
},
)
],
);
}
And lastly heres the function call of my final function iteration:
buildDropdownField(dropdownHeader:"tank 1", dropdownValue:_tank2, OnChanged: (Value) {
setState(() {
tank1 = Value;
});
})
This is a custom expansion tile I used in a project of mine. It comes from the code from a package, slightly to my needs.
import 'package:flutter/material.dart';
const Duration _kExpand = Duration(milliseconds: 200);
class CustomExpansionTile extends StatefulWidget {
const CustomExpansionTile({
Key key,
this.leading,
#required this.title,
this.backgroundColor,
this.children = const <Widget>[],
this.trailing,
#required this.expandedItem,
#required this.expansionCallback,
#required this.onHeaderClick,
this.subtitleWidget,
}) : super(key: key);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
final Widget leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
final Widget subtitleWidget;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
final Color backgroundColor;
/// A widget to display instead of a rotating arrow icon.
final Widget trailing;
final ValueNotifier<Key> expandedItem;
final Function(bool hasExpanded) expansionCallback;
final Function() onHeaderClick;
#override
_CustomExpansionTileState createState() => _CustomExpansionTileState();
}
class _CustomExpansionTileState extends State<CustomExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.5);
final ColorTween _borderColorTween = ColorTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
AnimationController _controller;
Animation<double> _iconTurns;
Animation<double> _heightFactor;
Animation<Color> _borderColor;
Animation<Color> _headerColor;
Animation<Color> _iconColor;
Animation<Color> _backgroundColor;
bool _isExpanded = false;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_controller.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded = widget.expandedItem.value == widget.key;
if (_isExpanded) _controller.value = 1.0;
widget.expandedItem.addListener(listener);
}
void listener() {
setState(() {
_changeState(widget.expandedItem.value == widget.key);
});
}
#override
void dispose() {
_controller.dispose();
widget.expandedItem.removeListener(listener);
super.dispose();
}
void _changeState(bool isExpanded) {
setState(() {
_isExpanded = isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
}
//action to perform when the "expand" icon is pressed
void _onExpansionIconClick() {
widget.expansionCallback(!_isExpanded);
_changeState(!_isExpanded);
widget.expandedItem.value = _isExpanded ? widget.key : null;
}
Widget _buildChildren(BuildContext context, Widget child) {
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
return Container(
decoration: BoxDecoration(
color: _backgroundColor.value ?? Colors.transparent,
border: Border(
top: BorderSide(color: borderSideColor),
bottom: BorderSide(color: borderSideColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTileTheme.merge(
iconColor: _iconColor.value,
textColor: _headerColor.value,
child: ListTile(
//if the list isExpanded, the click on the tile is disabled
//otherwise the action defined for the callback
//is performed
onTap: () => _isExpanded ? null : widget.onHeaderClick(),
leading: widget.leading,
title: widget.title,
subtitle: widget.subtitleWidget,
//arrow icon
trailing: InkWell(
onTap: _onExpansionIconClick,
child: Container(
padding: EdgeInsets.all(16),
//rotate arrow icon if the tile is expanded
child: widget.trailing ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
contentPadding: EdgeInsets.only(left: 16),
),
),
ClipRect(
child: Align(
heightFactor: _heightFactor.value,
child: child,
),
),
],
),
);
}
#override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
_borderColorTween..end = theme.dividerColor;
_headerColorTween
..begin = theme.textTheme.subtitle1.color
..end = theme.accentColor;
_iconColorTween
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColorTween..end = widget.backgroundColor;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : Column(children: widget.children),
);
}
}

SliverAppBar doesn't fully collapse with short list

I'm using Flutter's SliverAppBar with SliverList.
The app bar works fine with long list, that scroll a lot.
However, if I have medium size list with 8 items the app bar only partially collapses until the list runs out of items. It makes logical sense that the scroll stops because there are no more items but it leaves a really nasty effect on the app bar, which is being left in the middle of its transition in to collapsed toolbar.
Is there a way to force the list to scroll up until the toolbar is fully collapsed?
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: Text("Start!"),
icon: Icon(Icons.play_arrow),
backgroundColor: Colors.orange,
elevation: 12,
onPressed: () {
Routing.navigate(
context,
LoggingPage(
workout: _workout,
exercises: workoutExercises,
),
);
},
),
body: CustomScrollView( physics: ScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
actions: <Widget>[
MaterialButton(onPressed: (){}, child: Text("data"),)
],
expandedHeight: 300,
pinned: true,
floating: true,
snap: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(_workout.name),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
buildSliverListItem,
childCount: workoutExercises.length,
),
),
],
),
);
}
We can use NestedScrollView, SliverOverlapAbsorber and SliverOverlapInjector based on Code With Andrea's example here
https://github.com/bizz84/slivers_demo_flutter/blob/master/lib/pages/nested_scroll_view_page.dart.
We use a NestedScrollView. First, we embed the SliverAppbar within the headerSliverBuilder with a SliverOverlapAbsorber above the SliverAppBar.
Next, we embed the CustomScrollView within a builder inside the body of the NestedScrollView. We add a SliverOverlapInjector at the top of the body.
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
return <Widget>[
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
pinned: true,
//floating: true,
stretch: true,
expandedHeight: 300.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: const Text('Weather Report'),
background: Image.asset(
'assets/images/logo1.jpg',
fit: BoxFit.cover,
),
),
),
),
];
},
body: SafeArea(
child: Builder(
builder:(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber above.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i){
return ListTile(
leading: Icon(Icons.wb_sunny),
title: i%2 != 1 ? Text('Sunday ${i+1}') : Text('Monday ${i+1}'),
subtitle: Text('sunny, h: 80, l: 65'),
);
},
childCount: 5
),
),
],
);
}
),
),
),
);
As a workaround you can use widget with custom RenderSliver. It must be added to the end of a slivers list.
class CustomRenderSliver extends RenderSliver {
final double _minHeaderHeight;
final double _maxHeaderHeight;
CustomRenderSliver(this._minHeaderHeight, this._maxHeaderHeight);
#override
void performLayout() {
double maxHeight = this.constraints.viewportMainAxisExtent -
(this.constraints.precedingScrollExtent - _maxHeaderHeight) /*height of regular list elements*/ -
_minHeaderHeight;
if (maxHeight < 0.0) maxHeight = 0.0;
this.geometry = SliverGeometry(scrollExtent: maxHeight);
}
}
list with custom sliver
Complete code
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
final minHeaderHeight = 90.0;
final maxHeaderHeight = 180.0;
List<Widget> slivers = List.generate(4, (index) {
return SliverToBoxAdapter(
child: SizedBox(
height: 100,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Container(
color: Colors.blue,
),
),
),
);
});
// header
slivers.insert(
0, SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: minHeaderHeight,
maxHeight: maxHeaderHeight
),
));
// custom sliver
slivers.add(CustomRenderSliverWidget(minHeaderHeight, maxHeaderHeight));
runApp(MaterialApp(
home: CustomScrollView(
slivers: slivers,
),
));
}
class CustomRenderSliverWidget extends LeafRenderObjectWidget {
final double _minHeaderHeight;
final double _maxHeaderHeight;
CustomRenderSliverWidget(this._minHeaderHeight, this._maxHeaderHeight);
#override
RenderObject createRenderObject(BuildContext context) {
return CustomRenderSliver(_minHeaderHeight, _maxHeaderHeight);
}
}
class CustomRenderSliver extends RenderSliver {
final double _minHeaderHeight;
final double _maxHeaderHeight;
CustomRenderSliver(this._minHeaderHeight, this._maxHeaderHeight);
#override
void performLayout() {
double maxHeight = this.constraints.viewportMainAxisExtent -
(this.constraints.precedingScrollExtent - _maxHeaderHeight) /*height of regular list elements*/ -
_minHeaderHeight;
if (maxHeight < 0.0) maxHeight = 0.0;
this.geometry = SliverGeometry(scrollExtent: maxHeight);
}
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
#required this.minHeight,
#required this.maxHeight,
});
final double minHeight;
final double maxHeight;
#override
double get minExtent => minHeight;
#override
double get maxExtent => max(maxHeight, minHeight);
#override
Widget build(BuildContext context, double shrinkOffset,
bool overlapsContent) {
return Container(color: Colors.red,);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight;
}
}

Categories

Resources