Column's child's CustomPainter's size become (0, 0).
1) When we just used CustomPaint and CustomPainter, the size we expected was passed to CustomPainter, which is rendered as expected.
2) When we used CustomPaint and CustomPainter as Stack's children, the size we expected was not passed to CustomPainter.
3) But when we used it as SizedBox.expand's children, the size we expected was passed to CustomPainter and it was rendered.
4) When we used CustomPaint and CustomPainter as Column's children, the size we expected was not passed to CustomPainter. We are investigating ways to solve it.
5) However, it is either 0.0 or infinite in height. We want it to just expand to a good size.
import 'package:flutter/material.dart';
class TestWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
// 1) OK Size(360.0, 640.0)
// return CustomPaint(
// painter: TestPainter(),
// );
// 2) NG Size(0.0, 0.0)
// return Stack(
// children: <Widget>[
// CustomPaint(
// painter: TestPainter(),
// ),
// ],
// );
// 3) OK Size(360.0, 640.0)
// return Stack(
// children: <Widget>[
// SizedBox.expand(
// child: CustomPaint(
// painter: TestPainter(),
// ),
// ),
// ],
// );
// 4)5) NG BoxConstraints forces an infinite height.
// return Column(
// children: <Widget>[
// SizedBox.expand(
// child: CustomPaint(
// painter: TestPainter(),
// ),
// ),
// Text('test'),
// ],
// );
// 4)5) NG Size(0.0, 0.0)
// return Column(
// children: <Widget>[
// SizedBox(
// child: CustomPaint(
// painter: TestPainter(),
// ),
// ),
// Text('test'),
// ],
// );
// 4)5) Size(360.0, 0.0)
// return Column(
// children: <Widget>[
// SizedBox(
// width: double.infinity,
// child: CustomPaint(
// painter: TestPainter(),
// ),
// ),
// Text('test'),
// ],
// );
}
}
class TestPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
print(size);
Paint p = Paint();
p.color = Color.fromARGB(0xff, 0xff, 0x00, 0x00);
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, p);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
We expect CustomPaint and CustomPainter to be expanded to the appropriate size and that size will be passed to CustomPainter.
Below is the constructor of CustomPaint():
const CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget child,
}) : assert(size != null),
assert(isComplex != null),
assert(willChange != null),
super(key: key, child: child);
The size is regarded as Size.zero (meaning Size(0.0, 0.0)) when it is omitted, so you need to pass your desired size to 'CustomPaint()`.
return CustomPaint(
size: const Size(double.infinity, double.infinity), // or whatever size you want it to be
painter: TestPainter(),
);
As for expansion of a widget inside Column, use Expanded instead of SizedBox.expand.
To pass the desired argument in flutter class you can pass it easily from its instance
and use it in class using constructor
For example:
class TestPainter extends CustomPainter {
int size;
// Constructor
TestPainter({this.size});
}
And you can pass size as an argument to this TestPainter class
TestPainter(size: 10);
Create a constructor inside TestPainter class and accepts the argument from there.
e.g:
var size;
TestPainter(this.size);
Now while calling this class , pass the size value to the constructor
TestPainter(20.0);
And use it inside paint method as :
widget.size.width
Related
I am currently working on a pathfinding-visualizer-app and i am a bit overwhelmed by using a completely new programming language (at least for me).
But i think it is the best way to learn it as fast as possible.
To get to my problem:
I have a gridPaper with container Widgets. Every time i click on a container it changes the color from white to black.
So far so good...
For my pathfinding algorithm i need a 'start' and 'end' container (obviously a container where my algorithm starts searching for the end point).
Those i want to colorize in green (start) and red (end).
If i open the settings widget to click on "Start", it changes a "var = int" to 2 and then it goes to a switch case function.
From there if it gets a 1, it should print a black container, and if it gets a 2, it should fill the container green.
But then it starts to fill every container with the color green, because it goes through the complete offset ...
Do do have any ideas how to solve my problem ?
Code:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final appTitle = 'Path Finder';
final Color gridColor = Colors.lightBlue[100];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends HookWidget {
final double cellSize = 20.0;
final String title;
var block = 1;
GlobalKey _key = GlobalKey();
MyHomePage({
Key key,
this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final _activated = useState<List<Offset>>([]);
void _toggle(Offset offset) {
if (!_activated.value.remove(offset)) _activated.value.add(offset);
_activated.value = [..._activated.value];
}
return Scaffold(
appBar: AppBar(title: Text(title)),
body: GestureDetector(
onTapDown: (details) => _toggle(details.localPosition ~/ cellSize),
child: GridPaper(
child: Stack(
children: gridContainerMap(_activated),
),
color: Colors.lightBlue[100],
interval: cellSize,
divisions: 1,
subdivisions: 1,
),
),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Einstellungen'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Startpunkt'),
onTap: () {
// Update the state of the app
// ...
block = 2;
// Then close the drawer
Navigator.pop(context);
},
),
ListTile(
title: Text('Ziel'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
ListTile(
title: Text('Pathfinding-Algorithm'),
onTap: () {
// Update the state of the app
// ...
// Then close the drawer
Navigator.pop(context);
},
),
],
),
),
);
}
List<Widget> gridContainerMap(ValueNotifier<List<Offset>> _activated) {
switch (block) {
case 1:
return [
Container(color: Colors.white),
..._activated.value.map((offset) {
print('OFFSET: $offset');
return coloringWallContainer(offset);
}).toList(),
];
break;
case 2:
return [
Container(color: Colors.white),
..._activated.value.map((offset) {
print('OFFSET: $offset');
return coloringStartContainer(offset);
}).toList(),
];
break;
default:
}
}
Positioned coloringWallContainer(Offset offset) {
return Positioned(
left: offset.dx * cellSize,
top: offset.dy * cellSize,
width: cellSize,
height: cellSize,
child: ColoredBox(color: Colors.black),
);
}
Positioned coloringStartContainer(Offset offset) {
block = 1;
return Positioned(
left: offset.dx * cellSize,
top: offset.dy * cellSize,
width: cellSize,
height: cellSize,
child: ColoredBox(color: Colors.green),
);
}
}
Best regards
Robsen
Ah nevermind, i just needed to know where i get the Offset from the next grid element...
After that it was relatively easy.
I have a StatefulWidget where there is a ListView holding several childs widget.
One of the child is a GridView containing some items.
What I would want to achieve is to rebuild this GridView child when a button is pressed from the Parent widget. The button is located in the bottomNavigationBar in the Parent widget.
However, when I pressed the button, it should go to the _resetFilter() method, which works. But the setState() doesn't seem to update the GridView build() method inside Child widget.
class ParentState extends State<Parent> {
// removed for brevity
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
bottomNavigationBar: BottomAppBar(
child: new Row(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
child: SizedBox(
onPressed: () {
_resetFilter();
},
)
),
],
),
),
body: Container(
child: Form(
key: _formKey,
child: ListView(
children: <Widget>[
Column(
children: <Widget>[
Container(
child: Column(
children: <Widget>[
Container(...), // this works
Column(...),
Container(...), // this works
Container(
child: GridView.count(
// ...
children:
List.generate(oriSkills.length, (int i) {
bool isSkillExist = false;
if (_selectedSkills.contains(rc.titleCase)) {
isSkillExist = true;
} else {
isSkillExist = false;
}
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: isSkillExist, // this boolean showed correct value from the above logic
onChange: onSkillChange,
);
}),
),
),
],
),
)
],
)
],
)),
),
);
}
void _resetFilter() {
setState(() {
_theValue = 0.0;
searchC.text = "";
_selectedSkills = []; // this is the variable that I'd like the GridView to recreate from.
});
}
}
I tried to print one of the field name inside Child widget, but it always showing the old value instead of the new one.
Even after presing the button, it does passing correct value to ChildState.
class ChildState extends State<Child> {
final String name;
final MyCallbackFunction onChange;
bool skillSelect;
double size = 60.0;
ChildState({this.name, this.skillSelect, this.onChange});
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
void setSkillLevel() {
setState(() {
if (skillSelect) {
skillSelect = false;
onChange(name, false);
} else {
skillSelect = true;
onChange(name, true);
}
});
}
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(children: <Widget>[
InkResponse(
onTap: setSkillLevel,
child: Container(
height: size,
width: size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(skillSelect), BlendMode.color),
),
),
)),
]));
}
}
How can I update the Child widget to have the updated value from the Parent widget after reset button is pressed?
You might want to pass the values to the actual Child class. Not to its state.
The class is whats rebuilding once your parent rebuilds. So the new values will be reflected.
So your Child implementation should look something like this (don't forget to replace the onChange Type to your custom Function.
class Child extends StatefulWidget {
final String name;
final Function(void) onChange;
final bool skillSelect;
final double size;
final Function(bool) onSkillLevelChanged;
const Child({Key key, this.name, this.onChange, this.skillSelect, this.size, this.onSkillLevelChanged}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
InkResponse(
onTap: () {
if (widget.onSkillLevelChanged != null) {
widget.onSkillLevelChanged(!widget.skillSelect);
}
},
child: Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(widget.skillSelect), BlendMode.color),
),
),
)),
],
),
);
}
}
In this case the Child ist not responsible anymore for managing its skillSelect property. It simply calls a Function on its parent. The parent then builds with a new skillSelect boolean.
So you might use this child like this:
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: oriSkills[i]['isSkillExist'],
onChange: onSkillChange,
onSkillLevelChanged: (newSkillLevel) {
setState(() {
oriSkills[i]['isSkillExist'] = newSkillLevel;
});
},
);
I'm making an image gallery and I need the user to be able to long-press an image to show a popup menu which will let him delete the image.
My code, so far:
return GestureDetector(
onLongPress: () {
showMenu(
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: Image.memory(
this._asset.thumbData.buffer.asUint8List(),
fit: BoxFit.cover,
gaplessPlayback: true,
),
);
Which produces:
But also, I couldn't find out how to completely remove the image's widget when the longPress function is called. How to do so?
The OP and the First Answerer bypassed the original problem using PopupMenuButton, which worked fine in their case. But I think the more general question of how to position one's own menu and how to receive the user's response without using PopupMenuButton is worth answering, because sometimes we want a popup menu on a custom widget, and we want it to appear on some gestures other than a simple tap (e.g. the OP's original intention was to long-press).
I set out to make a simple app demonstrating the following:
Use a GestureDetector to capture long-press
Use the function showMenu() to display a popup menu, and position it near the finger's touch
How to receive the user's selection
(Bonus) How to make a PopupMenuEntry that represents multiple values (the oft-used PopupMenuItem can only represent a single value)
The result is, when you long-press on a big yellow area, a popup menu appears on which you can select +1 or -1, and the big number would increment or decrement accordingly:
Skip to the end for the entire body of code. Comments are sprinkled in there to explain what I am doing. Here are a few things to note:
showMenu()'s position parameter takes some effort to understand. It's a RelativeRect, which represents how a smaller rect is positioned inside a bigger rect. In our case, the bigger rect is the entire screen, the smaller rect is the area of touch. Flutter positions the popup menu according to these rules (in plain English):
if the smaller rect leans toward the left half of the bigger rect, the popup menu would align with the smaller rect's left edge
if the smaller rect leans toward the right half of the bigger rect, the popup menu would align with the smaller rect's right edge
if the smaller rect is in the middle, which edge wins depends on the language's text direction. Left edge wins if using English and other left-to-right languages, right edge wins otherwise.
It's always useful to reference PopupMenuButton's official implementation to see how it uses showMenu() to display the menu.
showMenu() returns a Future. Use Future.then() to register a callback to handle user selection. Another option is to use await.
Remember that PopupMenuEntry is a (subclass of) StatefulWidget. You can layout any number of sub-widgets inside it. This is how you represent multiple values in a PopupMenuEntry. If you want it to represent two values, just make it contain two buttons, however you want to lay them out.
To close the popup menu, use Navigator.pop(). Flutter treats popup menus like a smaller "page". When we display a popup menu, we are actually pushing a "page" to the navigator's stack. To close a popup menu, we pop it from the stack, thus completing the aforementioned Future.
Here is the full code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
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 _count = 0;
var _tapPosition;
void _showCustomMenu() {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
position: RelativeRect.fromRect(
_tapPosition & const Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
)
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: _storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(
fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
If you are going to use a gridView or listview for laying out the images on the screen, you can wrap each item with a gesture detector then you should keep your images in a list somewhere, then simply remove the image from the list and call setState().
Something like the following. (This code will probably won't compile but it should give you the idea)
ListView.builder(
itemCount: imageList.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onLongPress: () {
showMenu(
onSelected: () => setState(() => imageList.remove(index))}
items: <PopupMenuEntry>[
PopupMenuItem(
value: this._index,
child: Row(
children: <Widget>[
Icon(Icons.delete),
Text("Delete"),
],
),
)
],
context: context,
);
},
child: imageList[index],
);
}
)
Edit: You can use a popup menu too, like following
Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 100,
width: 100,
child: PopupMenuButton(
child: FlutterLogo(),
itemBuilder: (context) {
return <PopupMenuItem>[new PopupMenuItem(child: Text('Delete'))];
},
),
),
Building on the answers by Nick Lee and hacker1024, but instead of turning the solution into a mixin, you could simply just turn it into a widget:
class PopupMenuContainer<T> extends StatefulWidget {
final Widget child;
final List<PopupMenuEntry<T>> items;
final void Function(T) onItemSelected;
PopupMenuContainer({#required this.child, #required this.items, #required this.onItemSelected, Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => PopupMenuContainerState<T>();
}
class PopupMenuContainerState<T> extends State<PopupMenuContainer<T>>{
Offset _tapDownPosition;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details){
_tapDownPosition = details.globalPosition;
},
onLongPress: () async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
T value = await showMenu<T>(
context: context,
items: widget.items,
position: RelativeRect.fromLTRB(
_tapDownPosition.dx,
_tapDownPosition.dy,
overlay.size.width - _tapDownPosition.dx,
overlay.size.height - _tapDownPosition.dy,
),
);
widget.onItemSelected(value);
},
child: widget.child
);
}
}
And then you'd use it like this:
child: PopupMenuContainer<String>(
child: Image.asset('assets/image.png'),
items: [
PopupMenuItem(value: 'delete', child: Text('Delete'))
],
onItemSelected: (value) async {
if( value == 'delete' ){
await showDialog(context: context, child: AlertDialog(
title: Text('Delete image'),
content: Text('Are you sure you want to delete the image?'),
actions: [
uiFlatButton(child: Text('NO'), onTap: (){ Navigator.of(context).pop(false); }),
uiFlatButton(child: Text('YES'), onTap: (){ Navigator.of(context).pop(true); }),
],
));
}
},
),
Adjust the code to fit your needs.
Nick Lee's answer can be turned into a mixin quite easily, which can then be used anywhere you want to use a popup menu.
The mixin:
import 'package:flutter/material.dart' hide showMenu;
import 'package:flutter/material.dart' as material show showMenu;
/// A mixin to provide convenience methods to record a tap position and show a popup menu.
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) => _tapPosition = details.globalPosition;
/// Use this method to show the menu.
Future<T> showMenu<T>({
#required BuildContext context,
#required List<PopupMenuEntry<T>> items,
T initialValue,
double elevation,
String semanticLabel,
ShapeBorder shape,
Color color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition.dx,
_tapPosition.dy,
overlay.size.width - _tapPosition.dx,
overlay.size.height - _tapPosition.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
captureInheritedThemes: captureInheritedThemes,
useRootNavigator: useRootNavigator,
);
}
}
And then, to use it:
import 'package:flutter/material.dart';
import './custom_context_menu.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Popup Menu Usage',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Popup Menu Usage'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with CustomPopupMenu {
var _count = 0;
void _showCustomMenu() {
this.showMenu(
context: context,
items: <PopupMenuEntry<int>>[PlusMinusEntry()],
)
// This is how you handle user selection
.then<void>((int delta) {
// delta would be null if user taps on outside the popup menu
// (causing it to close without making selection)
if (delta == null) return;
setState(() {
_count = _count + delta;
});
});
// Another option:
//
// final delta = await showMenu(...);
//
// Then process `delta` however you want.
// Remember to make the surrounding function `async`, that is:
//
// void _showCustomMenu() async { ... }
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
// This does not give the tap position ...
onLongPress: _showCustomMenu,
// Have to remember it on tap-down.
onTapDown: storePosition,
child: Container(
color: Colors.amberAccent,
padding: const EdgeInsets.all(100.0),
child: Text(
'$_count',
style: const TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
class PlusMinusEntry extends PopupMenuEntry<int> {
#override
double height = 100;
// height doesn't matter, as long as we are not giving
// initialValue to showMenu().
#override
bool represents(int n) => n == 1 || n == -1;
#override
PlusMinusEntryState createState() => PlusMinusEntryState();
}
class PlusMinusEntryState extends State<PlusMinusEntry> {
void _plus1() {
// This is how you close the popup menu and return user selection.
Navigator.pop<int>(context, 1);
}
void _minus1() {
Navigator.pop<int>(context, -1);
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(child: FlatButton(onPressed: _plus1, child: Text('+1'))),
Expanded(child: FlatButton(onPressed: _minus1, child: Text('-1'))),
],
);
}
}
Answer for 2023
In Flutter 3.7 there is now a ContextMenuRegion widget that you can wrap around any existing widget. When the user long presses or right-clicks (depending on the platform), the menu you give it will appear.
return Scaffold(
body: Center(
child: ContextMenuRegion(
contextMenuBuilder: (context, offset) {
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: TextSelectionToolbarAnchors(
primaryAnchor: offset,
),
buttonItems: <ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () {
ContextMenuController.removeAny();
},
label: 'Save',
),
],
);
},
child: const SizedBox(
width: 200.0,
height: 200.0,
child: FlutterLogo(),
),
),
),
);
I am using RepaintBoundary to take the screenshot of the current widget which is a listView. But it only captures the content which is visible on the screen at the time.
RepaintBoundary(
key: src,
child: ListView(padding: EdgeInsets.only(left: 10.0),
scrollDirection: Axis.horizontal,
children: <Widget>[
Align(
alignment: Alignment(-0.8, -0.2),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: listLabel(orientation),
)
),
Padding(padding: EdgeInsets.all(5.0)),
Align(
alignment: FractionalOffset(0.3, 0.5),
child: Container(
height: orientation == Orientation.portrait? 430.0: 430.0*0.7,
decoration: BoxDecoration(
border: Border(left: BorderSide(color: Colors.black))
),
//width: 300.0,
child:
Wrap(
direction: Axis.vertical,
//runSpacing: 10.0,
children: colWidget(orientation),
)
)
),
Padding(padding: EdgeInsets.all(5.0)),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: listLabel(orientation),
)
],
),
);
screenshot function:
Future screenshot() async {
RenderRepaintBoundary boundary = src.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
print(pngBytes);
final directory = (await getExternalStorageDirectory()).path;
File imgFile =new File('$directory/layout2.pdf');
imgFile.writeAsBytes(pngBytes);
}
Is there any way, so that I can capture the whole listView, i.e., not only the content which is not visible on the screen but the scrollable content also. Or maybe if the whole widget is too large to fit in a picture, it can be captured in multiple images.
I achieve the solution of this problem using this package: Screenshot, that takes a screenshot of the entire widget. It's easy and simple, follow the steps on the PubDev or GitHub and you can make it work.
OBS: To take a full screenshot of the widget make sure that your widget is fully scrollable, and not just a part of it.
(In my case, i had a ListView inside a Container, and the package doesn't take the screenshot of all ListView because i have many itens on it, SO i have wrap my Container inside a SingleChildScrollView and add the NeverScrollableScrollPhysics physics in the ListView and it works! :D).
Screenshot of my screen
More details in this issue
This made me curious whether it was possible so I made a quick mock-up that shows it does work. But please be aware that by doing this you're essentially intentionally breaking the things flutter does to optimize, so you really shouldn't use it beyond where you absolutely have to.
Anyways, here's the code:
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(MyApp());
class UiImagePainter extends CustomPainter {
final ui.Image image;
UiImagePainter(this.image);
#override
void paint(ui.Canvas canvas, ui.Size size) {
// simple aspect fit for the image
var hr = size.height / image.height;
var wr = size.width / image.width;
double ratio;
double translateX;
double translateY;
if (hr < wr) {
ratio = hr;
translateX = (size.width - (ratio * image.width)) / 2;
translateY = 0.0;
} else {
ratio = wr;
translateX = 0.0;
translateY = (size.height - (ratio * image.height)) / 2;
}
canvas.translate(translateX, translateY);
canvas.scale(ratio, ratio);
canvas.drawImage(image, new Offset(0.0, 0.0), new Paint());
}
#override
bool shouldRepaint(UiImagePainter other) {
return other.image != image;
}
}
class UiImageDrawer extends StatelessWidget {
final ui.Image image;
const UiImageDrawer({Key key, this.image}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: UiImagePainter(image),
);
}
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
GlobalKey<OverRepaintBoundaryState> globalKey = GlobalKey();
ui.Image image;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
body: image == null
? Capturer(
overRepaintKey: globalKey,
)
: UiImageDrawer(image: image),
floatingActionButton: image == null
? FloatingActionButton(
child: Icon(Icons.camera),
onPressed: () async {
var renderObject = globalKey.currentContext.findRenderObject();
RenderRepaintBoundary boundary = renderObject;
ui.Image captureImage = await boundary.toImage();
setState(() => image = captureImage);
},
)
: FloatingActionButton(
onPressed: () => setState(() => image = null),
child: Icon(Icons.remove),
),
),
);
}
}
class Capturer extends StatelessWidget {
static final Random random = Random();
final GlobalKey<OverRepaintBoundaryState> overRepaintKey;
const Capturer({Key key, this.overRepaintKey}) : super(key: key);
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: OverRepaintBoundary(
key: overRepaintKey,
child: RepaintBoundary(
child: Column(
children: List.generate(
30,
(i) => Container(
color: Color.fromRGBO(random.nextInt(256), random.nextInt(256), random.nextInt(256), 1.0),
height: 100,
),
),
),
),
),
);
}
}
class OverRepaintBoundary extends StatefulWidget {
final Widget child;
const OverRepaintBoundary({Key key, this.child}) : super(key: key);
#override
OverRepaintBoundaryState createState() => OverRepaintBoundaryState();
}
class OverRepaintBoundaryState extends State<OverRepaintBoundary> {
#override
Widget build(BuildContext context) {
return widget.child;
}
}
What it's doing is making a scroll view that encapsulates the list (column), and making sure the repaintBoundary is around the column. With your code where you use a list, there's no way it can ever capture all the children as the list is essentially a repaintBoundary in and of itself.
Note in particular the 'overRepaintKey' and OverRepaintBoundary. You might be able to get away without using it by iterating through render children, but it makes it a lot easier.
There is a simple way
You need wrap SingleChildScrollView Widget to RepaintBoundary. just wrap your Scrollable widget (or his father) with SingleChildScrollView
SingleChildScrollView(
child: RepaintBoundary(
key: _globalKey
)
)
I'm trying to add a ListView using a custom SliverFooter as defined in this question which creates a RenderSliverFooter to create a Footer in a ListView that is always at the bottom of the screen like this design at https://i.stack.imgur.com/hE8Tn.png.
class SliverFooter extends SingleChildRenderObjectWidget {
/// Creates a sliver that fills the remaining space in the viewport.
const SliverFooter({
Key key,
Widget child,
}) : super(key: key, child: child);
#override
RenderSliverFooter createRenderObject(BuildContext context) => new RenderSliverFooter();
}
class RenderSliverFooter extends RenderSliverSingleBoxAdapter {
/// Creates a [RenderSliver] that wraps a [RenderBox] which is sized to fit
/// the remaining space in the viewport.
RenderSliverFooter({
RenderBox child,
}) : super(child: child);
#override
void performLayout() {
final extent = constraints.remainingPaintExtent - math.min(constraints.overlap, 0.0);
var childGrowthSize = .0; // added
if (child != null) {
// changed maxExtent from 'extent' to double.infinity
child.layout(constraints.asBoxConstraints(minExtent: extent, maxExtent: double.infinity), parentUsesSize: true);
childGrowthSize = constraints.axis == Axis.vertical ? child.size.height : child.size.width; // added
}
final paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: extent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
geometry = new SliverGeometry(
// used to be this : scrollExtent: constraints.viewportMainAxisExtent,
scrollExtent: math.max(extent, childGrowthSize),
paintExtent: paintedChildSize,
maxPaintExtent: paintedChildSize,
hasVisualOverflow: extent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
if (child != null) {
setChildParentData(child, constraints, geometry);
}
}
}
As stated in the answer you can use the SliverFooter like this:
new CustomScrollView(
slivers: <Widget>[
new SliverFixedExtentList(
itemExtent: 42.0,
delegate: new SliverChildBuilderDelegate((context, index) {
return new SizedBox.expand(
child: new Card(),
);
}, childCount: 42),
),
new SliverFooter(
child: new Align(
alignment: Alignment.bottomCenter,
child: new Container(
height: 42.0,
color: Colors.red,
),
),
),
],
),
The issue is when trying to add a ListView to the SliverFooter there is this error: A RenderSliverFooter expected a child of type RenderBox but received a child of type RenderSliverList.
Is there another way to create a SliverFooter so I can display another ListView in the footer? The current way only can display a fixed height it seems.