flutter : Create scrollable organic gridview with dynamic height - flutter

I'm trying to create a gridview which works like Stagarredgridview.count but without using any package, i've created it using CustomMultichildLayout but i can't make it scrollable. I know i can use any scrollable like singlechildscrollview or listview for this.
The main problem is infinite height which i can't seem to resolve, i've tried layout builder,expanded and flexible widgets for constraints even boxconstraints from Container but can't resolve infinite height.
Here's the code
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Column(
children: [
Expanded(
child: OrganicGrid(count: 50),
),
],
),
);
}
}
class OrganicGrid extends StatelessWidget {
final int count;
const OrganicGrid({
required this.count,
super.key,
});
#override
Widget build(BuildContext context) {
return CustomGrid(
children: List.generate(count, (index) => ContainerWidget(id: index + 1)),
);
}
}
class ContainerWidget extends StatelessWidget {
final int id;
const ContainerWidget({
required this.id,
super.key,
});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(
height: 50,
decoration: const BoxDecoration(
color: Colors.green,
// border: Border.all(color: Colors.red, width: 2),
),
child: Center(
child: Text(
"Text $id",
style: TextStyle(color: Colors.white),
)),
),
);
}
}
class CustomGrid extends StatelessWidget {
final List<Widget> children;
const CustomGrid({required this.children, super.key});
#override
Widget build(BuildContext context) {
List<Widget> rows = [];
bool oddExist = children.length % 2 != 0;
if (oddExist) {
rows.add(LayoutId(id: 0, child: children[0]));
}
if (oddExist) {
for (int i = 1; i < children.length; i++) {
rows.add(LayoutId(id: i, child: children[i]));
}
} else {
for (int i = 1; i <= children.length; i++) {
rows.add(LayoutId(id: i, child: children[i - 1]));
}
}
return CustomMultiChildLayout(
key: key,
delegate: CustomGridDelegate(numRows: rows.length, oddExist: oddExist),
children: rows,
);
}
}
class CustomGridDelegate extends MultiChildLayoutDelegate {
final int numRows;
final bool oddExist;
CustomGridDelegate({required this.numRows, required this.oddExist});
#override
void performLayout(Size size) {
const double padding = 8;
double width = size.width - padding * 3;
double childHeight = 60;
double dy = 0;
double dx = padding;
void childLayout(int i) {
positionChild(i, Offset(dx, dy));
layoutChild(
i,
BoxConstraints(minWidth: width / 2, maxWidth: width / 2),
);
if (i % 2 == 0) {
dy += childHeight;
dx = padding;
} else {
dx = width / 2;
dx += padding * 2;
}
}
void zerothChildLayout(int i) {
positionChild(i, Offset(dx, dy));
layoutChild(
0,
BoxConstraints(minWidth: width + padding),
);
dy += childHeight;
}
if (oddExist) {
zerothChildLayout(0);
for (int i = 1; i < numRows; i++) {
childLayout(i);
}
} else {
for (int i = 1; i <= numRows; i++) {
childLayout(i);
}
}
}
#override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
I've tried layoutbuilder,boxconstraints,expanded,flexible and similar widets without success, the only way it work is by wraping with fixed size container but that is not dynamic.
Any help would be appreciated, you can directly try this code by pasting.

Related

How can I achieve "child of a widget which is inside a scroll widget acting like sticky header" in Flutter?

I'm trying to find a way to implement a functionality in which, in a horizontally scrollable list, there are widgets that I will call P, (which are denoted as P1, P2 and P3 in the diagram) and their children C, (which are denoted as C1, C2 and C3). As the user scrolls the list horizontally, I want C's inside P's to act like sticky headers, until they reach the boundary of their parent.
I'm sorry if the description & diagram is not enough, I will try to clarify anything unclear.
Diagram of the problem
As I'm thinking of a way to implement this, I can't seem to find a plausible solution. Also if there is a package that can help with this issue, I would really appreciate any suggestions.
I am not sure about your picture, but maybe this is do you want?
our tools :
BuildOwners -> to measure size of the widget before rebuild,
NotificationListeners -> to trigger rebuild based on ScrollNotification. i use stateful Widget, but you can tweak it into ValueNotifier and Build the Sticker with ValueListenableBuilder instead.
ListView.Builder -> actually you can replace this with any kind of Scrollable, we only need to listen scroll event.
how its work?
its simple :
we need to know the P dx Offset, check if C offset small than P, then use that value to adjust x Positioned of C in Stack. and clamp it with max value (P.width)
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding + (px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding*2));
}
return widget.stickerHorizontalPadding;
}
full code :
main.dart :
import 'dart:ui';
import 'package:flutter/material.dart';
import 'scrollable_sticker.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
// i use chrome to test it, so igrone this
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown
},
),
home: const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: ScrollableSticker(
children: List.generate(10, (index) => Container(
width: 500,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.orange)),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 50.0),
child: Text(
"P1",
textDirection: TextDirection.ltr,
),
),
)),
stickerBuilder: (index) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Colors.red),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
'C$index',
),
),
)),
),
);
}
}
scrollable_sticker.dart :
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ScrollableSticker extends StatefulWidget {
final List<Widget> children;
final Widget Function(int index) stickerBuilder;
final double stickerHorizontalPadding;
const ScrollableSticker(
{Key? key,
required this.children,
required this.stickerBuilder,
this.stickerHorizontalPadding = 10.0})
: super(key: key);
#override
State<ScrollableSticker> createState() => _ScrollableStickerState();
}
class _ScrollableStickerState extends State<ScrollableSticker> {
late List<GlobalKey> _keys;
late GlobalKey _parentKey;
#override
void initState() {
super.initState();
_keys = List.generate(widget.children.length, (index) => GlobalKey());
_parentKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (sc) {
setState(() {});
return true;
},
child: ListView.builder(
key: _parentKey,
scrollDirection: Axis.horizontal,
itemCount: widget.children.length,
itemBuilder: (context, index) {
final itemSize = measureWidget(Directionality(
textDirection: TextDirection.ltr, child: widget.children[index]));
final stickerSize = measureWidget(Directionality(
textDirection: TextDirection.ltr,
child: widget.stickerBuilder(index)));
final BuildContext? itemContext = _keys[index].currentContext;
double x = widget.stickerHorizontalPadding;
if (itemContext != null) {
final pcontext = _parentKey.currentContext;
Offset? pOffset;
if (pcontext != null) {
RenderObject? obj = pcontext.findRenderObject();
if (obj != null) {
final prb = obj as RenderBox;
pOffset = prb.localToGlobal(Offset.zero);
}
}
final obj = itemContext.findRenderObject();
if (obj != null) {
final rb = obj as RenderBox;
final cx = rb.localToGlobal(pOffset ?? Offset.zero).dx;
x = _calculateStickerXPosition(
px: pOffset != null ? pOffset.dx : 0.0,
cx: cx,
cw: (itemSize.width - stickerSize.width));
}
}
return SizedBox(
key: _keys[index],
height: itemSize.height,
width: itemSize.width,
child: Stack(
children: [
widget.children[index],
Positioned(
top: itemSize.height / 2,
left: x,
child: FractionalTranslation(
translation: const Offset(0.0, -0.5),
child: widget.stickerBuilder(index)))
],
),
);
},
),
);
}
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding +
(px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding * 2));
}
return widget.stickerHorizontalPadding;
}
}
Size measureWidget(Widget widget) {
final PipelineOwner pipelineOwner = PipelineOwner();
final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
final RenderObjectToWidgetElement<RenderBox> element =
RenderObjectToWidgetAdapter<RenderBox>(
container: rootView,
debugShortDescription: '[root]',
child: widget,
).attachToRenderTree(buildOwner);
try {
rootView.scheduleInitialLayout();
pipelineOwner.flushLayout();
return rootView.size;
} finally {
// Clean up.
element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
buildOwner.finalizeTree();
}
}
class MeasurementView extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
#override
void performLayout() {
assert(child != null);
child!.layout(const BoxConstraints(), parentUsesSize: true);
size = child!.size;
}
#override
void debugAssertDoesMeetConstraints() => true;
}
you could try to use c padding dynamically
padding: EdgeInsets.only(left: 0.1 * [index], right: 1 * [index])
for example, I hope it helps.

How to prevent widget from passing out of screen border

i am animating widget by Transform.translate like following
late Offset offsetAll = const Offset(0,0);
Transform.translate(
offset: offsetAll,
child: GestureDetector(
onVerticalDragUpdate: (t){
offsetAll+=t.delta;
setState(() {});
},
child: Container(
height: 100,
padding: const EdgeInsets.all(10),
color: Colors.black54,
),
),
);
i am moving the Container vertically. but the problem is when i move the Container to top or bottom i noticed it could be hidden like following
How could i prevent that ? ..
how can i make it limit .. (if it arrive border so stop move )
i tried to wrap my widget into safeArea but does not work
Edit for Pskink
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Offset offset = Offset.zero;
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (d){
offset = d.localPosition;
setState(() {});
} ,
child: CustomSingleChildLayout(
delegate: FooDelegate(
offset: offset,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(vertical: 20),
),
child: Container(
color: Colors.orange,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text('first line\nsecond line\nthird line'),
),
),
),
),
);
}
}
class FooDelegate extends SingleChildLayoutDelegate {
FooDelegate({
required this.offset,
this.alignment = Alignment.center,
this.padding = EdgeInsets.zero,
}) : super();
final Offset offset;
final Alignment alignment;
final EdgeInsets padding;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
final anchor = alignment.alongSize(childSize);
final effectivePadding = padding + EdgeInsets.fromLTRB(
anchor.dx,
anchor.dy,
childSize.width - anchor.dx,
childSize.height - anchor.dy,
);
final rect = effectivePadding.deflateRect(Offset.zero & size);
return Offset(
offset.dx.clamp(rect.left, rect.right) - anchor.dx,
offset.dy.clamp(rect.top, rect.bottom) - anchor.dy,
);
}
#override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => false;
}
You can use CustomSingleChildLayout widget, which lets you position the child of this widget (the Container in your case) while giving you as input the size of the parent.
Why is this relevant? You ask. Well, you need to know the size of the child and the size of the parent in order to keep the child inside the parent bounds.
For example, if you are moving child to the right, then you want to stop moving at the moment you have: topLeftOfChildContainer.dx = Parent.size.width - child.width - paddingRight
If you want to have an idea how you do the calculations, see this method from the custom_positioned_widget class of the controllable_widgets package which uses CustomSingleChildLayout as explained above:
#override
Offset getPositionForChild(Size size, Size childSize) {
// childSize: size of the content
Offset childTopLeft = offsetBuilder.call(childSize);
if (canGoOffParentBounds) {
// no more checks on the position needed
return childTopLeft;
}
// make sure the child does not go off screen in all directions
// and respects the padding
if (childTopLeft.dx + childSize.width > size.width - padding.right) {
final distance =
-(childTopLeft.dx - (size.width - padding.right - childSize.width));
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dx < padding.left) {
final distance = padding.left - childTopLeft.dx;
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dy + childSize.height > size.height - padding.bottom) {
final distance = -(childTopLeft.dy -
(size.height - padding.bottom - childSize.height));
childTopLeft = childTopLeft.translate(0, distance);
}
if (childTopLeft.dy < padding.top) {
final distance = padding.top - childTopLeft.dy;
childTopLeft = childTopLeft.translate(0, distance);
}
return childTopLeft;
}
Full Working Example (without any package dependencies):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return const Exp3();
}
}
typedef OffsetBuilder = Offset Function(Size size);
class Exp3 extends StatefulWidget {
const Exp3({Key? key}) : super(key: key);
#override
State<Exp3> createState() => _Exp3State();
}
class _Exp3State extends State<Exp3> {
// function that takes size of the child container and returns its new offset based on the size.
// initial offset of the child container is (0, 0).
OffsetBuilder _offsetBuilder = (_) => Offset.zero;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(builder: (context) {
return Container( // parent container
color: Colors.red,
child: GestureDetector(
onPanUpdate: (details) {
// get the current offset builder before we modify it
// because we want to use it in the new offset builder
final currentBuilder = _offsetBuilder;
// create the new offset builder
_offsetBuilder = (Size containerSize) {
// the container size will be passed to you in this function
// you can use it to place your widget
// return the offset you like for the top left of the container
// now we will return the current offset + the delta
// Just be careful if you set canGoOffParentBounds to false, as this will prevent the widget from being painted outside the parent
// but it WILL NOT prevent the offset from being updated to be outside parent, you should handle this in this case, see below:
return currentBuilder.call(containerSize) + details.delta;
};
setState(() {}); // to update the UI (force rerender of the CustomSingleChildLayout)
},
child: CustomSingleChildLayout(
delegate: MyCustomSingleChildLayoutDelegate(
canGoOffParentBounds: false,
padding: const EdgeInsets.all(8.0),
offsetBuilder: _offsetBuilder,
),
child: Container(
width: 100,
height: 100,
color: Colors.yellow,
),
),
),
);
}),
);
}
}
class MyCustomSingleChildLayoutDelegate extends SingleChildLayoutDelegate {
final Offset Function(Size childSize) offsetBuilder;
final EdgeInsets padding;
final bool canGoOffParentBounds;
MyCustomSingleChildLayoutDelegate({
required this.offsetBuilder,
required this.padding,
required this.canGoOffParentBounds,
});
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The content can be at most the size of the parent minus 8.0 pixels in each
// direction.
return BoxConstraints.loose(constraints.biggest).deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
// childSize: size of the content
Offset childTopLeft = offsetBuilder.call(childSize);
if (canGoOffParentBounds) {
// no more checks on the position needed
return childTopLeft;
}
// make sure the child does not go off screen in all directions
// and respects the padding
if (childTopLeft.dx + childSize.width > size.width - padding.right) {
final distance = -(childTopLeft.dx - (size.width - padding.right - childSize.width));
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dx < padding.left) {
final distance = padding.left - childTopLeft.dx;
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dy + childSize.height > size.height - padding.bottom) {
final distance = -(childTopLeft.dy - (size.height - padding.bottom - childSize.height));
childTopLeft = childTopLeft.translate(0, distance);
}
if (childTopLeft.dy < padding.top) {
final distance = padding.top - childTopLeft.dy;
childTopLeft = childTopLeft.translate(0, distance);
}
return childTopLeft;
}
#override
bool shouldRelayout(MyCustomSingleChildLayoutDelegate oldDelegate) {
return oldDelegate.offsetBuilder != offsetBuilder;
}
}
Note: Please note the comment that tells you that you should not update the offsetBuilder if by updating it, the child becomes outside parent bounds, because although the CustomSingleChildLayout will still paint the child inside the parent, but if you update the offsetBuilder anyway inside your stateful widget's state, you will have inconsistent state between the actual rendered container and the offsetBuilder of your state. So you should also check if child is still inside bounds inside the offsetBuilder.
And if you want you can use CustomPositionedWidget of the mentioned package directly.
p.s.: I am the maintainer of the package above.
here is a simple custom SingleChildLayoutDelegate doing the job (of course it can be simplified a bit if you dont need optional alignment / padding parameters):
class FooDelegate extends SingleChildLayoutDelegate {
FooDelegate({
required this.offset,
this.alignment = Alignment.center,
this.padding = EdgeInsets.zero,
}) : super(relayout: offset);
final ValueNotifier<Offset> offset;
final Alignment alignment;
final EdgeInsets padding;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
final anchor = alignment.alongSize(childSize);
final effectivePadding = padding + EdgeInsets.fromLTRB(
anchor.dx,
anchor.dy,
childSize.width - anchor.dx,
childSize.height - anchor.dy,
);
final rect = effectivePadding.deflateRect(Offset.zero & size);
return Offset(
offset.value.dx.clamp(rect.left, rect.right) - anchor.dx,
offset.value.dy.clamp(rect.top, rect.bottom) - anchor.dy,
);
}
#override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => false;
}
test widget:
class Foo extends StatelessWidget {
final offset = ValueNotifier(Offset.zero);
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onPanStart: (d) => offset.value = d.localPosition,
onPanUpdate: (d) => offset.value = d.localPosition,
child: CustomSingleChildLayout(
delegate: FooDelegate(
offset: offset,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(vertical: 20),
),
child: Container(
color: Colors.orange,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text('first line\nsecond line\nthird line'),
),
),
),
);
}
}
EDIT
a less efficient version using setState instead of ValueNotifier:
class Foo extends StatefulWidget {
#override
State<Foo> createState() => _FooState();
}
class _FooState extends State<Foo> {
var offset = Offset.zero;
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onPanStart: (d) => setState(() => offset = d.localPosition),
onPanUpdate: (d) => setState(() => offset = d.localPosition),
child: CustomSingleChildLayout(
delegate: FooDelegate(
offset: offset,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(vertical: 20),
),
child: Container(
color: Colors.orange,
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text('first line\nsecond line\nthird line'),
),
),
),
);
}
}
class FooDelegate extends SingleChildLayoutDelegate {
FooDelegate({
required this.offset,
this.alignment = Alignment.center,
this.padding = EdgeInsets.zero,
});
final Offset offset;
final Alignment alignment;
final EdgeInsets padding;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
final anchor = alignment.alongSize(childSize);
final effectivePadding = padding + EdgeInsets.fromLTRB(
anchor.dx,
anchor.dy,
childSize.width - anchor.dx,
childSize.height - anchor.dy,
);
final rect = effectivePadding.deflateRect(Offset.zero & size);
return Offset(
offset.dx.clamp(rect.left, rect.right) - anchor.dx,
offset.dy.clamp(rect.top, rect.bottom) - anchor.dy,
);
}
#override
bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) => true;
}

How can i set Offset to specific value

i have found this simple full code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Exp3();
}
}
typedef OffsetBuilder = Offset Function(Size size);
class Exp3 extends StatefulWidget {
const Exp3({Key? key}) : super(key: key);
#override
State<Exp3> createState() => _Exp3State();
}
class _Exp3State extends State<Exp3> {
OffsetBuilder _offsetBuilder = (_) => Offset.zero;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(builder: (context) {
return Container( // parent container
color: Colors.red,
child: GestureDetector(
onPanUpdate: (details) {
final currentBuilder = _offsetBuilder;
_offsetBuilder = (Size containerSize) {
return currentBuilder.call(containerSize) + details.delta;
};
setState(() {});
},
child: CustomSingleChildLayout(
delegate: MyCustomSingleChildLayoutDelegate(
canGoOffParentBounds: false,
padding: const EdgeInsets.all(8.0),
offsetBuilder: _offsetBuilder,
),
child: Container(
width: 100,
height: 100,
color: Colors.yellow,
),
),
),
);
}
),
);
}
}
now my widget located at Offset.zero; for first snapshot by it's variable OffsetBuilder _offsetBuilder = (_) => Offset.zero;
Question: How i set OffsetBuilder _offsetBuilder = (_) => Offset.zero; to located at following value by first snapshot or first initialize
last point on the screen (max buttom) - 300
and here the class i use for SingleChildLayoutDelegate
class MyCustomSingleChildLayoutDelegate extends SingleChildLayoutDelegate {
final Offset Function(Size childSize) offsetBuilder;
final EdgeInsets padding;
final bool canGoOffParentBounds;
MyCustomSingleChildLayoutDelegate({
required this.offsetBuilder,
required this.padding,
required this.canGoOffParentBounds,
});
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints.loose(constraints.biggest).deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
Offset childTopLeft = offsetBuilder.call(childSize);
if (canGoOffParentBounds) {
// no more checks on the position needed
return childTopLeft;
}
if (childTopLeft.dx + childSize.width > size.width - padding.right) {
final distance = -(childTopLeft.dx - (size.width - padding.right - childSize.width));
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dx < padding.left) {
final distance = padding.left - childTopLeft.dx;
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dy + childSize.height > size.height - padding.bottom) {
final distance = -(childTopLeft.dy - (size.height - padding.bottom - childSize.height));
childTopLeft = childTopLeft.translate(0, distance);
}
if (childTopLeft.dy < padding.top) {
final distance = padding.top - childTopLeft.dy;
childTopLeft = childTopLeft.translate(0, distance);
}
return childTopLeft;
}
#override
bool shouldRelayout(MyCustomSingleChildLayoutDelegate oldDelegate) {
return oldDelegate.offsetBuilder != offsetBuilder;
}
}

How to measure widget width and check for fit in container

I have a list of Widgets that I want to display in non-scrollable horizontal layout.
In the example below, I have 5 Widgets, 2 of them are fully displayed and the other 3 do not fit, hence the '+3' label.
I do not mind if there is some empty space after the label, but the important thing is that we only display Widgets that fully fit in the row.
I guess the main problem is how can I test that a Widget fits based on it's width into a row?
I thought about VisibilityDetector, but I will ended up rendering all widgets, then removing the ones that are not 100% visible, and the entire logic seems quite flawed for this use case.
Any ideas?
Thanks
try my version:
class Home extends StatefulWidget {
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
// widget
List<GlobalKey> keys = [];
var widgets = List.generate(25, (index) {
GlobalKey globalKey = GlobalKey();
keys.add(globalKey);
return FilterChip(
key: globalKey,
label: Text("asdasasd"),
);
});
return Scaffold(
appBar: AppBar(
title: Text('AppBar'),
),
body: Column(
children: [
OverRow(
keys: keys,
widgets: widgets,
),
],
),
);
}
}
class OverRow extends StatefulWidget {
OverRow({Key key, this.keys, this.widgets}) : super(key: key);
List<GlobalKey> keys = [];
List<Widget> widgets = [];
#override
State<OverRow> createState() => _OverRowState();
}
class _OverRowState extends State<OverRow> {
int overIndex = -1;
#override
void initState() {
// TODO: implement initState
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
for (var i = 0; i < widget.keys.length; i++) {
final box = widget.keys[i].currentContext.findRenderObject() as RenderBox;
final pos = box.localToGlobal(Offset.zero);
var over = pos.dx + box.size.width > MediaQuery.of(context).size.width;
if (over) {
overIndex = i;
setState(() {});
return;
}
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Row(
children: overIndex == -1
? widget.widgets
: [
...widget.widgets.take(overIndex).toList(),
Container(
child: Text("+${widget.widgets.skip(overIndex).length}"),
)
]);
}
}
EDIT: Included dynamic width based in label length
Try this, with defined width of item container and the +label or with dynamic calculating the width based on font size.
Change useFixedWidth to true or false if you want dynamic or static width.
Example in a Stateless widget:
class MyWidget extends StatelessWidget {
final double itemWidth = 100; //maxwidth if item container
final double labelWidth = 40; //maxWidth of +label
List<String> items = List.generate(
10, (index) => 'Name ${index + 1}'); //replace with a real list of items
final bool useFixedWidth =
false; // define if want to calculate dynamic with of each item
final double letterWidth =
12; // defined width of a letter || change depending on font size
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
int totalItems = items.length;
int maxDisplayItems = 0;
List<double> dynamicItemsWidth = []; //if want to use dynamic width
if (useFixedWidth) {
//if want to use fixed width
maxDisplayItems =
(((constraints.maxWidth - labelWidth) / itemWidth).floor()).clamp(
0, totalItems); //check how many items fit including the +label
} else {
//if want to calculate based on string length
dynamicItemsWidth = items
.map((e) => e.length * letterWidth)
.toList(); //calculate individual item width
double _acumWidth = 0.0;
for (var width in dynamicItemsWidth) {
_acumWidth = _acumWidth + width;
if (_acumWidth < (constraints.maxWidth - labelWidth)) {
maxDisplayItems++;
}
}
}
bool showPlusSign =
maxDisplayItems < totalItems; //check if all items can be shown
return Row(children: [
Row(
children: List.generate(maxDisplayItems, (index) {
return SizedBox(
height: itemWidth,
width: useFixedWidth ? itemWidth : dynamicItemsWidth[index],
child: Container(
//color: Colors.red,
//width: itemWidth,
child: Center(child: Text('Name ${index + 1}'))));
}).toList()),
if (showPlusSign)
SizedBox(
width: labelWidth,
height: itemWidth,
child: Center(child: Text('+${totalItems - maxDisplayItems}')))
]);
});
}
}
shows this dynamic layout for 10 items:

How to add moving indicator on multicoloured horzontal bar in flutter

Need a moving indicator to show the value of particular color in horizontal bar
when i move the indicator it should show the respectedcoloured value. Any one help me out.
Here is the code:
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Scaffold(
body: Center(
child: MyAssetsBar(
width: width * .9,
background: colorFromHex("CFD8DC"),
//height: 50,
//radius: 10,
assetsLimit: 250,
//order: OrderType.Descending,
assets: [
MyAsset(size: 30, color: colorFromHex("29B6F6")),
MyAsset(size: 25, color: colorFromHex("E53935")),
MyAsset(size: 70, color: colorFromHex("4CAF50")),
MyAsset(size: 50, color: colorFromHex("8E24AA")),
MyAsset(size: 20, color: colorFromHex("FBC02D"))
],
),
),
);
}
}
/*Utils*/
Color colorFromHex(String hexColor) {
final hexCode = hexColor.replaceAll('#', '');
return Color(int.parse('FF$hexCode', radix: 16));
}
const double _kHeight = 25;
enum OrderType { Ascending, Descending, None }
/*Utils*/
class MyAsset {
final double size;
final Color color;
MyAsset({this.size, this.color});
}
class MyAssetsBar extends StatelessWidget {
MyAssetsBar(
{Key key,
#required this.width,
this.height = _kHeight,
this.radius,
this.assets,
this.assetsLimit,
this.order,
this.background = Colors.grey})
: assert(width != null),
assert(assets != null),
super(key: key);
final double width;
final double height;
final double radius;
final List<MyAsset> assets;
final double assetsLimit;
final OrderType order;
final Color background;
double _getValuesSum() {
double sum = 0;
assets.forEach((single) => sum += single.size);
return sum;
}
void orderMyAssetsList() {
switch (order) {
case OrderType.Ascending:
{
//From the smallest to the largest
assets.sort((a, b) {
return a.size.compareTo(b.size);
});
break;
}
case OrderType.Descending:
{
//From largest to smallest
assets.sort((a, b) {
return b.size.compareTo(a.size);
});
break;
}
case OrderType.None:
default:
{
break;
}
}
}
//single.size : assetsSum = x : width
Widget _createSingle(MyAsset singleAsset) {
return SizedBox(
width: (singleAsset.size * width) / (assetsLimit ?? _getValuesSum()),
child: Container(color: singleAsset.color),
);
}
#override
Widget build(BuildContext context) {
if (assetsLimit != null && assetsLimit < _getValuesSum()) {
print("assetsSum < _getValuesSum() - Check your values!");
return Container();
}
//Order assetsList
orderMyAssetsList();
final double rad = radius ?? (height / 2);
return ClipRRect(
child: Container(
decoration: new BoxDecoration(
color: background,
),
width: width,
height: height,
child: Row(
children: assets
.map(([![enter image description here][2]][2]singleAsset) => _createSingle(singleAsset))
.toList()),
),
);
}`
`
}
List item
actual result :
[1]: https://i.stack.imgur.com/Z6VGN.png
expected result: [2]:
https://i.stack.imgur.com/qQbRB.png