Related
I would like to have a widget that takes any number of children widgets and displays whatever widget is selected. A widget is selected by index of the widget in the children list.
In order to select a widget, I create a stateful widget whose builder method returns the selected child. The ChildByIndexState allows some other widget to access and update the selected child index.
class ChildByIndex extends StatefulWidget {
const ChildByIndex({
Key? key,
required this.index,
required this.children,
}) : super(key: key);
final int index;
final List<Widget> children;
#override
State<ChildByIndex> createState() => ChildByIndexState();
}
class ChildByIndexState extends State<ChildByIndex> {
late int _index = widget.index;
int get index => _index;
/// Update the state if the index is different from the current index.
set index(int value) {
if (value != _index) setState(() => _index = value);
}
#override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: SizedBox(key: ValueKey(_index), child: widget.children[_index]),
);
}
}
So that takes care of switching widgets with a nice animation, next I need to tackle the dynamic resizing based on the selected widget's intrinsic size. The demonstration widget uses a AnimatedSize widget to animate size transitions and a GlobalKey<ChildByIndexState> to set the index.
class Resizer extends StatelessWidget {
const Resizer({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final children = <Widget>[
Container(color: Colors.red, width: 100, height: 100),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
SizedBox(height: 100, child: Card(color: Colors.purple)),
SizedBox(height: 100, child: Card(color: Colors.pink)),
],
),
Container(color: Colors.green, width: 250, height: 150),
];
final childByIndexKey = GlobalKey<ChildByIndexState>();
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Positioned.fill(
child: Center(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
reverseDuration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
child: ChildByIndex(
key: childByIndexKey,
index: 0,
children: children,
),
),
),
),
/// Sets the index of [ChildByIndex] and wraps around to the first
/// child index when the index is past at the final child.
Positioned(
width: 64.0,
height: 64.0,
top: 8.0,
left: 8.0,
child: FloatingActionButton(
onPressed: () {
final childByIndexState = childByIndexKey.currentState;
if (childByIndexState == null) return;
childByIndexState.index =
(childByIndexState.index + 1) % children.length;
},
child: const Icon(Icons.ac_unit),
),
),
],
),
),
);
}
}
Issue
The AnimatedSize widget only works when going from a smaller size to a larger size. It skips (no animation) when going from a larger size to a smaller size. How can I fix this?
AnimatedContainer needs a width and height to animate towards, but I don't want to specify anything (the goal is intrinsic size) and the sizes are not available until the build method completes.
Solution:
A custom MultiChildRenderObjectWidget that uses a selectedIndex parameter to find a matching child from its children and then sets its size based on a SizeTween animation that begins from the size of the previously selected child (if any) and ends at the size of the newly selected child.
Output:
Considerations:
The following is an incomplete list of features that could be implemented. I leave them as an exercise to anyone who wishes to use the code.
This could be optimized by using a List<Widget> instead of the linked-list like child access provided by the ContainerRenderObjectMixin.
The child switching process could be done with a builder method, or a mix of previous constructed methods and builders for certain children.
Custom transitions can be parameterized through paint callbacks or objects that handle the painting of the widget.
Source: The source code is fairly well documented and can be read without further explanation.
The AnimatedSizeSwitcherParentData is not necessary for this current implementation. It will be useful for adding additional behavior, such as animating the position/offset of the currently selected child.
/// Every child of a [MultiChildRenderObject] has [ParentData].
/// Since this is a container-type of widget, we can used the predefined
/// [ContainerBoxParentData] which allows children to have access to their
/// previous and next sibling.
///
/// * See [ContainerBoxParentData].
/// * See [ContainerParentDataMixin].
class AnimatedSizeSwitcherParentData extends ContainerBoxParentData<RenderBox> {}
The IntrinsicSizeSwitcher and its related RenderIntrinsicSizeSwitcher provide the functionality.
/// A widget sizes itself according to the size of its selected child.
class IntrinsicSizeSwitcher extends MultiChildRenderObjectWidget {
IntrinsicSizeSwitcher({
Key? key,
// Since this is a [MultiChildRenderObjectWidget], we pass the children
// to the super constructor.
required AnimationController animationController,
required Curve curve,
required int selectedIndex,
required List<Widget> children,
}) : assert(selectedIndex >= 0),
_animationController = animationController,
_animation = CurvedAnimation(parent: animationController, curve: curve),
_selectedIndex = selectedIndex,
super(key: key, children: children);
final AnimationController _animationController;
final CurvedAnimation _animation;
final int _selectedIndex;
#override
RenderObject createRenderObject(BuildContext context) {
// The custom [RenderObject] that we define to create our custom widget.
return RenderIntrinsicSizeSwitcher(
animationController: _animationController,
animation: _animation,
selectedIndex: _selectedIndex,
);
}
#override
void updateRenderObject(
BuildContext context,
RenderIntrinsicSizeSwitcher renderObject,
) {
/// The [RenderObject] is updated when [selectedIndex] changes, so that
/// the old child can be replaced by the new child.
renderObject.selectedIndex = _selectedIndex;
}
}
class RenderIntrinsicSizeSwitcher extends RenderBox
with ContainerRenderObjectMixin<RenderBox, AnimatedSizeSwitcherParentData> {
RenderIntrinsicSizeSwitcher({
required AnimationController animationController,
required CurvedAnimation animation,
required int selectedIndex,
}) : _animationController = animationController,
_animation = animation,
_selectedIndex = selectedIndex {
_onLayout = _onFirstLayout;
/// Listen to animation changes so that the layout of this [RenderBox] can
/// be adjusted according to [animationController.value].
animationController.addListener(
() {
if (_lastValue != animationController.value) markNeedsLayout();
},
);
}
final AnimationController _animationController;
final CurvedAnimation _animation;
/// A [SizeTween] whose [begin] is the size of the previous child, and
/// whose [end] is the size of the next child.
final SizeTween _sizeTween = SizeTween();
/// Called by [performLayout]. This method is initialized to [_onFirstLayout]
/// so that the first child can be found by index and laid out.
///
/// After [_onFirstLayout] completes, additional layout passes have two
/// possibilities: the selected child changed or the current child is being
/// used to update the size of this [RenderBox].
late void Function() _onLayout;
int _selectedIndex;
int get selectedIndex => _selectedIndex;
set selectedIndex(int value) {
assert(selectedIndex >= 0);
/// Update [_selectedIndex] if it is different from [value], because this
/// method restarts [_animationController], which calls [markNeedsLayout].
if (_selectedIndex == value) return;
_selectedIndex = value;
/// No need to call [markNeedsLayout] because this [RenderBox] is a
/// listener of [_animationController].
///
/// The listener callback calls [markNeedsLayout] whenever [_lastValue]
/// differs from [_animationController.value] in order to use the new
/// animation value to update the layout.
_animationController.forward(from: .0);
}
#override
bool get sizedByParent => false;
Pair<RenderBox?, Size>? _oldSelection;
Pair<RenderBox?, Size>? _selection;
double? _lastValue;
#override
void setupParentData(covariant RenderObject child) {
if (child.parentData is AnimatedSizeSwitcherParentData) return;
child.parentData = AnimatedSizeSwitcherParentData();
}
Pair<RenderBox?, Size> _findSelectedChildAndDryLayout() {
var child = firstChild;
var index = 0;
/// Find the child matching [selectedIndex].
while (child != null) {
if (index == selectedIndex) {
return Pair(first: child, second: child.computeDryLayout(constraints));
}
var childParentData = child.parentData as AnimatedSizeSwitcherParentData;
child = childParentData.nextSibling;
++index;
}
/// No matching child was found.
return const Pair(first: null, second: Size.zero);
}
/// Find the child corresponding to [selectedIndex] and perform a wet layout.
Pair<RenderBox?, Size> _findSelectedChildAndWetLayout() {
var child = firstChild;
var index = 0;
while (child != null) {
if (index == selectedIndex) {
return Pair(
first: child,
second: wetLayoutSizeComputer(child, constraints),
);
}
var childParentData = child.parentData as AnimatedSizeSwitcherParentData;
child = childParentData.nextSibling;
++index;
}
return const Pair(first: null, second: Size.zero);
}
#override
void performLayout() {
_lastValue = _animationController.value;
_onLayout();
}
#override
Size computeDryLayout(BoxConstraints constraints) {
final child = _selection?.first;
if (child == null) return Size.zero;
return child.getDryLayout(constraints);
}
#override
double computeMaxIntrinsicWidth(double height) {
final child = _selection?.first;
if (child == null) return .0;
return child.getMaxIntrinsicWidth(height);
}
#override
double computeMaxIntrinsicHeight(double width) {
final child = _selection?.first;
if (child == null) return .0;
return child.getMaxIntrinsicHeight(width);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final child = _selection?.first;
/// If there is no current child, then there is nothing to hit test.
if (child == null) return false;
var childParentData = child.parentData as BoxParentData;
/// This [RenderBox] only displays one child at a time, so it only needs to
/// hit test the child being displayed.
final isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed);
},
);
return isHit;
}
#override
void paint(PaintingContext context, Offset offset) {
final child = _selection?.first;
/// If there is no current child, then there is nothing to paint.
if (child == null) return;
/// Clip the painted area to [size], which is set to the value of
/// [animatedSize] during layout; this prevents having to resizing the old
/// child which can cause visual overflows.
context.pushClipRect(
true,
offset,
Offset.zero & size,
(PaintingContext context, Offset offset) {
/// The animation dependent alpha value is used to fade out the old
/// child and fade in the current child.
final alpha = (_animationController.value * 255).toInt();
final oldChild = _oldSelection?.first;
/// If there is an old child, paint it first, so that it is painted
/// below the current child.
///
/// We only want to paint the old child if the animation is running.
/// Once the animation is completed, the old child is fully transparent.
/// Subsequently, it is no longer necessary to paint it.
if (oldChild != null && _animationController.isAnimating) {
context.pushOpacity(
offset,
255 - alpha,
(PaintingContext context, Offset offset) {
final childOffset = (oldChild.parentData as BoxParentData).offset;
context.paintChild(oldChild, childOffset + offset);
},
);
}
context.pushOpacity(
offset,
alpha,
(PaintingContext context, Offset offset) {
final childOffset = (child.parentData as BoxParentData).offset;
context.paintChild(child, childOffset + offset);
},
);
},
);
}
#override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout);
final child = _selection?.first;
if (child == null) return null;
final result = child.getDistanceToActualBaseline(baseline);
if (result == null) return null;
return result + (child.parentData as BoxParentData).offset.dy;
}
/// Calculates the animated size of this [RenderBox].
void _performLayout(RenderBox child) {
final childParentData = child.parentData as AnimatedSizeSwitcherParentData;
final animatedSize = _sizeTween.evaluate(_animation)!;
final childSize = wetLayoutSizeComputer(child, constraints);
final oldChild = _oldSelection?.first;
if (oldChild != null) {
final oldChildSize = wetLayoutSizeComputer(oldChild, constraints);
final oldChildParentData = oldChild.parentData as BoxParentData;
/// Center the old child.
oldChildParentData.offset = Offset(
(animatedSize.width - oldChildSize.width) / 2.0,
(animatedSize.height - oldChildSize.height) / 2.0,
);
}
/// Center the new child.
childParentData.offset = Offset(
(animatedSize.width - childSize.width) / 2.0,
(animatedSize.height - childSize.height) / 2.0,
);
size = animatedSize;
}
void _onFirstLayout() {
/// The first layout pass must find the selected child by index and perform
/// a wet layout.
final selectedChild = _findSelectedChildAndWetLayout();
/// If [selection.first] is null, then the child list is empty, so there's
/// nothing to lay out, and [Size.zero] is returned.
if (selectedChild.first == null) {
size = Size.zero;
return;
}
/// Since this is the first pass, [_sizeTween.begin] and [_sizeTween.end]
/// will just be set to the size of the currently selected child.
_sizeTween.begin = _sizeTween.end = selectedChild.second;
/// The selection is updated to the child matching [selectedChildIndex].
_selection = selectedChild;
/// Subsequent layout passes are ensured to have a selected child.
_onLayout = _onUpdateLayout;
/// The size is set to the size of the selected child.
size = selectedChild.second;
}
void _onUpdateLayout() {
/// A dry layout is needed just to get the size of the selected child.
final newSelection = _findSelectedChildAndDryLayout();
/// After [_onFirstLayout], it is safe to assume that [_selection] is not
/// null and that the child that has gone through the wet layout process.
final child = _selection!.first!;
/// If the selection is the same, perform a layout pass.
if (child == newSelection.first) {
_performLayout(child);
} else {
/// The selected child index has changed.
/// The size animation will begin from the current child's size.
_sizeTween.begin = child.size;
/// The size animation will end at the new child's size.
_sizeTween.end = newSelection.second;
assert(newSelection.first != null);
_performLayout(newSelection.first!);
/// Update the old and new children selection state.
_oldSelection = _selection;
_selection = newSelection;
}
}
}
A convenience wrapper that makes using the IntrinsicSizeSwitcher widget simple:
/// Convenience wrapper around an [IntrinsicSizeSwitcher].
class AnimatedSizeSwitcher extends StatefulWidget {
const AnimatedSizeSwitcher({
Key? key,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.linear,
this.initialChildIndex = 0,
required this.children,
}) : super(key: key);
final Duration duration;
final Curve curve;
final int initialChildIndex;
final List<Widget> children;
#override
State<AnimatedSizeSwitcher> createState() => AnimatedSizeSwitcherState();
}
class AnimatedSizeSwitcherState extends State<AnimatedSizeSwitcher>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late int _index;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_index = widget.initialChildIndex;
/// The [_animationController] is started at 1.0 so that the first child is
/// visible, because [RenderIntrinsicSizeSwitcher.paint] uses
/// [_animationController.value] as an opacity factor.
SchedulerBinding.instance.addPostFrameCallback(
(timeStamp) => _animationController.forward(from: 1.0),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return IntrinsicSizeSwitcher(
animationController: _animationController,
curve: widget.curve,
selectedIndex: _index,
children: widget.children,
);
}
/// Rebuilds the widget by setting the selected index to whatever is next,
/// wrapping back to 0 when the end is reached.
void nextChild() {
setState(() => _index = (_index + 1) % widget.children.length);
}
}
Utility classes:
The Pair<T, E> holds two values.
class Pair<T, E> {
final T first;
final E second;
const Pair({required this.first, required this.second});
#override
String toString() => "first = $first, second = $second";
}
The SizeComputer<T> class is used to abstract a call to RenderBox.getDryLayout or RenderBox.layout.
abstract class SizeComputer<T> {
const SizeComputer();
Size call(T item, BoxConstraints constraints);
}
class DryLayoutSizeComputer extends SizeComputer<RenderBox> {
const DryLayoutSizeComputer();
#override
Size call(RenderBox item, BoxConstraints constraints) {
final size = item.getDryLayout(constraints);
assert(size.isFinite);
return size;
}
}
class WetLayoutSizeComputer extends SizeComputer<RenderBox> {
const WetLayoutSizeComputer();
#override
Size call(RenderBox item, BoxConstraints constraints) {
item.layout(constraints, parentUsesSize: true);
assert(item.hasSize);
return item.size;
}
}
const dryLayoutSizeComputer = DryLayoutSizeComputer();
const wetLayoutSizeComputer = WetLayoutSizeComputer();
The test application:
void main() => runApp(const MaterialApp(home: AnimatedSizeSwitcherApp()));
class AnimatedSizeSwitcherApp extends StatelessWidget {
const AnimatedSizeSwitcherApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final key = GlobalKey<_AnimatedSizeTestState>();
return Scaffold(
body: GestureDetector(
onTap: () => key.currentState?.displayNextChild(),
child: Center(child: AnimatedSizeTest(key: key)),
),
);
}
}
class AnimatedSizeTest extends StatefulWidget {
const AnimatedSizeTest({Key? key}) : super(key: key);
#override
State<AnimatedSizeTest> createState() => _AnimatedSizeTestState();
}
class _AnimatedSizeTestState extends State<AnimatedSizeTest> {
static const _decoration = BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6.0)),
);
late final children = <Widget>[
Container(
decoration: _decoration.copyWith(color: Colors.red),
width: 110,
height: 100,
),
Container(
decoration: _decoration.copyWith(color: Colors.green),
width: 350,
height: 200,
),
Container(
decoration: _decoration.copyWith(color: Colors.blue),
width: 200,
height: 250,
),
Container(
decoration: _decoration.copyWith(color: Colors.amber),
width: 300,
height: 350,
),
];
final _switcherKey = GlobalKey<AnimatedSizeSwitcherState>();
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4.0),
decoration: _decoration.copyWith(
boxShadow: [
const BoxShadow(
color: Colors.black38,
spreadRadius: 1.0,
blurRadius: 2.0,
),
],
),
child: AnimatedSizeSwitcher(
key: _switcherKey,
curve: Curves.easeOutQuad,
initialChildIndex: 0,
children: children,
),
);
}
void displayNextChild() {
final switcherState = _switcherKey.currentState;
if (switcherState == null) return;
switcherState.nextChild();
}
}
I am trying to expand/animate a container relative to the contained widget dynamic dimensions.
I tried getting the extracting inner widget dimensions by getting its renderbox dimensions.
i have this for getting dimensions change
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
typedef void OnWidgetSizeChange(Size size);
class MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
final 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) {
print('=============bro we here=======');
return MeasureSizeRenderObject(onChange);
}
}
using it like this
...
double _width = 0;
double _height = 0;
...
AnimatedContainerApp(
width: _width,
height: _height,
child: MeasureSize(
onChange: (size) {
print(size.height);
setState(() {
_height = size.height;
});
},
child: !isExpanded
? Container(
height: 200,
width: double.Infinity
)
: Container(
height: 400,
width: double.Infinity
)),
),
with AnimatedContainerApp being a simple AnimatedContainer widget.
I am unable to trigger and update dimensions for the animation. also if the is a better way to achive that I am open for it.
You can use AnimatedSize widget https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html but make sure that it will animate only one side. or else you can check https://stackoverflow.com/a/59133346/19165706 solution here.
I want to add a new option in the text selection toolbar, an extra option apart of the classics cut, copy, paste, selectAll.
enter image description here
I used SelectableText but its toolbarOptions just let active/desactive the classic options, not create a new one. So I'm trying using EditableText and creating my own text_selection.dart copying the material text_selection class
I supose there is something wrong when I call my text_selection class because the toolbar doesn't show and there is not any error message.
enter image description here
Here is my widget that use my text_selection class
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'my_text_selection_controls.dart';
class PageContent extends StatefulWidget {
final String page = 'a text just for testing';
PageContent();
#override
_PageContentState createState() => _PageContentState();
}
class _PageContentState extends State<PageContent> {
var textController = new TextEditingController();
FocusNode _textfieldFocusNode;
#override
void initState(){
super.initState();
_textfieldFocusNode = FocusNode();
textController.text = widget.page;
}
#override
void dispose() {
_textfieldFocusNode.dispose();
textController.dispose();
super.dispose();
}
void screenTapped(){
print('calling a callback');
}
#override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: GestureDetector(
onTap: () => screenTapped(),
child: Container(
alignment: Alignment.center,
child:
EditableText(
focusNode: _textfieldFocusNode,
controller: textController,
backgroundCursorColor: Colors.lightGreen,
selectionColor: Colors.blue,
style: TextStyle(color: Colors.black, fontSize: 17),
cursorColor: Colors.blue,
textInputAction: TextInputAction.newline,
maxLines: null,
enableInteractiveSelection: true,
selectionControls: mymaterialTextSelectionControls, //USING MY TEXT SELECTION CLASS
),
color: Color(0xfffdf5e6),
)
),
),
);
}
}
And here is my_text_selection class. Is just a copy of the material class.
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
const double _kHandleSize = 22.0;
// Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const double _kToolbarScreenPadding = 8.0;
const double _kToolbarHeight = 44.0;
// Padding when positioning toolbar below selection.
const double _kToolbarContentDistanceBelow = 16.0;
const double _kToolbarContentDistance = 8.0;
/// Manages a copy/paste text selection toolbar.
class _TextSelectionToolbar extends StatelessWidget {
const _TextSelectionToolbar({
Key key,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
}) : super(key: key);
final VoidCallback handleCut;
final VoidCallback handleCopy;
final VoidCallback handlePaste;
final VoidCallback handleSelectAll;
#override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final List<Widget> items = <Widget>[
if (handleCut != null) FlatButton(child: Text(localizations.cutButtonLabel), onPressed: handleCut),
if (handleCopy != null) FlatButton(child: Text(localizations.copyButtonLabel), onPressed: handleCopy),
if (handlePaste != null) FlatButton(child: Text(localizations.pasteButtonLabel), onPressed: handlePaste),
if (handleSelectAll != null) FlatButton(child: Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll),
];
// If there is no option available, build an empty widget.
if (items.isEmpty) {
return Container(width: 0.0, height: 0.0);
}
return Material(
elevation: 1.0,
child: Container(
height: _kToolbarHeight,
child: Row(mainAxisSize: MainAxisSize.min, children: items),
),
);
}
}
/// Centers the toolbar around the given position, ensuring that it remains on
/// screen.
class _TextSelectionToolbarLayout extends SingleChildLayoutDelegate {
_TextSelectionToolbarLayout(this.screenSize, this.globalEditableRegion, this.position);
/// The size of the screen at the time that the toolbar was last laid out.
final Size screenSize;
/// Size and position of the editing region at the time the toolbar was last
/// laid out, in global coordinates.
final Rect globalEditableRegion;
/// Anchor position of the toolbar, relative to the top left of the
/// [globalEditableRegion].
final Offset position;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.loosen();
}
#override
Offset getPositionForChild(Size size, Size childSize) {
final Offset globalPosition = globalEditableRegion.topLeft + position;
double x = globalPosition.dx - childSize.width / 2.0;
double y = globalPosition.dy - childSize.height;
if (x < _kToolbarScreenPadding)
x = _kToolbarScreenPadding;
else if (x + childSize.width > screenSize.width - _kToolbarScreenPadding)
x = screenSize.width - childSize.width - _kToolbarScreenPadding;
if (y < _kToolbarScreenPadding)
y = _kToolbarScreenPadding;
else if (y + childSize.height > screenSize.height - _kToolbarScreenPadding)
y = screenSize.height - childSize.height - _kToolbarScreenPadding;
return Offset(x, y);
}
#override
bool shouldRelayout(_TextSelectionToolbarLayout oldDelegate) {
return position != oldDelegate.position;
}
}
/// Draws a single text selection handle which points up and to the left.
class _TextSelectionHandlePainter extends CustomPainter {
_TextSelectionHandlePainter({ this.color });
final Color color;
#override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = color;
final double radius = size.width/2.0;
canvas.drawCircle(Offset(radius, radius), radius, paint);
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, radius, radius), paint);
}
#override
bool shouldRepaint(_TextSelectionHandlePainter oldPainter) {
return color != oldPainter.color;
}
}
class _MaterialTextSelectionControls extends TextSelectionControls {
/// Returns the size of the Material handle.
#override
Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize);
/// Builder for material-style copy/paste text selection toolbar.
#override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset position,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
// The toolbar should appear below the TextField
// when there is not enough space above the TextField to show it.
final TextSelectionPoint startTextSelectionPoint = endpoints[0];
final double toolbarHeightNeeded = MediaQuery.of(context).padding.top
+ _kToolbarScreenPadding
+ _kToolbarHeight
+ _kToolbarContentDistance;
final double availableHeight = globalEditableRegion.top + endpoints.first.point.dy - textLineHeight;
final bool fitsAbove = toolbarHeightNeeded <= availableHeight;
final double y = fitsAbove
? startTextSelectionPoint.point.dy - _kToolbarContentDistance - textLineHeight
: startTextSelectionPoint.point.dy + _kToolbarHeight + _kToolbarContentDistanceBelow;
final Offset preciseMidpoint = Offset(position.dx, y);
return ConstrainedBox(
constraints: BoxConstraints.tight(globalEditableRegion.size),
child: CustomSingleChildLayout(
delegate: _TextSelectionToolbarLayout(
MediaQuery.of(context).size,
globalEditableRegion,
preciseMidpoint,
),
child: _TextSelectionToolbar(
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
),
),
);
}
/// Builder for material-style text selection handles.
#override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) {
final Widget handle = SizedBox(
width: _kHandleSize,
height: _kHandleSize,
child: CustomPaint(
painter: _TextSelectionHandlePainter(
color: Theme.of(context).textSelectionHandleColor
),
),
);
// [handle] is a circle, with a rectangle in the top left quadrant of that
// circle (an onion pointing to 10:30). We rotate [handle] to point
// straight up or up-right depending on the handle type.
switch (type) {
case TextSelectionHandleType.left: // points up-right
return Transform.rotate(
angle: math.pi / 2.0,
child: handle,
);
case TextSelectionHandleType.right: // points up-left
return handle;
case TextSelectionHandleType.collapsed: // points up
return Transform.rotate(
angle: math.pi / 4.0,
child: handle,
);
}
assert(type != null);
return null;
}
/// Gets anchor for material-style text selection handles.
///
/// See [TextSelectionControls.getHandleAnchor].
#override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
switch (type) {
case TextSelectionHandleType.left:
return const Offset(_kHandleSize, 0);
case TextSelectionHandleType.right:
return Offset.zero;
default:
return const Offset(_kHandleSize / 2, -4);
}
}
#override
bool canSelectAll(TextSelectionDelegate delegate) {
// Android allows SelectAll when selection is not collapsed, unless
// everything has already been selected.
final TextEditingValue value = delegate.textEditingValue;
return delegate.selectAllEnabled &&
value.text.isNotEmpty &&
!(value.selection.start == 0 && value.selection.end == value.text.length);
}
}
/// Text selection controls that follow the Material Design specification.
final TextSelectionControls mymaterialTextSelectionControls = _MaterialTextSelectionControls();
I am working on Google Map Markers in Flutter.
On the click of each Marker, I want to show a Custom Info Window which can include a button, image etc. But in Flutter there is a property TextInfoWindow which only accept String.
How can i achieve adding buttons, images to the map marker's InfoWindow.
Stumbled across this problem and found a solution which works for me:
To solve it I did write a Custom Info Widget, feel free to customize it. For example with some shadow via ClipShadowPath.
Implementation
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'custom_info_widget.dart';
void main() => runApp(MyApp());
class PointObject {
final Widget child;
final LatLng location;
PointObject({this.child, this.location});
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: "/",
routes: {
"/": (context) => HomePage(),
},
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
PointObject point = PointObject(
child: Text('Lorem Ipsum'),
location: LatLng(47.6, 8.8796),
);
StreamSubscription _mapIdleSubscription;
InfoWidgetRoute _infoWidgetRoute;
GoogleMapController _mapController;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.green,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: const LatLng(47.6, 8.6796),
zoom: 10,
),
circles: Set<Circle>()
..add(Circle(
circleId: CircleId('hi2'),
center: LatLng(47.6, 8.8796),
radius: 50,
strokeWidth: 10,
strokeColor: Colors.black,
)),
markers: Set<Marker>()
..add(Marker(
markerId: MarkerId(point.location.latitude.toString() +
point.location.longitude.toString()),
position: point.location,
onTap: () => _onTap(point),
)),
onMapCreated: (mapController) {
_mapController = mapController;
},
/// This fakes the onMapIdle, as the googleMaps on Map Idle does not always work
/// (see: https://github.com/flutter/flutter/issues/37682)
/// When the Map Idles and a _infoWidgetRoute exists, it gets displayed.
onCameraMove: (newPosition) {
_mapIdleSubscription?.cancel();
_mapIdleSubscription = Future.delayed(Duration(milliseconds: 150))
.asStream()
.listen((_) {
if (_infoWidgetRoute != null) {
Navigator.of(context, rootNavigator: true)
.push(_infoWidgetRoute)
.then<void>(
(newValue) {
_infoWidgetRoute = null;
},
);
}
});
},
),
),
);
}
/// now my _onTap Method. First it creates the Info Widget Route and then
/// animates the Camera twice:
/// First to a place near the marker, then to the marker.
/// This is done to ensure that onCameraMove is always called
_onTap(PointObject point) async {
final RenderBox renderBox = context.findRenderObject();
Rect _itemRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
_infoWidgetRoute = InfoWidgetRoute(
child: point.child,
buildContext: context,
textStyle: const TextStyle(
fontSize: 14,
color: Colors.black,
),
mapsWidgetSize: _itemRect,
);
await _mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
point.location.latitude - 0.0001,
point.location.longitude,
),
zoom: 15,
),
),
);
await _mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
point.location.latitude,
point.location.longitude,
),
zoom: 15,
),
),
);
}
}
CustomInfoWidget:
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:meta/meta.dart';
class _InfoWidgetRouteLayout<T> extends SingleChildLayoutDelegate {
final Rect mapsWidgetSize;
final double width;
final double height;
_InfoWidgetRouteLayout(
{#required this.mapsWidgetSize,
#required this.height,
#required this.width});
/// Depending of the size of the marker or the widget, the offset in y direction has to be adjusted;
/// If the appear to be of different size, the commented code can be uncommented and
/// adjusted to get the right position of the Widget.
/// Or better: Adjust the marker size based on the device pixel ratio!!!!)
#override
Offset getPositionForChild(Size size, Size childSize) {
// if (Platform.isIOS) {
return Offset(
mapsWidgetSize.center.dx - childSize.width / 2,
mapsWidgetSize.center.dy - childSize.height - 50,
);
// } else {
// return Offset(
// mapsWidgetSize.center.dx - childSize.width / 2,
// mapsWidgetSize.center.dy - childSize.height - 10,
// );
// }
}
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
//we expand the layout to our predefined sizes
return BoxConstraints.expand(width: width, height: height);
}
#override
bool shouldRelayout(_InfoWidgetRouteLayout oldDelegate) {
return mapsWidgetSize != oldDelegate.mapsWidgetSize;
}
}
class InfoWidgetRoute extends PopupRoute {
final Widget child;
final double width;
final double height;
final BuildContext buildContext;
final TextStyle textStyle;
final Rect mapsWidgetSize;
InfoWidgetRoute({
#required this.child,
#required this.buildContext,
#required this.textStyle,
#required this.mapsWidgetSize,
this.width = 150,
this.height = 50,
this.barrierLabel,
});
#override
Duration get transitionDuration => Duration(milliseconds: 100);
#override
bool get barrierDismissible => true;
#override
Color get barrierColor => null;
#override
final String barrierLabel;
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return MediaQuery.removePadding(
context: context,
removeBottom: true,
removeLeft: true,
removeRight: true,
removeTop: true,
child: Builder(builder: (BuildContext context) {
return CustomSingleChildLayout(
delegate: _InfoWidgetRouteLayout(
mapsWidgetSize: mapsWidgetSize, width: width, height: height),
child: InfoWidgetPopUp(
infoWidgetRoute: this,
),
);
}),
);
}
}
class InfoWidgetPopUp extends StatefulWidget {
const InfoWidgetPopUp({
Key key,
#required this.infoWidgetRoute,
}) : assert(infoWidgetRoute != null),
super(key: key);
final InfoWidgetRoute infoWidgetRoute;
#override
_InfoWidgetPopUpState createState() => _InfoWidgetPopUpState();
}
class _InfoWidgetPopUpState extends State<InfoWidgetPopUp> {
CurvedAnimation _fadeOpacity;
#override
void initState() {
super.initState();
_fadeOpacity = CurvedAnimation(
parent: widget.infoWidgetRoute.animation,
curve: Curves.easeIn,
reverseCurve: Curves.easeOut,
);
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeOpacity,
child: Material(
type: MaterialType.transparency,
textStyle: widget.infoWidgetRoute.textStyle,
child: ClipPath(
clipper: _InfoWidgetClipper(),
child: Container(
color: Colors.white,
padding: EdgeInsets.only(bottom: 10),
child: Center(child: widget.infoWidgetRoute.child),
),
),
),
);
}
}
class _InfoWidgetClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
Path path = Path();
path.lineTo(0.0, size.height - 20);
path.quadraticBezierTo(0.0, size.height - 10, 10.0, size.height - 10);
path.lineTo(size.width / 2 - 10, size.height - 10);
path.lineTo(size.width / 2, size.height);
path.lineTo(size.width / 2 + 10, size.height - 10);
path.lineTo(size.width - 10, size.height - 10);
path.quadraticBezierTo(
size.width, size.height - 10, size.width, size.height - 20);
path.lineTo(size.width, 10.0);
path.quadraticBezierTo(size.width, 0.0, size.width - 10.0, 0.0);
path.lineTo(10, 0.0);
path.quadraticBezierTo(0.0, 0.0, 0.0, 10);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
I stumbled across the same problem just today, I couldn't get a multiline string to show properly in TextInfoWindow. I ended up circumventing the problem by implementing a modal bottom sheet (https://docs.flutter.io/flutter/material/showModalBottomSheet.html) that shows when you click on a marker, which in my case worked out quite nicely.
I can also imagine many use cases where you'd want to fully customize the marker's info window, but reading this issue on GitHub (https://github.com/flutter/flutter/issues/23938) it looks like it's currently not possible, because the InfoWindow is not a Flutter widget.
You can display marker made of widgets as custom 'info window'. Basically you are creating png image of your widget and displaying it as a marker.
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class MarkerInfo extends StatefulWidget {
final Function getBitmapImage;
final String text;
MarkerInfo({Key key, this.getBitmapImage, this.text}) : super(key: key);
#override
_MarkerInfoState createState() => _MarkerInfoState();
}
class _MarkerInfoState extends State<MarkerInfo> {
final markerKey = GlobalKey();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => getUint8List(markerKey)
.then((markerBitmap) => widget.getBitmapImage(markerBitmap)));
}
Future<Uint8List> getUint8List(GlobalKey markerKey) async {
RenderRepaintBoundary boundary =
markerKey.currentContext.findRenderObject();
var image = await boundary.toImage(pixelRatio: 2.0);
ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
return byteData.buffer.asUint8List();
}
#override
Widget build(BuildContext context) {
return RepaintBoundary(
key: markerKey,
child: Container(
padding: EdgeInsets.only(bottom: 29),
child: Container(
width: 100,
height: 100,
color: Color(0xFF000000),
child: Text(
widget.text,
style: TextStyle(
color: Color(0xFFFFFFFF),
),
),
),
),
);
}
}
If you use this approach you have to make sure you render the widget, because this will not work otherwise. For converting widgets to images - widget has to be rendered in order to convert it. I'm hiding my widget under the map in Stack.
return Stack(
children: <Widget>[
MarkerInfo(
text: tripMinutes.toString(),
getBitmapImage: (img) {
customMarkerInfo = img;
}),
GoogleMap(
markers: markers,
...
Last step is to create a Marker. Data passed from the widget is saved in customMarkerInfo - bytes, so convert it to Bitmap.
markers.add(
Marker(
position: position,
icon: BitmapDescriptor.fromBytes(customMarkerInfo),
markerId: MarkerId('MarkerID'),
),
);
Example
Here’s a solution to create custom marker that doesn’t rely on InfoWindow. Although, this approch won’t allow you to add a button on custom marker.
Flutter google maps plugin lets us use image data / asset to create a custom marker. So, this approach uses drawing on Canvas to create a custom marker and using PictureRecorder to convert the same to a picture, which later on would be used by google maps plugin to render a custom marker.
Sample code to draw on Canvas and convert the same to Image data that can be used by the plugin.
void paintTappedImage() async {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas canvas = Canvas(recorder, Rect.fromPoints(const Offset(0.0, 0.0), const Offset(200.0, 200.0)));
final Paint paint = Paint()
..color = Colors.black.withOpacity(1)
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(0.0, 0.0, 152.0, 48.0), const Radius.circular(4.0)),
paint);
paintText(canvas);
paintImage(labelIcon, const Rect.fromLTWH(8, 8, 32.0, 32.0), canvas, paint,
BoxFit.contain);
paintImage(markerImage, const Rect.fromLTWH(24.0, 48.0, 110.0, 110.0), canvas,
paint, BoxFit.contain);
final Picture picture = recorder.endRecording();
final img = await picture.toImage(200, 200);
final pngByteData = await img.toByteData(format: ImageByteFormat.png);
setState(() {
_customMarkerIcon = BitmapDescriptor.fromBytes(Uint8List.view(pngByteData.buffer));
});
}
void paintText(Canvas canvas) {
final textStyle = TextStyle(
color: Colors.white,
fontSize: 24,
);
final textSpan = TextSpan(
text: '18 mins',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: 88,
);
final offset = Offset(48, 8);
textPainter.paint(canvas, offset);
}
void paintImage(
ui.Image image, Rect outputRect, Canvas canvas, Paint paint, BoxFit fit) {
final Size imageSize =
Size(image.width.toDouble(), image.height.toDouble());
final FittedSizes sizes = applyBoxFit(fit, imageSize, outputRect.size);
final Rect inputSubrect =
Alignment.center.inscribe(sizes.source, Offset.zero & imageSize);
final Rect outputSubrect =
Alignment.center.inscribe(sizes.destination, outputRect);
canvas.drawImageRect(image, inputSubrect, outputSubrect, paint);
}
once the marker is tapped, we can replace the tapped image with the new image generated from Canvas. Sample code for the same taken from google maps plugin example app.
void _onMarkerTapped(MarkerId markerId) async {
final Marker tappedMarker = markers[markerId];
if (tappedMarker != null) {
if (markers.containsKey(selectedMarker)) {
final Marker resetOld =
markers[selectedMarker].copyWith(iconParam: _markerIconUntapped);
setState(() {
markers[selectedMarker] = resetOld;
});
}
Marker newMarker;
selectedMarker = markerId;
newMarker = tappedMarker.copyWith(iconParam: _customMarkerIcon);
setState(() {
markers[markerId] = newMarker;
});
tappedCount++;
}
}
Reference:
How to convert a flutter canvas to Image.
Flutter plugin example app.
Bellow is 4 step I had implemented for custom InfoWindow on my project
Step 1: Create a stack for GoogleMap and Info Window Custom.
Stack(
children: <Widget>[
Positioned.fill(child: GoogleMap(...),),
Positioned(
top: {offsetY},
left: {offsetX},
child: YourCustomInfoWidget(...),
)
]
)
Step 2: When user click Marker calculator position of marker on screen with func:
screenCoordinate = await _mapController.getScreenCoordinate(currentPosition.target)
Step 3: Calculator offsetY, offsetX and setState.
Relate issue: https://github.com/flutter/flutter/issues/41653
devicePixelRatio = Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;
offsetY = (screenCoordinate?.y?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.width;
offsetX = (screenCoordinate?.x?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.height;
Step 4: Disable Marker auto move camera when tap
Marker(
...
consumeTapEvents: true,)
To create a widget-based info window you need to stack the widget on google map. With the help of ChangeNotifierProvider, ChangeNotifier, and Consumer you can easily rebuild your widget even when the camera moves on google map.
InfoWindowModel class:
class InfoWindowModel extends ChangeNotifier {
bool _showInfoWindow = false;
bool _tempHidden = false;
User _user;
double _leftMargin;
double _topMargin;
void rebuildInfoWindow() {
notifyListeners();
}
void updateUser(User user) {
_user = user;
}
void updateVisibility(bool visibility) {
_showInfoWindow = visibility;
}
void updateInfoWindow(
BuildContext context,
GoogleMapController controller,
LatLng location,
double infoWindowWidth,
double markerOffset,
) async {
ScreenCoordinate screenCoordinate =
await controller.getScreenCoordinate(location);
double devicePixelRatio =
Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;
double left = (screenCoordinate.x.toDouble() / devicePixelRatio) -
(infoWindowWidth / 2);
double top =
(screenCoordinate.y.toDouble() / devicePixelRatio) - markerOffset;
if (left < 0 || top < 0) {
_tempHidden = true;
} else {
_tempHidden = false;
_leftMargin = left;
_topMargin = top;
}
}
bool get showInfoWindow =>
(_showInfoWindow == true && _tempHidden == false) ? true : false;
double get leftMargin => _leftMargin;
double get topMargin => _topMargin;
User get user => _user;
}
Complete Example is available on my blog!
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()
})