How to get height of a Widget? - flutter

I don't understand how LayoutBuilder is used to get the height of a Widget.
I need to display the list of Widgets and get their height so I can compute some special scroll effects. I am developing a package and other developers provide widget (I don't control them). I read that LayoutBuilder can be used to get height.
In very simple case, I tried to wrap Widget in LayoutBuilder.builder and put it in the Stack, but I always get minHeight 0.0, and maxHeight INFINITY. Am I misusing the LayoutBuilder?
EDIT: It seems that LayoutBuilder is a no go. I found the CustomSingleChildLayout which is almost a solution.
I extended that delegate, and I was able to get the height of widget in getPositionForChild(Size size, Size childSize) method. BUT, the first method that is called is Size getSize(BoxConstraints constraints) and as constraints, I get 0 to INFINITY because I'm laying these CustomSingleChildLayouts in a ListView.
My problem is that SingleChildLayoutDelegate getSize operates like it needs to return the height of a view. I don't know the height of a child at that moment. I can only return constraints.smallest (which is 0, the height is 0), or constraints.biggest which is infinity and crashes the app.
In the docs it even says:
...but the size of the parent cannot depend on the size of the child.
And that's a weird limitation.

To get the size/position of a widget on screen, you can use GlobalKey to get its BuildContext to then find the RenderBox of that specific widget, which will contain its global position and rendered size.
Just one thing to be careful of: That context may not exist if the widget is not rendered. Which can cause a problem with ListView as widgets are rendered only if they are potentially visible.
Another problem is that you can't get a widget's RenderBox during build call as the widget hasn't been rendered yet.
But what if I need to get the size during the build! What can I do?
There's one cool widget that can help: Overlay and its OverlayEntry.
They are used to display widgets on top of everything else (similar to stack).
But the coolest thing is that they are on a different build flow; they are built after regular widgets.
That have one super cool implication: OverlayEntry can have a size that depends on widgets of the actual widget tree.
Okay. But don't OverlayEntry requires to be rebuilt manually?
Yes, they do. But there's another thing to be aware of: ScrollController, passed to a Scrollable, is a listenable similar to AnimationController.
Which means you could combine an AnimatedBuilder with a ScrollController, it would have the lovely effect to rebuild your widget automatically on a scroll. Perfect for this situation, right?
Combining everything into an example:
In the following example, you'll see an overlay that follows a widget inside ListView and shares the same height.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final controller = ScrollController();
OverlayEntry sticky;
GlobalKey stickyKey = GlobalKey();
#override
void initState() {
if (sticky != null) {
sticky.remove();
}
sticky = OverlayEntry(
builder: (context) => stickyBuilder(context),
);
SchedulerBinding.instance.addPostFrameCallback((_) {
Overlay.of(context).insert(sticky);
});
super.initState();
}
#override
void dispose() {
sticky.remove();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
if (index == 6) {
return Container(
key: stickyKey,
height: 100.0,
color: Colors.green,
child: const Text("I'm fat"),
);
}
return ListTile(
title: Text(
'Hello $index',
style: const TextStyle(color: Colors.white),
),
);
},
),
);
}
Widget stickyBuilder(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (_,Widget child) {
final keyContext = stickyKey.currentContext;
if (keyContext != null) {
// widget is visible
final box = keyContext.findRenderObject() as RenderBox;
final pos = box.localToGlobal(Offset.zero);
return Positioned(
top: pos.dy + box.size.height,
left: 50.0,
right: 50.0,
height: box.size.height,
child: Material(
child: Container(
alignment: Alignment.center,
color: Colors.purple,
child: const Text("^ Nah I think you're okay"),
),
),
);
}
return Container();
},
);
}
}
Note:
When navigating to a different screen, call following otherwise sticky would stay visible.
sticky.remove();

This is (I think) the most straightforward way to do this.
Copy-paste the following into your project.
UPDATE: using RenderProxyBox results in a slightly more correct implementation, because it's called on every rebuild of the child and its descendants, which is not always the case for the top-level build() method.
NOTE: This is not exactly an efficient way to do this, as pointed by Hixie here. But it is the easiest.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
typedef void OnWidgetSizeChange(Size size);
class MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
OnWidgetSizeChange onChange;
MeasureSizeRenderObject(this.onChange);
#override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance!.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class MeasureSize extends SingleChildRenderObjectWidget {
final OnWidgetSizeChange onChange;
const MeasureSize({
Key? key,
required this.onChange,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return MeasureSizeRenderObject(onChange);
}
#override
void updateRenderObject(
BuildContext context, covariant MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}
Then, simply wrap the widget whose size you would like to measure with MeasureSize.
var myChildSize = Size.zero;
Widget build(BuildContext context) {
return ...(
child: MeasureSize(
onChange: (size) {
setState(() {
myChildSize = size;
});
},
child: ...
),
);
}
So yes, the size of the parent cannot can depend on the size of the child if you try hard enough.
Personal anecdote - This is handy for restricting the size of widgets like Align, which likes to take up an absurd amount of space.

Here's a sample on how you can use LayoutBuilder to determine the widget's size.
Since LayoutBuilder widget is able to determine its parent widget's constraints, one of its use case is to be able to have its child widgets adapt to their parent's dimensions.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var dimension = 40.0;
increaseWidgetSize() {
setState(() {
dimension += 20;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(children: <Widget>[
Text('Dimension: $dimension'),
Container(
color: Colors.teal,
alignment: Alignment.center,
height: dimension,
width: dimension,
// LayoutBuilder inherits its parent widget's dimension. In this case, the Container in teal
child: LayoutBuilder(builder: (context, constraints) {
debugPrint('Max height: ${constraints.maxHeight}, max width: ${constraints.maxWidth}');
return Container(); // create function here to adapt to the parent widget's constraints
}),
),
]),
),
floatingActionButton: FloatingActionButton(
onPressed: increaseWidgetSize,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Demo
Logs
I/flutter (26712): Max height: 40.0, max width: 40.0
I/flutter (26712): Max height: 60.0, max width: 60.0
I/flutter (26712): Max height: 80.0, max width: 80.0
I/flutter (26712): Max height: 100.0, max width: 100.0
Update: You can also use MediaQuery to achieve similar function.
#override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size ;
if (screenSize.width > layoutSize){
return Widget();
} else {
return Widget(); /// Widget if doesn't match the size
}
}

Let me give you a widget for that
class SizeProviderWidget extends StatefulWidget {
final Widget child;
final Function(Size) onChildSize;
const SizeProviderWidget(
{Key? key, required this.onChildSize, required this.child})
: super(key: key);
#override
_SizeProviderWidgetState createState() => _SizeProviderWidgetState();
}
class _SizeProviderWidgetState extends State<SizeProviderWidget> {
#override
void initState() {
///add size listener for first build
_onResize();
super.initState();
}
void _onResize() {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
if (context.size is Size) {
widget.onChildSize(context.size!);
}
});
}
#override
Widget build(BuildContext context) {
///add size listener for every build uncomment the fallowing
///_onResize();
return widget.child;
}
}
EDIT
Just wrap the SizeProviderWidget with OrientationBuilder to make it respect the orientation of the device

I made this widget as a simple stateless solution:
class ChildSizeNotifier extends StatelessWidget {
final ValueNotifier<Size> notifier = ValueNotifier(const Size(0, 0));
final Widget Function(BuildContext context, Size size, Widget child) builder;
final Widget child;
ChildSizeNotifier({
Key key,
#required this.builder,
this.child,
}) : super(key: key) {}
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
notifier.value = (context.findRenderObject() as RenderBox).size;
},
);
return ValueListenableBuilder(
valueListenable: notifier,
builder: builder,
child: child,
);
}
}
Use it like this
ChildSizeNotifier(
builder: (context, size, child) {
// size is the size of the text
return Text(size.height > 50 ? 'big' : 'small');
},
)

If I understand correctly, you want to measure the dimension of some arbitrary widgets, and you can wrap those widgets with another widget. In that case, the method in the this answer should work for you.
Basically the solution is to bind a callback in the widget lifecycle, which will be called after the first frame is rendered, from there you can access context.size. The catch is that you have to wrap the widget you want to measure within a stateful widget. And, if you absolutely need the size within build() then you can only access it in the second render (it's only available after the first render).

findRenderObject() returns the RenderBox which is used to give the size of the drawn widget and it should be called after the widget tree is built, so it must be used with some callback mechanism or addPostFrameCallback() callbacks.
class SizeWidget extends StatefulWidget {
#override
_SizeWidgetState createState() => _SizeWidgetState();
}
class _SizeWidgetState extends State<SizeWidget> {
final GlobalKey _textKey = GlobalKey();
Size textSize;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => getSizeAndPosition());
}
getSizeAndPosition() {
RenderBox _cardBox = _textKey.currentContext.findRenderObject();
textSize = _cardBox.size;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Size Position"),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
"Currern Size of Text",
key: _textKey,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
SizedBox(
height: 20,
),
Text(
"Size - $textSize",
textAlign: TextAlign.center,
),
],
),
);
}
}
Output:

There is no direct way to calculate the size of the widget, so to find that we have to take the help of the context of the widget.
Calling context.size returns us the Size object, which contains the height and width of the widget. context.size calculates the render box of a widget and returns the size.
Checkout https://medium.com/flutterworld/flutter-how-to-get-the-height-of-the-widget-be4892abb1a2

In cases where you don't want to wait for a frame to get the size, but want to know it before including it in your tree:
The simplest way is to follow the example of the BuildOwner documentation.
With the following you can just do
final size = MeasureUtil.measureWidget(MyWidgetTree());
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// Small utility to measure a widget before actually putting it on screen.
///
/// This can be helpful e.g. for positioning context menus based on the size they will take up.
///
/// NOTE: Use sparingly, since this takes a complete layout and sizing pass for the subtree you
/// want to measure.
///
/// Compare https://api.flutter.dev/flutter/widgets/BuildOwner-class.html
class MeasureUtil {
static Size measureWidget(Widget widget, [BoxConstraints constraints = const BoxConstraints()]) {
final PipelineOwner pipelineOwner = PipelineOwner();
final _MeasurementView rootView = pipelineOwner.rootNode = _MeasurementView(constraints);
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> {
final BoxConstraints boxConstraints;
_MeasurementView(this.boxConstraints);
#override
void performLayout() {
assert(child != null);
child!.layout(boxConstraints, parentUsesSize: true);
size = child!.size;
}
#override
void debugAssertDoesMeetConstraints() => true;
}
This creates an entirely new render tree separate from the main one, and wont be shown on your screen.
So for example
print(
MeasureUtil.measureWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.abc),
SizedBox(
width: 100,
),
Text("Moin Meister")
],
),
),
),
);
Would give you Size(210.0, 24.0)

Might be this could help
Tested on Flutter: 2.2.3
Copy Below code this in your project.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class WidgetSize extends StatefulWidget {
final Widget child;
final Function onChange;
const WidgetSize({
Key? key,
required this.onChange,
required this.child,
}) : super(key: key);
#override
_WidgetSizeState createState() => _WidgetSizeState();
}
class _WidgetSizeState extends State<WidgetSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance!.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
declare a variable to store Size
Size mySize = Size.zero;
Add following code to get the size:
child: WidgetSize(
onChange: (Size mapSize) {
setState(() {
mySize = mapSize;
print("mySize:" + mySize.toString());
});
},
child: ()

This is Remi's answer with null safety, since the edit queue is full, I have to post it here.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
final controller = ScrollController();
OverlayEntry? sticky;
GlobalKey stickyKey = GlobalKey();
#override
void initState() {
sticky?.remove();
sticky = OverlayEntry(
builder: (context) => stickyBuilder(context),
);
SchedulerBinding.instance
.addPostFrameCallback((_) => Overlay.of(context)?.insert(sticky!));
super.initState();
}
#override
void dispose() {
sticky?.remove();
super.dispose();
}
#override
Widget build(BuildContext context) => Scaffold(
body: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
if (index == 6) {
return Container(
key: stickyKey,
height: 100.0,
color: Colors.green,
child: const Text("I'm fat"),
);
}
return ListTile(
title: Text(
'Hello $index',
style: const TextStyle(color: Colors.white),
),
);
},
),
);
Widget stickyBuilder(BuildContext context) => AnimatedBuilder(
animation: controller,
builder: (_, Widget? child) {
final keyContext = stickyKey.currentContext;
if (keyContext != null) {
final box = keyContext.findRenderObject() as RenderBox;
final pos = box.localToGlobal(Offset.zero);
return Positioned(
top: pos.dy + box.size.height,
left: 50.0,
right: 50.0,
height: box.size.height,
child: Material(
child: Container(
alignment: Alignment.center,
color: Colors.purple,
child: const Text("Nah I think you're okay"),
),
),
);
}
return Container();
},
);
}

use the package: z_tools.
The steps:
1. change main file
void main() async {
runZoned(
() => runApp(
CalculateWidgetAppContainer(
child: Center(
child: LocalizedApp(delegate, MyApp()),
),
),
),
onError: (Object obj, StackTrace stack) {
print('global exception: obj = $obj;\nstack = $stack');
},
);
}
2. use in function
_Cell(
title: 'cal: Column-min',
callback: () async {
Widget widget1 = Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 100,
height: 30,
color: Colors.blue,
),
Container(
height: 20.0,
width: 30,
),
Text('111'),
],
);
// size = Size(100.0, 66.0)
print('size = ${await getWidgetSize(widget1)}');
},
),

The easiest way is to use MeasuredSize it's a widget that calculates the size of it's child in runtime.
You can use it like so:
MeasuredSize(
onChange: (Size size) {
setState(() {
print(size);
});
},
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
);
You can find it here: https://pub.dev/packages/measured_size

It's easy and still can be done in StatelessWidget.
class ColumnHeightWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final scrollController = ScrollController();
final columnKey = GlobalKey();
_scrollToCurrentProgress(columnKey, scrollController);
return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: Column(
children: [],
),
),
);
}
void _scrollToCurrentProgress(GlobalKey<State<StatefulWidget>> columnKey,
ScrollController scrollController) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final RenderBox renderBoxRed =
columnKey.currentContext.findRenderObject();
final height = renderBoxRed.size.height;
scrollController.animateTo(percentOfHeightYouWantToScroll * height,
duration: Duration(seconds: 1), curve: Curves.decelerate);
});
}
}
in the same manner you can calculate any widget child height and scroll to that position.

**Credit to #Manuputty**
class OrigChildWH extends StatelessWidget {
final Widget Function(BuildContext context, Size size, Widget? child) builder;
final Widget? child;
const XRChildWH({
Key? key,
required this.builder,
this.child,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (context, orientation) {
return ChildSizeNotifier(builder: builder);
});
}
}
class ChildSizeNotifier extends StatelessWidget {
final ValueNotifier<Size> notifier = ValueNotifier(const Size(0, 0));
final Widget Function(BuildContext context, Size size, Widget? child) builder;
final Widget? child;
ChildSizeNotifier({
Key? key,
required this.builder,
this.child,
}) : super(key: key);
#override
Widget build(BuildContext context) {
WidgetsBinding.instance!.addPostFrameCallback(
(_) {
notifier.value = (context.findRenderObject() as RenderBox).size;
},
);
return ValueListenableBuilder(
valueListenable: notifier,
builder: builder,
child: child,
);
}
}
**Simple to use:**
OrigChildWH(
builder: (context, size, child) {
//Your child here: mine:: Container()
return Container()
})

Related

Flutter: Modify Hero child between two screens

I have two screens, in the first screen i have an image, let's call original image, when i tap a button, i navigate to the second screen with a hero animation over the image. in the second screen i will cut or paint (draw lines) the original image and then i save this image with the RepaintBoundary Widget. My question is how i can modify the hero child (with original image) in the first screen, so that when i make the pop in the second screen, the Hero animation happens normally with the modified image in both screens.
class Screen1 extends StatefulWidget {
final Uint8List bytes;
const Screen1({Key? key, required this.bytes}) : super(key: key);
#override
State<Screen1> createState() => _Screen1State();
}
class _Screen1State extends State<Screen1> {
late final ValueNotifier<ImageProvider> _imageNotifier; /// With Image()
// late final ValueNotifier<Uint8List> _imageNotifier; /// With Image.memory()
#override
void initState() {
super.initState();
_imageNotifier = ValueNotifier(Image.memory(widget.bytes).image);
// _imageNotifier = ValueNotifier(widget.bytes);
}
#override
void dispose() {
_imageNotifier.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder<ImageProvider>(
valueListenable: _imageNotifier,
builder: (_, image, __) {
return Hero(
tag: 'image',
child: Image(image: image),
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.draw),
onPressed: () => Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (_, __, ___) => Screen2(imageNotifier: _imageNotifier))
),
),
);
}
}
class Screen2 extends StatefulWidget {
final ValueNotifier<ImageProvider> imageNotifier;
const Screen2({Key? key, required this.imageNotifier}) : super(key: key);
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final _key = GlobalKey();
static const _duration = Duration(milliseconds: 100);
Future<void> _takeSnapshot() async {
final boundary = _key.currentContext!.findRenderObject() as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 2.0);
image.toByteData(format: ui.ImageByteFormat.png).then((byteData) {
final imageProvider = Image.memory(byteData!.buffer.asUint8List()).image;
widget.imageNotifier.value = imageProvider;
/// Delay for update first screen
Future.delayed(_duration, () => Navigator.of(context).pop());
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RepaintBoundary(
key: _key,
child: Stack(
children: [
/// ORIGINAL IMAGE
ValueListenableBuilder<ImageProvider>(
valueListenable: widget.imageNotifier,
builder: (_, image, __) {
return Hero(
tag: 'image',
child: Image(image: image)
);
}
),
/// LINES WITH CUSTOM PAINTER
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.camera),
onPressed: _takeSnapshot,
),
);
}
}
The code above shows a minimal example of what i want to achieve, I dont put the code to draw the lines because it is significantly long, but the point is that i take a snapshot of the stack widget and update the notifier.
I don't know if to paint the image it is better to use an Image.memory (Uint8List) or Image (ImageProvider), this will be the type of the ValueNotifier.
The points above are some ideas that i have to achive the target, but I would like to know if there is a better way that you know.
Thanks!

How to use dots_indicator in Flutter Web?

I am using dots_indicator 2.0.0 in Flutter and it works perfect for IOS and Android. However, I am testing on the web and it does not work (It does not allow scrolling with the mouse from one side to the other and does not show any error messages).
My stateful code related to dots_indicator is as follows...
Initstate...
#override
void initState() {
_pageController.addListener(() {
if (_pageController.page!.round() != currentIndex) {
currentIndex = _pageController.page;
print('Current Index: $currentIndex');
setState(() {});
}
});
super.initState();
}
Building pages...
Widget _crearPages(BuildContext context) {
List<Widget> _pages = [];
_pages.add(Page1());
_pages.add(Page2());
_pages.add(Page3());
_pages.add(Page4());
return Stack(
children: [
PageView(
children: _pages,
controller: _pageController,
onPageChanged: (int page) {
currentIndex = _pageController.page;
},
),
_dotsIndicator(context, _pages.length),
],
);
}
Dots Indicator...
Widget _dotsIndicator(BuildContext context, int dotsCount) {
final _size = MediaQuery.of(context).size;
return Positioned(
bottom: 11.0,
width: 200.0,
left: (_size.width-200.0)/2,
child: DotsIndicator(
dotsCount: dotsCount,
position: currentIndex!,
decorator: DotsDecorator(
activeColor: (dotsCount == 1) ? Colors.transparent : Theme.of(context).primaryColor,
),
),
);
}
Is dots_indicator really compatible with Flutter Web?
How can I make dots_indicator work on the web?
you need to create custom scroll behavior class like that
class MyCustomScrollBehavior extends MaterialScrollBehavior {
#override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
and add scrollBehavior property to MaterialApp widget
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(), // <- this one
home: HomePage(), // your widget
);
}
}
also read this https://docs.flutter.dev/release/breaking-changes/default-scroll-behavior-drag

RepaintBoundary with a StreamBuilder

I thought I understood RepaintBoundary but now I don't.
Background
I wrote this answer describing how you can add a RepaintBoundary around a widget that has to draw a lot to prevent other parts of the widget tree from redrawing. That worked as expected.
Problem now
I'm trying to make a real life example now where the widget is being rebuilt inside a StreamBuilder based on an audio player stream. I tried wrapping the whole StreamBuilder in a RepaintBoundary like this:
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
But the rest of the UI is still repainting (except the play/pause button which I also wrapped in a RepaintBoundary).
The build method of that ProgressBarWidget looks like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
}
But if I remove the StreamBuilder like this:
#override
Widget build(BuildContext context) {
print('building progress bar');
return ProgressBar(
progress: Duration.zero,
total: Duration(minutes: 5),
onSeek: (duration) {
_player.seek(duration);
},
);
}
Then the repaint boundary works again when I manually move the thumb.
What is it about the StreamBuilder that makes the RepaintBoundary not work?
Full code
The full code for the widget layout is here:
import 'package:flutter/material.dart';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/rendering.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
void main() {
debugRepaintTextRainbowEnabled = true;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: HomeWidget(),
);
}
}
class HomeWidget extends StatefulWidget {
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
AudioPlayer _player;
final url = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3';
Stream<DurationState> _durationState;
#override
void initState() {
super.initState();
_player = AudioPlayer();
_durationState = Rx.combineLatest2<Duration, PlaybackEvent, DurationState>(
_player.positionStream,
_player.playbackEventStream,
(position, playbackEvent) => DurationState(
progress: position,
buffered: playbackEvent.bufferedPosition,
total: playbackEvent.duration,
));
_init();
}
Future<void> _init() async {
try {
await _player.setUrl(url);
} catch (e) {
print("An error occured $e");
}
}
#override
Widget build(BuildContext context) {
print("building app");
return Scaffold(
body: Column(
children: [
Spacer(),
RepaintBoundary(
child: ProgressBarWidget(
durationState: _durationState, player: _player),
),
RepaintBoundary(
child: PlayPauseButton(player: _player),
),
],
),
);
}
}
class ProgressBarWidget extends StatelessWidget {
const ProgressBarWidget({
Key key,
#required Stream<DurationState> durationState,
#required AudioPlayer player,
}) : _durationState = durationState,
_player = player,
super(key: key);
final Stream<DurationState> _durationState;
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building progress bar');
return StreamBuilder<DurationState>(
stream: _durationState,
builder: (context, snapshot) {
final durationState = snapshot.data;
final progress = durationState?.progress ?? Duration.zero;
final buffered = durationState?.buffered ?? Duration.zero;
final total = durationState?.total ?? Duration.zero;
return ProgressBar(
progress: progress,
buffered: buffered,
total: total,
onSeek: (duration) {
_player.seek(duration);
},
);
},
);
// ProgressBar(
// progress: Duration.zero,
// total: Duration(minutes: 5),
// onSeek: (duration) {
// _player.seek(duration);
// },
// );
}
}
class PlayPauseButton extends StatelessWidget {
const PlayPauseButton({
Key key,
#required AudioPlayer player,
}) : _player = player,
super(key: key);
final AudioPlayer _player;
#override
Widget build(BuildContext context) {
print('building play/pause button');
return StreamBuilder<PlayerState>(
stream: _player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: _player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: _player.pause,
);
} else {
return IconButton(
icon: Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => _player.seek(Duration.zero),
);
}
},
);
}
}
class DurationState {
const DurationState({this.progress, this.buffered, this.total});
final Duration progress;
final Duration buffered;
final Duration total;
}
The whole project is on GitHub.
When you don't have the StreamBuilder and drag in the ProgressBar, it will probably just repaint itself and not require a relayout.
When the StreamBuilder gets a new event from the stream, it rebuilds ProgressBar. Depending on the details of ProgressBar, when it gets rebuild it will also require a relayout (perhaps it contains a layout builder). Since it is in a Column and the Column uses the size of it children during layout (to determine the position of the next child), then Column has to do it layout again as well, which might cause its children to need a repaint.
Play around with this: You'll notice that marking Foo to repaint (horizontal drag) only causes Foo to repaint (when it is wrapped with a RepaintBoundary). Marking Foo for relayout (a tap) will also cause the Column to relayout and repaint. When the LayoutBuilder is present (which causes a relayout when it is rebuild), you'll see that a rebuild of Foo (by vertical drag) also causes the Column to repaint.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => Column(
children: [
Container(
height: 400,
color: Color(0x11ff0000),
),
RepaintBoundary(
child: Foo(),
),
],
);
}
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
#override
Widget build(BuildContext context) => GestureDetector(
onHorizontalDragUpdate: (_) => context.findRenderObject().markNeedsPaint(),
onTap: () => context.findRenderObject().markNeedsLayout(),
onVerticalDragUpdate: (_) => setState(() {}),
child: LayoutBuilder(
builder: (context, _) => Container(
height: 100,
width: 100.0,
color: Color(0xff002200),
),
),
);
}
This is a supplemental answer to tell how specifically I solved the problem after getting #spkersten's help.
The ProgressBar widget was rebuilding internally whenever the text labels would change. My first attempt at solving the problem was to wrap the widget in a SizedBox with a fixed height and width. This did work in that it prevented the rest of the screen from needing relayout or repainting. However, it was difficult to know what the height of the progress bar was going to be before laying it out.
So my second solution was to paint the text manually rather than use Text widgets. That way I could refrain from calling markNeedsLayout when the text changed. This solved the problem.
My current implementation of the progress bar is here.

How to expand a widget in ListView ontap?

I have created a ListView with container boxes as widgets. I want a specific container to expand onTap upto a specific screen height and width. I need help in implementing this in flutter. I have made a prototype on AdobeXD.
AdobeXD Prototype GIF
I am new to flutter, any kind of help is appreciated.
A flutter plugin called flutter swiper might help you achieve what you want to achieve.
Visit this pub dev and you can read documentation.
Here you go brother, Although its not blurring the background but I think it will get you going.
It's working something like this:
Below the code which you can copy paste. I have added comments in the code for understanding it in better way. Cheers :)
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeApp(),
);
}
}
class HomeApp extends StatefulWidget {
#override
_HomeAppState createState() => _HomeAppState();
}
class _HomeAppState extends State<HomeApp> {
// Items in the list --> Custom Widgets
List<Widget> arr = [
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
ListContainerHere(),
];
Widget getListWidget(List<Widget> items) {
List<Widget> list = new List<Widget>();
for (var i = 0; i <= items.length; i++) {
list.add(new ListContainerHere(
index: i,
));
}
return Row(children: list);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter App :)"),
),
body: Center(
// Using a 'Row' as Horizontal ListView
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, child: getListWidget(arr)),
),
);
}
}
// Widgets that will be rendered in the Horizontal Row
class ListContainerHere extends StatefulWidget {
final int index;
ListContainerHere({this.index});
#override
_ListContainerHereState createState() => _ListContainerHereState();
}
class _ListContainerHereState extends State<ListContainerHere> {
// Varibale to change the height and width accordingly
// Initally no item will be expanded
bool isExpanded = false;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
// Changing the value of 'isExpanded' when an item is tapped in the List
setState(() {
isExpanded = !isExpanded;
});
},
// AnimatedContainer for slowing down the changing
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
// Changing the width and height
height: isExpanded ? 250 : 150,
width: isExpanded ? 250 : 150,
// Decoration Portion of the Container
decoration: BoxDecoration(
color: Colors.blue, borderRadius: BorderRadius.circular(15.0)),
),
),
);
}
}

How to display scrolling index for scrollController in flutter?

I would like to display the index at the bottom of the listView by utilizing the scrollController, the same way it's displayed in the follow image :
After the user scrolls down, or scrolls up, the count on the left, highlighted by red, gets increased/decreased based on the scroll direction of the user.
What I want to achieve is to automatically update the displayed item's index, indicated by red on the picture. So whenever the user scrolls down or up, this index gets updated by the displayed item's index.
The picture shows that I have reached the 26th item. Whenever I scroll down or up, this index gets updated.
I have tried using the offset that is getting emitted for the scrolling event with no luck.
The way is using the scroll controller like you were doing.
You need to use a known item size and a listener.
// Declaring the controller and the item size
ScrollController _scrollController;
final itemSize = 100.0;
// Initializing
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
// Your list widget (must not be nested lists)
ListView.builder(
controller: _scrollController,
itemCount: <Your list length>,
itemExtent: itemSize,
itemBuilder: (context, index) {
return ListTile(<your items>);
},
),
// With the listener and the itemSize, you can calculate which item
// is on screen using the provided callBack. Something like this:
void _scrollListener() {
setState(() {
var index = (_scrollController.offset / itemSize).round() + 1;
});
}
Adding a listener to a scrollController will call the callback provided every time the list is scrolled. You can handle many behaviours of the list using the same logic, including identifying the type of event that fired the listener, the direction of the scroll, etc.
There is a lib called scroll to index that could help you. You could take the $index to show inside your toast message. Example bellow is from the lib's author:
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scroll To Index Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Scroll To Index Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const maxCount = 100;
final random = math.Random();
final scrollDirection = Axis.vertical;
AutoScrollController controller;
List<List<int>> randomList;
#override
void initState() {
super.initState();
controller = AutoScrollController(
viewportBoundaryGetter: () => Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
axis: scrollDirection
);
randomList = List.generate(maxCount, (index) => <int>[index, (1000 * random.nextDouble()).toInt()]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
scrollDirection: scrollDirection,
controller: controller,
children: randomList.map<Widget>((data) {
return Padding(
padding: EdgeInsets.all(8),
child: _getRow(data[0], math.max(data[1].toDouble(), 50.0)),
);
}).toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Increment',
child: Text(counter.toString()),
),
);
}
int counter = -1;
Future _scrollToIndex() async {
setState(() {
counter++;
if (counter >= maxCount)
counter = 0;
});
await controller.scrollToIndex(counter, preferPosition: AutoScrollPosition.begin);
controller.highlight(counter);
}
Widget _getRow(int index, double height) {
return _wrapScrollTag(
index: index,
child: Container(
padding: EdgeInsets.all(8),
alignment: Alignment.topCenter,
height: height,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightBlue,
width: 4
),
borderRadius: BorderRadius.circular(12)
),
child: Text('index: $index, height: $height'),
)
);
}
Widget _wrapScrollTag({int index, Widget child})
=> AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
child: child,
highlightColor: Colors.black.withOpacity(0.1),
);
}
https://medium.com/flutter-community/create-shop-list-with-flutter-d13d3c20d68b
Maybe this one can help you. Also source code is available. A simple math about item height might help.