Scrollable CustomMultiChildLayout - flutter

I can't get CustomMultiChildLayout to be scrollable.
For now my CustomMultiChildLayout just laying out children in a column. In a future I'll make it more complex, but now I want to make it scrollable. I tried putting it inside ListView, but got RenderIndexedSemantics object was given an infinite size during layout..
I tried putting it inside SingleChildScrollView with IntrinsicHeight but in this case it can't be scrolled and it's height is the same as viewport.
Here is some examples that is not working properly:
return SingleChildScrollView(
child: IntrinsicHeight(
child: CustomMultiChildLayout(
delegate: ViewLayoutBuilder(itemCount: cards.length),
children: _buildChildren(context),
),
),
);
and
return ListView(
children: <Widget>[
CustomMultiChildLayout(
delegate: ViewLayoutBuilder(itemCount: cards.length),
children: _buildChildren(context),
)
],
)
Here is my ViewLayoutBuilder delegate:
class ViewLayoutBuilder extends MultiChildLayoutDelegate {
final int itemCount;
ViewLayoutBuilder({#required this.itemCount});
#override
void performLayout(Size size) {
Offset position = Offset(0,0);
Size firstSize = layoutChild('card_0', BoxConstraints.tightFor(width: size.width));
positionChild('card_0', position);
position = position.translate(0, firstSize.height);
for (int i = 1; i < itemCount; i++) {
final String cardId = 'card_$i';
if (hasChild(cardId)) {
Size newSize = layoutChild('$cardId', BoxConstraints.tightFor(width: size.width));
positionChild('$cardId', position);
position = position.translate(0, newSize.height);
}
}
}
#override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
The main error in all my tries is that my CustomMultiChildLayout given an infinity height. In other cases it is cropped to a viewport height and can't be scrolled.

Related

Flutter - Divider widget changing width based on screen position

I am trying to use the Divider class. It seems that its rendered width is changing based on where it is on the screen even though I have explicitly set the width.
I have the following code (which I've simplified a bit):
class HistoryListState extends State<HistoryList> {
// use this function to generate the list of widgets used as a label for each list item
// each label consists of the workout name, a divider, and the first 3 exercises
List<Widget> generateLabel(TrackedWorkout workout) {
List<Widget> widgets = [
Text(workout.name),
// DIVIDER IN QUESTION
const Divider(
color: Color.fromARGB(255, 0, 0, 0),
thickness: 1,
),
];
for (int i = 0; i < workout.exercises.length; i++) {
if (i > 2) {
widgets.add(const Text('...'));
break;
}
widgets.add(Text(workout.exercises[i].name));
}
return widgets;
}
#override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.builder(
itemCount: widget.workouts.length,
itemBuilder: (BuildContext context, int index) {
return ElevatedButton(
onPressed: () {},
child: Column(
children: generateLabel(widget.workouts[index]),
),
);
}),
);
}
}
Video demo
As you can see, initially, each divider has slightly different widths. Then when the list is scrolled, the dividers change widths too.

Constrainting layout to not go out of bounds with Positioned widget

I am currently working on a layout that displays a Positioned widget on the entire screen.
It's positioning itself close to the detected barcode, Look at the image below for an example.
But when the barcode moves to close the the left edge of the screen, the UI elements are drawn partially offscreen. Is there a way I can fix this without having to calculate when I am going out of bounds each frame?
Here is the code that I use to set this up:
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
child: _controller == null
? const Center(
child: Text(
'Initializing Camera...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: Stack(
fit: StackFit.expand,
children: <Widget>[
CameraPreview(_controller!),
_buildResults(),
if (_scanResults.isNotEmpty)
_buildUIElements()
],
),
);
}
Widget _buildUIElements() {
Barcode barcode = _scanResults[0];
final Size imageSize = Size(
_controller!.value.previewSize!.height,
_controller!.value.previewSize!.width,
);
var boundingBox = barcode.boundingBox!;
var rect = scaleRect(rect: boundingBox, imageSize: imageSize, widgetSize: MediaQuery.of(context).size);
return AnimatedPositioned(
top: rect.bottom,
left: rect.left,
child: Card(
child: Text('This is an amaizing product'),
),
duration: const Duration(milliseconds: 500),
);
}
Maybe there is a better way to achieve this?
Don't mind the excessive use of ! still learning the whole null-safety thing :)
EDIT 1:
As suggested by pskink I have looked at how the tooltips in flutter work and made use of the SingleChildLayoutDelegate in combination with a CustomSingleChildLayout and this works perfectly for tracking the position but now there is no option to animate this.
My delegate class is as follows:
class CustomSingleChildDelegate extends SingleChildLayoutDelegate {
CustomSingleChildDelegate ({
required this.target,
required this.verticalOffset,
required this.preferBelow,
});
final Offset target;
final double verticalOffset;
final bool preferBelow;
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
#override
Offset getPositionForChild(Size size, Size childSize) {
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
);
}
#override
bool shouldRelayout(CustomSingleChildDelegate oldDelegate) {
return target != oldDelegate.target
|| verticalOffset != oldDelegate.verticalOffset
|| preferBelow != oldDelegate.preferBelow;
}
}
And then updated my builder function with:
return CustomSingleChildLayout(
delegate: CustomSingleChildDelegate (target: rect.bottomCenter, verticalOffset: 20, preferBelow: true),
child: Card(
child: Text('This is an amaizing product'),
),
)
Having the AnimatedPositioned as child of the layout causes an exception.

How to dynamically draw rectangles on an image in Flutter?

I want to allow the user draw rectangles on the image he receives in a certain way and get the coordinates of the drawn rectangle. One of the ways I was thinking about is allowing him to tap on the image four times to draw a rectangle from these four coordinates. Currently I'm unable to get the exact local tap position.. it's not always exact.
Any recommendations for my requirements?
This is my current code:
double posx = 100.0;
double posy = 100.0;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
Offset _tapPosition;
void _handleTapDown(TapDownDetails details) {
final RenderBox referenceBox = context.findRenderObject();
setState(() {
_tapPosition = referenceBox.globalToLocal(details.globalPosition);
posx = _tapPosition.dx;
posy = _tapPosition.dy;
});
}
#override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return
GestureDetector(
onTapDown: _handleTapDown,
onTap: (){
},
child:new Stack(fit: StackFit.expand, children: <Widget>[
// Hack to expand stack to fill all the space. There must be a better
// way to do it.
new Container(
color:Colors.white,
child: Image.asset("lib/assets/rectangles.png"),
),
// new Container(height:200,width: 100,color: Colors.white),
new Positioned(
child: new Text('.'),
left: posx,
top: posy,
)
]),
);
}

StackedBar widget in Flutter

I'm trying to create my own horizontal stacked bar widget in Flutter and I have some difficulties. This is repro of the code I have:
class StackedBar extends StatefulWidget {
HashMap<int, double> factors = HashMap();
StackedBar(){
factors[0] = 0.1;
factors[1] = 0.3;
factors[2] = 0.5;
factors[3] = 0.1;
}
#override
_StackedBarState createState() => new _StackedBarState();
}
class _StackedBarState extends State<StackedBar> {
#override
Widget build(BuildContext context) {
List<Widget> widgets = [];
for (int i =0; i < widget.factors.length; i++) {
var w = Flexible(
child: FractionallySizedBox(
widthFactor: widget.factors[i],
child: Container(
height: 50,
color: ColorHelper.getColor(i))));
widgets.add(w);
}
return Container(
color: Colors.white,
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: widgets));
}
}
The result looks like this:
So as you can see the bar is not covering entire row. I think the problem is that FractionallySizedBox is not calculating it's fraction from rows width but rather from the space that widget will have available for him inside the row (in my case probably 1/4 of the row's width). Am I correct? If so what would be the correct solution?
Use Expanded instead of Flexible. Change your code like this:
final HashMap<int, double> factors = HashMap();
StackedBar(){
factors[0] = 1;
factors[1] = 3;
factors[2] = 5;
factors[3] = 1;
}
and inside your build method :
for (int i =0; i < widget.factors.length; i++) {
var w = Expanded(
flex: widget.factors[i],
child: Container(
height: 50,
color: Colors.accents[i]));
widgets.add(w);
}

In Flutter, how can a positioned Widget feel taps outside of its parent Stack area?

A Stack contains MyWidget inside of a Positioned.
Stack(
overflow: Overflow.visible,
children: [
Positioned(
top: 0.0,
left: 0.0,
child: MyWidget(),
)],
);
Since overflow is Overflow.visible and MyWidget is larger than the Stack, it displays outside of the Stack, which is what I want.
However, I can't tap in the area of MyWidget which is outside of the Stack area. It simply ignores the tap there.
How can I make sure MyWidget accepts gestures there?
This behavior occurs because the stack checks whether the pointer is inside its bounds before checking whether a child got hit:
Class: RenderBox (which RenderStack extends)
bool hitTest(BoxHitTestResult result, { #required Offset position }) {
...
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
My workaround is deleting the
if (_size.contains(position))
check.
Unfortunately, this is not possible without copying code from the framework.
Here is what I did:
Copied the Stack class and named it Stack2
Copied RenderStack and named it RenderStack2
Made Stack2 reference RenderStack2
Added the hitTest method from above without the _size.contains check
Copied Positioned and named it Positioned2 and made it reference Stack2 as its generic parameter
Used Stack2 and Positioned2 in my code
This solution is by no means optimal, but it achieves the desired behavior.
I had a similar issue. Basically since the stack's children don't use the fully overflown box size for their hit testing, i used a nested stack and an arbitrary big height so that i can capture the clicks of the nested stack's overflown boxes. Not sure if it can work for you but here goes nothing :)
So in your example maybe you could try something like that
Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: 0.0,
left: 0.0,
height : 500.0 // biggest possible child size or just very big
child: Stack(
children: [MyWidget()]
),
)],
);
You can consider using inheritance to copy the hitTest method to break the hit rule, example
class Stack2 extends Stack {
Stack2({
Key key,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit = StackFit.loose,
Overflow overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
}) : super(
key: key,
alignment: alignment,
textDirection: textDirection,
fit: fit,
overflow: overflow,
children: children,
);
#override
RenderStack createRenderObject(BuildContext context) {
return RenderStack2(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
overflow: overflow,
);
}
}
class RenderStack2 extends RenderStack {
RenderStack2({
List<RenderBox> children,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit = StackFit.loose,
Overflow overflow = Overflow.clip,
}) : super(
children: children,
alignment: alignment,
textDirection: textDirection,
fit: fit,
overflow: overflow,
);
#override
bool hitTest(BoxHitTestResult result, {Offset position}) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
return false;
}
}
Ok, I did a workaround about this, basically I added a GestureDetector on the parent and implemented the onTapDown.
Also you have to keep track your Widget using GlobalKey to get the current position.
When the Tap at the parent level is detected check if the tap position is inside your widget.
The code below:
final GlobalKey key = new GlobalKey();
void onTapDown(BuildContext context, TapDownDetails details) {
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
final RenderBox containerBox = key.currentContext.findRenderObject();
final Offset containerOffset = containerBox.localToGlobal(localOffset);
final onTap = containerBox.paintBounds.contains(containerOffset);
if (onTap){
print("DO YOUR STUFF...");
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Container(
color: Colors.red,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 400.0,
child: Container(
color: Colors.black,
child: Stack(
overflow: Overflow.visible,
children: [
Positioned(
top: 0.0, left: 0.0,
child: Container(
key: key,
width: 500.0,
height: 200.0,
color: Colors.blue,
),
),
],
),
),
),
),
),
);
}
This limitation can be worked around by using an OverlayEntry widget as the Stack's parent (since OverlayEntry fills up the entire screen all children are also hit tested). Here is a proof of concept solution on DartPad.
Create a custom widget that returns a Future:
Widget build(BuildContext context) {
Future(showOverlay);
return Container();
}
This future should then remove any previous instance of OverlayEntry and insert the Stack with your custom widgets:
void showOverlay() {
hideOverlay();
RenderBox? renderBox = context.findAncestorRenderObjectOfType<RenderBox>();
var parentSize = renderBox!.size;
var parentPosition = renderBox.localToGlobal(Offset.zero);
overlay = _overlayEntryBuilder(parentPosition, parentSize);
Overlay.of(context)!.insert(overlay!);
}
void hideOverlay() {
overlay?.remove();
}
Use a builder function to generate the Stack:
OverlayEntry _overlayEntryBuilder(Offset parentPosition, Size parentSize) {
return OverlayEntry(
maintainState: false,
builder: (context) {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: parentPosition.dx + parentSize.width,
top: parentPosition.dy + parentSize.height,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {},
child: Container(),
),
),
),
],
);
},
);
}
Column is another Stack
key point:
verticalDirection is up.
transform down the top widget.
Below is my code, you can copy and test:
Column(
verticalDirection: VerticalDirection.up,
children: [
Container(
width: 200,
height: 100,
color: Colors.red,
),
Transform.translate(
offset: const Offset(0, 30),
child: GestureDetector(
onTap: () {
print('tap orange view');
},
child: Container(
width: 60,
height: 60,
color: Colors.orange,
),
),
),
],
),
I write a container to resolve this problem, which not implements beautifully, but can be used and did code encapsulation for easily to use.
Here is the implement:
import 'package:flutter/widgets.dart';
/// Creates a widget that can check its' overflow children's hitTest
///
/// [overflowKeys] is must, and there should be used on overflow widget's outermost widget those' sizes cover the overflow child, because it will [hitTest] its' children, but not [hitTest] its' parents. And i cannot found a way to check RenderBox's parent in flutter.
///
/// The [OverflowWithHitTest]'s size must contains the overflow widgets, so you can use it as outer as possible.
///
/// This will not reduce rendering performance, because it only overcheck the given widgets marked by [overflowKeys].
///
/// Demo:
///
/// class MyPage extends State<UserCenterPage> {
///
/// var overflowKeys = <GlobalKey>[GlobalKey()];
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: OverflowWithHitTest(
///
/// overflowKeys: overflowKeys,
///
/// child: Container(
/// height: 50,
/// child: UnconstrainedBox(
/// child: Container(
/// width: 200,
/// height: 50,
/// color: Colors.red,
/// child: OverflowBox(
/// alignment: Alignment.topLeft,
/// minWidth: 100,
/// maxWidth: 200,
/// minHeight: 100,
/// maxHeight: 200,
/// child: GestureDetector(
/// key: overflowKeys[0],
/// behavior: HitTestBehavior.translucent,
/// onTap: () {
/// print('==== onTap;');
/// },
/// child: Container(
/// color: Colors.blue,
/// height: 200,
/// child: Text('aaaa'),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
///
///
class OverflowWithHitTest extends SingleChildRenderObjectWidget {
const OverflowWithHitTest({
required this.overflowKeys,
Widget? child,
Key? key,
}) : super(key: key, child: child);
final List<GlobalKey> overflowKeys;
#override
_OverflowWithHitTestBox createRenderObject(BuildContext context) {
return _OverflowWithHitTestBox(overflowKeys: overflowKeys);
}
#override
void updateRenderObject(
BuildContext context, _OverflowWithHitTestBox renderObject) {
renderObject.overflowKeys = overflowKeys;
}
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<List<GlobalKey>>('overflowKeys', overflowKeys));
}
}
class _OverflowWithHitTestBox extends RenderProxyBoxWithHitTestBehavior {
_OverflowWithHitTestBox({required List<GlobalKey> overflowKeys})
: _overflowKeys = overflowKeys,
super(behavior: HitTestBehavior.translucent);
/// Global keys of overflow children
List<GlobalKey> get overflowKeys => _overflowKeys;
List<GlobalKey> _overflowKeys;
set overflowKeys(List<GlobalKey> value) {
var changed = false;
if (value.length != _overflowKeys.length) {
changed = true;
} else {
for (var ind = 0; ind < value.length; ind++) {
if (value[ind] != _overflowKeys[ind]) {
changed = true;
}
}
}
if (!changed) {
return;
}
_overflowKeys = value;
markNeedsPaint();
}
#override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
if (hitTestOverflowChildren(result, position: position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
bool hitTarget = false;
if (size.contains(position)) {
hitTarget =
hitTestChildren(result, position: position) || hitTestSelf(position);
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
bool hitTestOverflowChildren(BoxHitTestResult result,
{required Offset position}) {
if (overflowKeys.length == 0) {
return false;
}
var hitGlobalPosition = this.localToGlobal(position);
for (var child in overflowKeys) {
if (child.currentContext == null) {
continue;
}
var renderObj = child.currentContext!.findRenderObject();
if (renderObj == null || renderObj is! RenderBox) {
continue;
}
var localPosition = renderObj.globalToLocal(hitGlobalPosition);
if (renderObj.hitTest(result, position: localPosition)) {
return true;
}
}
return false;
}
}