How to measure widget width and check for fit in container - flutter

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:

Related

flutter : Create scrollable organic gridview with dynamic height

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.

Is the build function creating new widgets to render or reusing?

I have a working snippet that I've wrote, but I kinda don't understand how flutter is (re)using the widgets creating in the build method:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(calculateNextOffset());
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
left: offsets[j].dx,
top: offsets[j].dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (int k = 0; k < offsets.length; k++) {
offsets[k] = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
Every time I click on a container, i expect my "offsets" state to be updated, but I also expect all the AnimationPositioned widgets, GestureDetector widgets and the square widgets that you see would be rerendered.
With rerendered i mean they would disappear from the screen and new ones would be rerendered (and the animation from the first widgets would be cancelled and never displayed)
However it works? Could someone explain this to me?
EDIT: I've updated my snippet of code in my question to match what i'm asking, which i'm also going to rephrase here:
Every time I click on a square, i want that square to disappear and all the other square to randomly animate to another position. But every time I click on a square, another random square is deleted, and the one i'm clicking is animating.
I want the square that I click on disappears and the rest will animate.
Following up from the comments - Actually, the square you click is disappears. However, to see this visually add this to your container color:
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
Now, your offsets are generating just fine and random. However, because you have an AnimatedContainer widget. This widget will remember the last x & y position of your square and animate the new square starting from that old x,y value to the new on you passed it. So if you really want the square you click on disappear - you will need to either use Positioned widget:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 100.0;
static const squareHeight = 100.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
offsets.add(calculateNextOffset());
}
print(offsets);
List<Widget> squareWidgets = [];
for (var offset in offsets) {
squareWidgets.add(Positioned(
left: offset.dx,
top: offset.dy,
//curve: Curves.easeIn,
//duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
for (var i = 0; i < offsets.length; i++) {
offsets[i] = calculateNextOffset();
}
offsets.add(calculateNextOffset());
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
If you want the rest of the squares to animate while only the one that is clicked disappears. You will need to rethink your implementation and track all square perhaps using unique keys and custom animations. Hope that helps!
I've finally found it:
In the context of the snippet inside the question: Every time you click on a square, it will correctly remove that item, but the widgets are rerendered from that new list, and the last widget that was previously rendered will be removed instead of the one that I clicked.
This has to do because every widget in the widget tree is rendered as an element inside the element tree. If the state of the element inside the element tree is the same, it will not rerender that one. and they are all just blue squares in the end, so there is no distinction.
You can find a very nice video made by the flutter devs here:
When to Use Keys - Flutter Widgets 101 Ep. 4
Long story short: Here is the snippet with the fix, which is to add a Key to each widget, then the state will change on the element inside the element tree and it will rerender (and remove) the correct widgets/elements:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<OffsetData> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(OffsetData(UniqueKey(), calculateNextOffset()));
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
key: offsets[j].key, // This line is the trick
left: offsets[j].offset.dx,
top: offsets[j].offset.dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (var offsetData in offsets) {
offsetData.offset = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
class OffsetData {
Offset offset;
final Key key;
OffsetData(this.key, this.offset);
}

How to position a CompositedTransformFollower based on its child's size?

I'm using CompositedTransformTarget and CompositedTransformFollower to display an OverlayEntry. How can I position the CompositedTransformFollower relative to the CompositedTransformTarget, i.e. how can I align its bottom to the top-center of the target in order to display it horizontally centered above the target, while maintaining interactivity (i.e. hit tests on the child should work)?
I tried to calculate the offset to give to CompositedTransformFollower, but I cannot do the correct calculation, because at that time I don't have the size of the child.
Sample Code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(brightness: Brightness.light),
home: Scaffold(
appBar: AppBar(title: Text('test')),
body: Center(child: OverlayButton()),
),
);
}
}
class OverlayButton extends StatefulWidget {
#override
_OverlayButtonState createState() => _OverlayButtonState();
}
class _OverlayButtonState extends State<OverlayButton> {
OverlayEntry _overlayEntry;
final LayerLink _layerLink = LayerLink();
bool _overlayIsShown = false;
#override
void dispose() {
super.dispose();
if (_overlayIsShown) {
_hideOverlay();
}
}
void _showOverlay() {
if (_overlayIsShown) return;
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
_overlayIsShown = true;
}
void _hideOverlay() {
_overlayIsShown = false;
_overlayEntry.remove();
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: RaisedButton(child: Text('Open Overlay'), onPressed: _showOverlay),
);
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var anchorSize = renderBox.size;
return OverlayEntry(builder: (context) {
// TODO: dynamically use the correct child width / height for
// positioning us correctly on top + centered on the anchor
var childWidth = 200.0;
var childHeight = 40.0;
var childOffset =
Offset(-(childWidth - anchorSize.width) / 2, -(childHeight));
return Row(
children: <Widget>[
CompositedTransformFollower(
link: _layerLink,
offset: childOffset,
child: RaisedButton(
child: Text('close'),
onPressed: _hideOverlay,
),
),
],
);
});
}
}
General answer:
It looks like we want to know the exact size of a child, before deciding where to place the child. There's a widget made for this purpose, called CustomSingleChildLayout. Also, if you need to layout multiple children, you should check out the slightly more complex version, CustomMultiChildLayout.
For this case:
First you need to insert a CustomSingleChildLayout in the OverlayEntry you are building, for example, I added 2 lines here:
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var anchorSize = renderBox.size;
return OverlayEntry(builder: (context) {
return CompositedTransformFollower(
link: _layerLink,
child: CustomSingleChildLayout( // # Added Line 1 #
delegate: MyDelegate(anchorSize), // # Added Line 2 #
child: RaisedButton(
child: Text('close'),
onPressed: _hideOverlay,
),
),
);
});
}
Then let's create a delegate that handles your business logic. Notice I've also created a parameter to take in your "anchorSize":
class MyDelegate extends SingleChildLayoutDelegate {
final Size anchorSize;
MyDelegate(this.anchorSize);
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// we allow our child to be smaller than parent's constraint:
return constraints.loosen();
}
#override
Offset getPositionForChild(Size size, Size childSize) {
print("my size: $size");
print("childSize: $childSize");
print("anchor size being passed in: $anchorSize}");
// todo: where to position the child? perform calculation here:
return Offset(anchorSize.width, childSize.height / 2);
}
#override
bool shouldRelayout(_) => true;
}
As you can see, in the getPositionForChild method, we are able to gather all the information needed for calculating an offset. After calculating, we can just return that offset and CustomSingleChildLayout will take care of the placement.
To expand on this idea, you probably don't even need CompositedTransformFollower anymore, you can just do a full-screen overlay and calculate the offset that way.

UI works always - but error when checking widget sizes

I want to have a text in a given SizedBox (cannot change). But this text can vary from time to time.
So I coded a class, which displays me the texts centred when it fits in the given size, but scrolls over it when it is too big. But in my code, there is a tiny fraction of a second with an error.
It has to be dynamically displayed as needed.
How my code works:
I build by default a centred text. This is also the point, where I get an overflow error by x pixels.
I check then in an asynchronous method the text size and the given size.
If the text widget is too long, I start scrolling it (AlwaysScrollingText).
The question is now, how do I get the text size without an overflow xxx pixels.
import 'dart:async';
import 'package:flutter/material.dart';
class ScrollingText extends StatefulWidget {
final String text;
final TextStyle style;
final Axis scrollAxis;
final double ratioOfBlankToScreen;
final double width;
ScrollingText({
#required this.text,
#required this.width,
this.style,
this.scrollAxis: Axis.horizontal,
this.ratioOfBlankToScreen: 0.25,
}) : assert(text != null, width != null);
#override
State<StatefulWidget> createState() {
return ScrollingTextState();
}
}
class ScrollingTextState extends State<ScrollingText> {
bool scroll = false;
GlobalKey _sizeKey = GlobalKey();
#override
void didUpdateWidget(ScrollingText oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.text != widget.text) scroll = false;
}
#override
Widget build(BuildContext context) {
checkScroll();
return scroll
? SizedBox(
width: widget.width,
child: AlwaysScrollingText(
text: widget.text,
style: widget.style,
))
: getText();
}
Widget getText() {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.text, style: widget.style, maxLines: 1, key: _sizeKey)
],
),
);
}
checkScroll() async {
await Future.delayed(Duration(milliseconds: 500));
if (_sizeKey.currentContext == null) return;
double _textWidth =
_sizeKey.currentContext.findRenderObject().paintBounds.size.width;
bool scroll = _textWidth > widget.width;
print('$_textWidth > ${widget.width}');
if (scroll != this.scroll)
setState(() {
this.scroll = scroll;
});
}
}
class AlwaysScrollingText extends StatefulWidget {
final String text;
final TextStyle style;
final double ratioOfBlankToScreen;
AlwaysScrollingText({
#required this.text,
this.style,
this.ratioOfBlankToScreen: 0.25,
}) : assert(text != null,);
#override
_AlwaysScrollingTextState createState() => _AlwaysScrollingTextState();
}
class _AlwaysScrollingTextState extends State<AlwaysScrollingText>
with SingleTickerProviderStateMixin {
ScrollController scrollController;
double screenWidth;
double screenHeight;
double position = 0.0;
Timer timer;
final double _moveDistance = 3.0;
final int _timerRest = 100;
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((callback) {
startTimer();
});
}
void startTimer() {
if (_key.currentContext != null) {
double widgetWidth = getSizeFromKey(_key).width;
timer = Timer.periodic(Duration(milliseconds: _timerRest), (timer) {
double maxScrollExtent = scrollController.position.maxScrollExtent;
double pixels = scrollController.position.pixels;
if (pixels + _moveDistance >= maxScrollExtent) {
position = (maxScrollExtent -
screenWidth * widget.ratioOfBlankToScreen +
widgetWidth) /
2 -
widgetWidth +
pixels -
maxScrollExtent;
scrollController.jumpTo(position);
}
position += _moveDistance;
scrollController.animateTo(position,
duration: Duration(milliseconds: _timerRest), curve: Curves.linear);
});
}
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
screenWidth = MediaQuery.of(context).size.width;
screenHeight = MediaQuery.of(context).size.height;
}
Widget getBothEndsChild() {
return Center(
child: Text(
widget.text,
style: widget.style,
maxLines: 1,
));
}
Widget getCenterChild() {
return Container(width: screenWidth * widget.ratioOfBlankToScreen);
}
#override
void dispose() {
super.dispose();
if (timer != null) {
timer.cancel();
}
}
#override
Widget build(BuildContext context) {
return ListView(
key: _key,
scrollDirection: Axis.horizontal,
controller: scrollController,
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
getCenterChild(),
getBothEndsChild(),
],
);
}
Size getSizeFromKey(GlobalKey key) =>
key.currentContext?.findRenderObject()?.paintBounds?.size;
}
Actual result:
https://i.imgur.com/FFewRtB.gif (not enough reputation to post images :c)
You can put your text field inside the expanded widget.Here is link to class.
expanded widget.
And here is code.
Expanded(
child: Text("hello"),
);
The bug was fixed with this little trick:
Replaced bool scroll = _textWidth > widget.width; with bool scroll = _textWidth >= widget.width;
Removed the Row widget in the getText method

How to change all items on Column dependin on which one has been tapped

I'm not sure if how to do this.
I have a Column of AnimatedContainers. Initially all of them are 200 height. I want to implement some kind of callback, so when user tap on one item, this one becomes smaller(say 100 height) and the rest of the item in the list disappear. My AnimatedContainers are Stateful widgets
I guess I would have to use to callbacks, one for the columnn (parent) and the other to notify the children, but I don't know how to do this.
Summarised, what I have right now is
Stateless(Column(Stateful(List<AnimatedContainer>)))
If something is not clear please comment
Thanks
EDIT to add some code and more info
class SectionButton extends StatefulWidget {
double screenHeight;
Stream stream;
int index;
SectionButton(this.screenHeight, this.stream, this.index);
#override
_SectionButtonState createState() => _SectionButtonState();
}
class _SectionButtonState extends State<SectionButton> {
double height;
StreamSubscription streamSubscription;
initState() {
super.initState();
this.height = this.widget.screenHeight / n_buttons;
streamSubscription =
widget.stream.listen((_) => collapse(this.widget.index));
}
void collapse(int i) {
if (this.widget.index != i) {
setState(() {
this.height = 0;
});
} else {
setState(() {
this.height = this.widget.screenHeight / appBarFraction;
});
}
}
#override
dispose() {
super.dispose();
streamSubscription.cancel();
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
height: this.height,
duration: Duration(milliseconds: 300),
);
}
}
class HomeScreen extends StatelessWidget {
List<Widget> sections = [];
bool areCollapsed = false;
final changeNotifier = new StreamController.broadcast();
void createSections(screenHeight) {
for (var i = 0; i < buttonNames.length; i++) {
this.sections.add(GestureDetector(onTap:(){
print("Section i was tapped");
changeNotifier.sink.add(i);}, //THIS IS NOT WORKING
child: SectionButton(screenHeight, changeNotifier.stream, i),),);
}
}
#override
Widget build(BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
double screenHeight =
mediaQueryData.size.height - mediaQueryData.padding.vertical;
createSections(screenHeight);
return SafeArea(
child: SizedBox.expand(
child: Column(children: this.sections)
),
);
}
}
BTW what I'm trying to implement is something like this:
You have to store your height in a variable and wrap your widget with GestureDetector
GestureDetector(
onTap: () {
setState(() { _height = 100.0; });
},
child: Container(
child: Text('Click Me'),
),
)
Alternatively, you can also use AnimatedSize Widget.
Animated widget that automatically transitions its size over a given
duration whenever the given child's size changes.