Flutter Drag and Drop - flutter

I'm facing a problem implementing drag and drop in my project. I want dynamically add and delete draggable elements. The problem is that I can't understand how to get the reference to the widget I'm currently moving so that I can understand which coordinates in the list I have to change.
Here is an example of the code where I use the static number of draggable widgets. They are assigned with coordinates from the list. But what if I have to dynamically add and delete those draggable widgets how can I understand which coordinates to change?
So, I have an array of draggble elements and array of coordinates. Widget with index 0 refers to coordinates with index 0. How can I understand which widget I'm currently moving so that I can get the index of it in array of widgets and then change proper coordinates in array of coordinates.
class DragAndDrop extends StatefulWidget {
const DragAndDrop({
Key? key,
this.width,
this.height,
}) : super(key: key);
final double? width;
final double? height;
#override
_DragAndDropState createState() => _DragAndDropState();
}
class _DragAndDropState extends State<DragAndDrop> {
List<double?> _x = [0.0, 20.0];
List<double?> _y = [0.0, 20.0];
int k = -1;
List<Widget> pel = [];
final GlobalKey stackKey = GlobalKey();
#override
Widget build(BuildContext context) {
pel.add(Container(color: Colors.blue));
k++;
Widget drag = Draggable<int>(
data: k,
child: Icon(
Icons.keyboard_arrow_down,
color: Color(0x95000000),
size: 40,
),
feedback: Icon(
Icons.keyboard_arrow_down,
color: Color.fromRGBO(212, 14, 14, 0.584),
size: 40,
),
childWhenDragging: Container(),
onDragStarted: () {},
onDragEnd: (dragDetails) {
setState(() {
final parentPos = stackKey.globalPaintBounds;
if (parentPos == null) return;
if (dragDetails.offset.dx - parentPos.left < 0)
_x[0] = 0;
else if (dragDetails.offset.dx - parentPos.left >
double.parse(widget.width.toString()) - 40)
_x[0] = double.parse(widget.width.toString()) - 40;
else
_x[0] = dragDetails.offset.dx - parentPos.left;
if (dragDetails.offset.dy - parentPos.top < 0)
_y[0] = 0;
else if (dragDetails.offset.dy - parentPos.top >
double.parse(widget.height.toString()) - 40)
_y[0] = double.parse(widget.height.toString()) - 40;
else
_y[0] = dragDetails.offset.dy - parentPos.top;
});
},
);
pel.add(Positioned(
left: _x[0],
top: _y[0],
child: drag,
));
pel.add(Positioned(
left: _x[1],
top: _y[1],
child: Draggable<int>(
data: k,
child: Icon(
Icons.keyboard_arrow_down,
color: Color(0x95000000),
size: 40,
),
feedback: Icon(
Icons.keyboard_arrow_down,
color: Color.fromRGBO(212, 14, 14, 0.584),
size: 40,
),
childWhenDragging: Container(),
onDragStarted: () {},
onDragEnd: (dragDetails) {
setState(() {
final parentPos = stackKey.globalPaintBounds;
if (parentPos == null) return;
_x[1] = dragDetails.offset.dx - parentPos.left; // 11.
_y[1] = dragDetails.offset.dy - parentPos.top;
});
},
),
));
return Stack(
key: stackKey,
fit: StackFit.expand,
children: pel,
);
}
}
extension GlobalKeyExtension on GlobalKey {
Rect? get globalPaintBounds {
final renderObject = currentContext?.findRenderObject();
var translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
return renderObject!.paintBounds
.shift(Offset(translation.x, translation.y));
} else {
return null;
}
}
}
I tried to use a variable that I can assign to Dragble.data field but I'm not able to get it inside the widget.

Related

Flutter: How to rotate diagonally a rect within a plane to paint

I have a word search app in which I use Rect to paint over a grid.
I have a gesture detector that allows me to identify which grid fields have been selected, through the global key and using onPanStart, onPanUpdate and onPanEnd I can see all the words that have been marked.
Everything works fine though, I don't know how to add a diagonal marker.
I currently get this result:
I would like to implement this diagonal painting
Demonstration of how the marking is currently, ps: The Gesture Detector still needs to be improved for painting, diagonally
I tried to add the RotationTransition, but the behavior didn't look good, below.
RotationTransition(
alignment: Alignment.centerLeft,
turns: new AlwaysStoppedAnimation(45 / 360),
child: Container(
padding: const EdgeInsets.all(2.0),
child: DecoratedBox(
decoration: BoxDecoration(
color: colorSelection!.withOpacity(0.4),
border: Border.all(
color: colorSelection!.withOpacity(0.1),
//width: width,
),
borderRadius: BorderRadius.circular(radius),
),
),
),
)
An overview of the important parts
Widget build(BuildContext context) {
return Stack(
children: [
GridView.count(
physics: NeverScrollableScrollPhysics(),
childAspectRatio: letterWidth / letterHeight,
crossAxisCount: nCols,
children: puzzle.asMap().map(
(index, letter) => MapEntry(
index,
BoardLetter(
letter,
isSelected: selection.contains(index),
isHit: hitIndexes.contains(index),
key: uniqueLetters[index]['key'],
),
),
)
.values
.toList(),
),
...markers,
Positioned(
width: widget.width,
height: widget.height,
child: RotationTransition(
turns: new AlwaysStoppedAnimation(360 / 360),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart: onPanStart,
onPanEnd: onPanEnd,
onPanUpdate: onPanUpdate,
),
),
),
],
);
Selection checks words and markers.last paints areas.
void onPanUpdate(DragUpdateDetails details) {
final currentIndex = computeLetterIndex(details.localPosition);
if(currentIndex >= 0 && currentIndex <= 99){
final key = uniqueLetters[currentIndex]['key'];
final RenderBox renderBox = key.currentContext.findRenderObject();
print("render box size ${renderBox.localToGlobal(Offset.zero, ancestor: context.findRenderObject())}");
markerRect = renderBox.localToGlobal(Offset.zero, ancestor: context.findRenderObject()) & renderBox.size;
List<int> _selection = [];
if (checkSameRow(startIndex, currentIndex)) {
if(direction == "horizontal"){
markers.last = adjustedMarker(markers.last, markerRect!);
_selection = genSelection(startIndex, currentIndex, 1);
}
} else if (checkSameCol(startIndex, currentIndex)) {
//print("direction $direction");
if(direction == "vertical"){
markers.last = adjustedMarker(markers.last, markerRect!);
_selection = genSelection(startIndex, currentIndex, nCols);
}
} else if (checkSameMainDiagonal(startIndex, currentIndex)) {
markers.last = adjustedMarker(markers.last, markerRect!);
_selection = genSelection(startIndex, currentIndex, nCols + 1);
} else if (checkSameCounterDiagonal(startIndex, currentIndex)) {
markers.last = adjustedMarker(markers.last, markerRect!);
_selection = genSelection(startIndex, currentIndex, nCols - 1);
}
setState(() {
selection = _selection;
});
}
}
It is checked if a word was found, otherwise it removes the paint(markers.last)
void onPanEnd(DragEndDetails details) {
final word = selection
.map((index) => puzzle[index])
.fold("", (dynamic value, letter) => value + letter);
// Check if this is a valid word
var reversedWord = word.split('').reversed.join();
var wordIndex = widget.words
.indexWhere((gameWord) => gameWord == word || gameWord == reversedWord);
if (wordIndex != -1) {
print("word $word/$reversedWord was hit");
widget.onHitWord(word, wordIndex);
this.setState(() {
direction = "";
colorNumber = colorNumber + 1 ;
hitIndexes = List.from(hitIndexes)..addAll(selection);
});
}else{
setState(() {
direction = "";
markers.removeLast();
selection = [];
});
}
}
The initial position is captured and marked.
void onPanStart(DragStartDetails details) {
startIndex = computeLetterIndex(details.localPosition);
final currentIndex = computeLetterIndex(details.localPosition);
final key = uniqueLetters[currentIndex]['key'];
final renderBox = key.currentContext.findRenderObject();
print(uniqueLetters[currentIndex]['letter']);
setState(() {
markerRect = renderBox.localToGlobal(Offset.zero, ancestor: context.findRenderObject()) & renderBox.size;
addMarker(markerRect, currentIndex);
});
}
The bookmark is added
void addMarker(Rect rect, int startIndex) {
markers.add(
WordMarker(
rect: rect,
startIndex: startIndex,
colorSelection: getColor(context),
color: getColor(context),
));
}
Markers are adjusted with rect
WordMarker adjustedMarker(WordMarker originalMarker, Rect endRect) {
originalMarker.colorSelection = getColor(context);
originalMarker.copyWith(rect: originalMarker.rect!.inflate(20.0));
return originalMarker.copyWith(rect: originalMarker.rect!.expandToInclude(endRect));
}

Progressive Menu in Flutter (Hide Overflowing Elements in Dropdown)

I'm building a resizable Flutter desktop app and I'm wondering how it would be possible to automatically hide overflowing items in a menu (e.g. a Row) and making them visible in a "More" dropdown instead.
Conceptual Sample Images (Source: https://css-tricks.com/container-adapting-tabs-with-more-button/)
A lot of width available (shows all items):
Less width available (hides items in dropdown):
Thanks in advance!
Update: I had the idea of using a Wrap-element for it, but then I've had the following problems:
How do I limit the Wrap to only show one line of children? (related to https://github.com/flutter/flutter/issues/65331)
How do I get the info on which of the elements are on line 1 and which are hidden.
Maybe you can try creating a MultiChildRenderObjectWidget in which you can calculate the children size before painting. It's more complicated though because your like making a custom Row class.
I created a sample but it may still contain bugs and need some improvements.
Sample...
threshold
collapsible_menu_bar.dart
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class MenuBar extends StatelessWidget {
MenuBar({
required this.onItemPressed,
required this.children,
this.minItemWidth = 110,
this.minMoreItemsWidth = 70,
Key? key,
}) : super(key: key);
final ValueChanged<int> onItemPressed;
final List<MenuBarItem> children;
final double minItemWidth;
final double minMoreItemsWidth;
static int tmpStartIndex = 0;
#override
Widget build(BuildContext context) {
return CollapsibleMenuBar(
onCollapseIndex: (int startIndex) {
if (tmpStartIndex == startIndex) {
return;
}
tmpStartIndex = startIndex;
},
minItemWidth: minItemWidth,
minMoreItemsWidth: minMoreItemsWidth,
children: [
...children,
PopupMenuButton(
offset: const Offset(0, 40),
color: Colors.red,
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.center,
color: Colors.amber,
child: const Text('More'),
),
itemBuilder: (_) => children
.sublist(tmpStartIndex)
.map((e) => PopupMenuItem(child: e))
.toList(),
),
],
);
}
}
///
///
///
class MenuBarItem extends StatelessWidget {
const MenuBarItem({
required this.onPressed,
required this.text,
Key? key,
}) : super(key: key);
final VoidCallback? onPressed;
final String text;
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
style: TextButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text(
text,
style: const TextStyle(color: Colors.white),
),
);
}
}
///
///
///
class CollapsibleMenuBar extends MultiChildRenderObjectWidget {
CollapsibleMenuBar({
required this.onCollapseIndex,
required List<Widget> children,
required this.minItemWidth,
required this.minMoreItemsWidth,
Key? key,
}) : super(key: key, children: children);
final ValueChanged<int> onCollapseIndex;
final double minItemWidth;
final double minMoreItemsWidth;
#override
RenderObject createRenderObject(BuildContext context) {
return RenderCollapsibleMenuBar(
onCollapseIndex,
minItemWidth,
minMoreItemsWidth,
);
}
}
///
///
///
class CollapsibleMenuBarParentData extends ContainerBoxParentData<RenderBox> {}
///
///
///
class RenderCollapsibleMenuBar extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, CollapsibleMenuBarParentData>,
RenderBoxContainerDefaultsMixin<RenderBox,
CollapsibleMenuBarParentData> {
RenderCollapsibleMenuBar(
this.onCollapseIndex,
this.minItemWidth,
this.minMoreItemsWidth,
);
final ValueChanged<int> onCollapseIndex;
final double minItemWidth;
final double minMoreItemsWidth;
#override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! CollapsibleMenuBarParentData) {
child.parentData = CollapsibleMenuBarParentData();
}
}
#override
void performLayout() {
// Make width of children equal.
final double childWidth = max(
constraints.maxWidth / (childCount - 1),
minItemWidth,
);
double totalWidth = 0;
double totalHeight = 0;
RenderBox? child = firstChild;
Offset childOffset = Offset(0, 0);
int childIdx = 0;
while (child != null && child != lastChild) {
CollapsibleMenuBarParentData childParentData =
child.parentData as CollapsibleMenuBarParentData;
// Set child's dimension.
child.layout(
BoxConstraints(
minWidth: childWidth,
maxWidth: childWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// If the total width exceeds the max screen width,
// display "more" item.
if (totalWidth + child.size.width > constraints.maxWidth) {
// Set overflow item dimension to 0.
child.layout(
BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// Get popup menu item.
child = lastChild!;
childParentData = child.parentData as CollapsibleMenuBarParentData;
// Set popup menu item's dimension. Will cover the remaining width.
child.layout(
BoxConstraints(
minWidth: constraints.maxWidth - totalWidth,
maxWidth: constraints.maxWidth - totalWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
}
if (child == lastChild) {
// If "more" item's width is below threshold, hide left item.
if (child.size.width <= minMoreItemsWidth) {
childIdx--;
RenderBox nthChild = getChildrenAsList()[childIdx];
// Hide left item of "more" item.
totalWidth -= nthChild.size.width;
childOffset -= Offset(nthChild.size.width, 0);
nthChild.layout(
BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
// Resize "more" item.
child.layout(
BoxConstraints(
minWidth: constraints.maxWidth - totalWidth,
maxWidth: constraints.maxWidth - totalWidth,
maxHeight: constraints.maxHeight,
),
parentUsesSize: true,
);
}
// Update the start index of children to be displayed
// in "more" items.
onCollapseIndex(childIdx);
}
totalWidth += child.size.width;
totalHeight = max(totalHeight, child.size.height);
childParentData.offset = Offset(childOffset.dx, 0);
childOffset += Offset(child.size.width, 0);
if (child != lastChild) {
childIdx++;
}
child = childParentData.nextSibling;
}
// If all children is displayed except for "more" item.
if (childIdx == childCount - 1) {
// Set the layout of popup button to size 0.
lastChild!.layout(BoxConstraints(
minWidth: 0,
maxWidth: 0,
maxHeight: constraints.maxHeight,
));
}
size = Size(totalWidth, totalHeight);
}
#override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return defaultHitTestChildren(result, position: position);
}
}
Usage:
class HomePage extends StatelessWidget {
final List<String> _data = const <String>[
'Falkenberg',
'Braga',
'Stockholm',
'Trnnava',
'Plodiv',
'Klaipeda',
'Punta Cana',
'Lisbon',
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: MenuBar(
onItemPressed: (int i) {},
children: _data
.map((String data) => MenuBarItem(
onPressed: () => print(data),
text: data,
))
.toList(),
),
),
],
),
);
}
}
Try watching this video to learn more how it works.

Mimic iOS contact form AppBar

I'm trying to mimic iOS contact form app bar.
expanded
collapsed
Here is where I get so far
Main Screen
class CompanyScreen extends StatefulWidget {
#override
_CompanyScreenState createState() => _CompanyScreenState();
}
class _CompanyScreenState extends State<CompanyScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: SafeAreaPersistentHeaderDelegate(
expandedHeight: 200,
flexibleSpace:
SafeArea(child: Image.asset('assets/images/user.png'))),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
}
}
SliverHeader
class SafeAreaPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget title;
final Widget flexibleSpace;
final double expandedHeight;
SafeAreaPersistentHeaderDelegate(
{this.title, this.flexibleSpace, this.expandedHeight});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[
Container(
height: 60,
child: FlatButton(
child: Text('Done'),
),
)
],
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title,
flexibleSpace: (title == null && flexibleSpace != null)
? Semantics(child: flexibleSpace, header: true)
: flexibleSpace,
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
#override
double get maxExtent => expandedHeight;
#override
double get minExtent => 80;
#override
bool shouldRebuild(SafeAreaPersistentHeaderDelegate old) {
if (old.flexibleSpace != flexibleSpace) {
return true;
}
return false;
}
}
UPDATE: It all works but I have a problem add the text under the image (Add Photo) and make that text disappear when collapsed. With this solution, if I wrap the image into a column then image expands overflow and doesn't scale.
Requirements:
AppBar and the flex area must be in safe area
Widget with image must have text at the bottom which can be changed dynamically (Add image or Change image) and it must be clickable
The text under the image area must disappear when flex area is collapsed with some transition
Ability to add title in app bar lined up with action buttons
When title in app bar is provided then flex area should scale bellow the title, if not flex area should scale into the title area as on the above image
Any help with this greatly appreciated
I gave it a try.. I'm not an expert on slivers so this solution might not be perfect. I have taken your code as starting point. The column seems to deactivate all scaling so I scaled all manually.
here is your app bar
UPDATE I have tweaked it a little so it feels more like iOS app bar plus I've added extra feature
import 'dart:math';
import 'package:flutter/material.dart';
double _defaultTextHeight = 14;
double _defaultTextPadding = 5;
double _defaultAppBarHeight = 60;
double _defaultMinAppBarHeight = 40;
double _unknownTextValue = 1;
class AppBarSliverHeader extends SliverPersistentHeaderDelegate {
final String title;
final double expandedHeight;
final double safeAreaPadding;
final Widget flexibleImage;
final double flexibleSize;
final String flexibleTitle;
final double flexiblePadding;
final bool flexToTop;
final Function onTap;
final Widget rightButton;
final Widget leftButton;
AppBarSliverHeader(
{this.title,
this.onTap,
this.flexibleImage,
#required this.expandedHeight,
#required this.safeAreaPadding,
this.flexibleTitle = '',
this.flexToTop = false,
this.leftButton,
this.rightButton,
this.flexibleSize = 30,
this.flexiblePadding = 4});
double _textPadding(double shrinkOffset) {
return _defaultTextPadding * _scaleFactor(shrinkOffset);
}
double _widgetPadding(double shrinkOffset) {
double offset;
if (title == null) {
offset = _defaultMinAppBarHeight * _scaleFactor(shrinkOffset);
} else {
if (flexToTop) {
offset = _defaultAppBarHeight * _scaleFactor(shrinkOffset);
} else {
offset = (_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _topOffset(double shrinkOffset) {
double offset;
if (title == null) {
offset = safeAreaPadding +
(_defaultMinAppBarHeight * _scaleFactor(shrinkOffset));
} else {
if (flexToTop) {
offset = safeAreaPadding +
(_defaultAppBarHeight * _scaleFactor(shrinkOffset));
} else {
offset = safeAreaPadding +
((_defaultAppBarHeight - _defaultMinAppBarHeight) *
_scaleFactor(shrinkOffset)) +
_defaultMinAppBarHeight;
}
}
return offset;
}
double _calculateWidgetHeight(double shrinkOffset) {
double actualTextHeight = _scaleFactor(shrinkOffset) * _defaultTextHeight +
_textPadding(shrinkOffset) +
_unknownTextValue;
final padding = title == null
? (2 * flexiblePadding)
: flexToTop ? (2 * flexiblePadding) : flexiblePadding;
final trueMinExtent = minExtent - _topOffset(shrinkOffset);
final trueMaxExtent = maxExtent - _topOffset(shrinkOffset);
double minWidgetSize =
trueMinExtent - padding;
double widgetHeight =
((trueMaxExtent - actualTextHeight) - shrinkOffset) - padding;
return widgetHeight >= minWidgetSize ? widgetHeight : minWidgetSize;
}
double _scaleFactor(double shrinkOffset) {
final ratio = (maxExtent - minExtent) / 100;
double percentageHeight = shrinkOffset / ratio;
double limitedPercentageHeight =
percentageHeight >= 100 ? 100 : percentageHeight;
return 1 - (limitedPercentageHeight / 100);
}
Widget _builtContent(BuildContext context, double shrinkOffset) {
_topOffset(shrinkOffset);
return SafeArea(
bottom: false,
child: Semantics(
child: Padding(
padding: title == null
? EdgeInsets.symmetric(vertical: flexiblePadding)
: flexToTop
? EdgeInsets.symmetric(vertical: flexiblePadding)
: EdgeInsets.only(bottom: flexiblePadding),
child: GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LimitedBox(
maxWidth: _calculateWidgetHeight(shrinkOffset),
maxHeight: _calculateWidgetHeight(shrinkOffset),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(
_calculateWidgetHeight(shrinkOffset))),
color: Colors.white),
child: ClipRRect(
borderRadius: BorderRadius.circular(
_calculateWidgetHeight(shrinkOffset)),
child: flexibleImage,
),
)),
Padding(
padding: EdgeInsets.only(top: _textPadding(shrinkOffset)),
child: Text(
flexibleTitle,
textScaleFactor: _scaleFactor(shrinkOffset),
style: TextStyle(
fontSize: _defaultTextHeight,
color: Colors.white
.withOpacity(_scaleFactor(shrinkOffset)), height: 1),
),
)
],
),
),
),
button: true,
),
);
}
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: 1,
child: AppBar(
actions: <Widget>[rightButton == null ? Container() : rightButton],
leading: leftButton == null ? Container() : leftButton,
backgroundColor: Colors.blue,
automaticallyImplyLeading: false,
title: title != null
? Text(
title,
style: TextStyle(
color: flexToTop
? Colors.white.withOpacity(_scaleFactor(shrinkOffset))
: Colors.white),
)
: null,
flexibleSpace: Padding(
padding: EdgeInsets.only(top: _widgetPadding(shrinkOffset)),
child: _builtContent(context, shrinkOffset),
),
centerTitle: true,
toolbarOpacity: 1,
bottomOpacity: 1.0),
);
return appBar;
}
#override
double get maxExtent => expandedHeight + safeAreaPadding;
#override
double get minExtent => title == null
? _defaultAppBarHeight + safeAreaPadding
: flexToTop
? _defaultAppBarHeight + safeAreaPadding
: _defaultAppBarHeight + safeAreaPadding + flexibleSize;
#override
bool shouldRebuild(AppBarSliverHeader old) {
if (old.flexibleImage != flexibleImage) {
return true;
}
return false;
}
}
and here is usage
Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
floating: true,
delegate: AppBarSliverHeader(
expandedHeight: 250,
safeAreaPadding: MediaQuery.of(context).padding.top,
title: 'New Contact',
flexibleImage: Image.asset('assets/images/avatar.png'),
flexibleTitle: 'Add Image',
flexiblePadding: 6,
flexibleSize: 50,
flexToTop: true,
onTap: () {
print('hello');
},
leftButton: IconButton(
icon: Text('Cancel'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
),
rightButton: IconButton(
icon: Text('Done'),
iconSize: 60,
padding: EdgeInsets.zero,
onPressed: () {},
)),
),
SliverList(
delegate: SliverChildListDelegate([
TextField(),
]),
)
],
),
);
There are some things which took me by surprise as well. First is text size. It seems like text size is not an actual text size so I've added _unknownTextValue there for compensation. Also even if text size is set to 0 then the Text widget has still 1px size so I've compensated that in commented code. Another thing is I wanted to use CircularAvatar for the image but apparently the CircularAvatar widget has built in animation when changing the size which interfere with app bar animation so I've built custom avatar.
UPDATE: To make actual text height same as font size, I have added height property 1 to TextStyle. It seems to work however there is still occasional overflow on the textfield of up to 1px so I've kept _unknownTextValue at 1px
As I said I'm not sliver expert so there might be a better solutions out there so I would suggest you to wait for other answers
NOTE: I only tested it on 2 iOS devices so you should test further to use it
With Title
Without Title
With Title and flexToTop activated

Flutter: The getter 'Options' isn't defined for class 'Marker'

I know flutter has changed Google_Maps_api which is causing multiple errors in my code such as
1) 'Options' isn't defined for class 'Marker'
2) Too Many Positional Arguments : 0 expected 2 found
3) 'addMarker' isn't defined for class 'GoogleMapController'
4) 'clearMarker' isn't defined for class 'GoogleMapController'
I have checked this:
Flutter: How to add marker to Google Maps with new Marker API?
I am new to FLUTTER please help me solve these errors thanks
The Git I am using is :
https://github.com/code4func/flutter-taxiapp
import 'package:fl_uberapp/src/model/place_item_res.dart';
import 'package:fl_uberapp/src/model/step_res.dart';
import 'package:fl_uberapp/src/model/trip_info_res.dart';
import 'package:fl_uberapp/src/repository/place_service.dart';
import 'package:fl_uberapp/src/resources/widgets/car_pickup.dart';
import 'package:fl_uberapp/src/resources/widgets/home_menu.dart';
import 'package:fl_uberapp/src/resources/widgets/ride_picker.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _scaffoldKey = new GlobalKey<ScaffoldState>();
var _tripDistance = 0;
final Map<String, Marker> _markers = <String, Marker>{};
GoogleMapController _mapController;
#override
Widget build(BuildContext context) {
print("build UI");
return Scaffold(
key: _scaffoldKey,
body: Container(
constraints: BoxConstraints.expand(),
color: Colors.white,
child: Stack(
children: <Widget>[
GoogleMap(
// key: ggKey,
// markers: Set.of(markers.values),
onMapCreated: (GoogleMapController controller) {
_mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(10.7915178, 106.7271422),
zoom: 14.4746,
),
),
Positioned(
left: 0,
top: 0,
right: 0,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
AppBar(
backgroundColor: Colors.transparent,
elevation: 0.0,
title: Text(
"Taxi App",
style: TextStyle(color: Colors.black),
),
leading: FlatButton(
onPressed: () {
print("click menu");
_scaffoldKey.currentState.openDrawer();
},
child: Image.asset("ic_menu.png")),
actions: <Widget>[Image.asset("ic_notify.png")],
),
Padding(
padding: EdgeInsets.only(top: 20, left: 20, right: 20),
child: RidePicker(onPlaceSelected),
),
],
),
),
Positioned(left: 20, right: 20, bottom: 40,
height: 248,
child: CarPickup(_tripDistance),
)
],
),
),
drawer: Drawer(
child: HomeMenu(),
),
);
}
void onPlaceSelected(PlaceItemRes place, bool fromAddress) {
var mkId = fromAddress ? "from_address" : "to_address";
_addMarker(mkId, place);
_moveCamera();
_checkDrawPolyline();
}
void _addMarker(String mkId, PlaceItemRes place) async {
_addMarker(MarkerId, place);
// remove old
_markers.remove(mkId);
_mapController.clearMarkers();
_markers[mkId] = Marker(
mkId,
MarkerOptions(
position: LatLng(place.lat, place.lng),
infoWindowText: InfoWindowText(place.name, place.address)));
for (var m in _markers.values) {
await _mapController.addMarker(m.options);
}
}
void _moveCamera() {
print("move camera: ");
print(_markers);
if (_markers.values.length > 1) {
var fromLatLng = _markers["from_address"].options.position;
var toLatLng = _markers["to_address"].options.position;
var sLat, sLng, nLat, nLng;
if(fromLatLng.latitude <= toLatLng.latitude) {
sLat = fromLatLng.latitude;
nLat = toLatLng.latitude;
} else {
sLat = toLatLng.latitude;
nLat = fromLatLng.latitude;
}
if(fromLatLng.longitude <= toLatLng.longitude) {
sLng = fromLatLng.longitude;
nLng = toLatLng.longitude;
} else {
sLng = toLatLng.longitude;
nLng = fromLatLng.longitude;
}
LatLngBounds bounds = LatLngBounds(northeast: LatLng(nLat, nLng), southwest: LatLng(sLat, sLng));
_mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 50));
} else {
_mapController.animateCamera(CameraUpdate.newLatLng(
_markers.values.elementAt(0).options.position));
}
}
void _checkDrawPolyline() {
// remove old polyline
_mapController.clearPolylines();
if (_markers.length > 1) {
var from = _markers["from_address"].options.position;
var to = _markers["to_address"].options.position;
PlaceService.getStep(
from.latitude, from.longitude, to.latitude, to.longitude)
.then((vl) {
TripInfoRes infoRes = vl;
_tripDistance = infoRes.distance;
setState(() {
});
List<StepsRes> rs = infoRes.steps;
List<LatLng> paths = new List();
for (var t in rs) {
paths
.add(LatLng(t.startLocation.latitude, t.startLocation.longitude));
paths.add(LatLng(t.endLocation.latitude, t.endLocation.longitude));
}
// print(paths);
_mapController.addPolyline(PolylineOptions(
points: paths, color: Color(0xFF3ADF00).value, width: 10));
});
}
}
}
Going through the errors that you've received, it seems that you're trying to call non-existent functions from the Objects that you're using.
'Options' isn't defined for class 'Marker' - checking Marker class from the docs, there's no options that you can call.
i.e.
_markers.values.elementAt(0).options.position
var from = _markers["from_address"].options.position;
var to = _markers["to_address"].options.position;
Too Many Positional Arguments : 0 expected 2 found - this error was thrown because there's two arguments that you're trying to use on the method that doesn't expect to receive an argument. Check which line this error was thrown.
As for the last two, GoogleMapController doesn't have addMarker() and clearMarker().
To add markers on GoogleMapController, you need to define markers on GoogleMap widget.
GoogleMap(
markers: Set<Marker>.of(_markers.values),
...
)
Other than that, the rest of the code seems fine.

Flutter: Drag Draggable Stack item inside a Draggable Stack Item

I have a Draggable on a DragTarget as part of a Stack. Inside is another Stack with Draggables, again on DragTargets and so on... (Stack over Stack over Stack etc.).
The Draggable is a Positioned with a Listener telling where to be placed.
homeView.dart
body: Stack(children: [
DraggableWidget(parentKey, Offset(0, 0)),
]),
draggableWidget.dart
class DraggableWidget extends StatefulWidget {
final Key itemKey;
final Offset itemPosition;
DraggableWidget(this.itemKey, this.itemPosition);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
Offset tempDelta = Offset(0, 0);
Window<List<Key>> item;
List<DraggableWidget> childList = [];
Map<Key, Window<List>> structureMap;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
structureMap = Provider.of<Data>(context).structureMap;
if (structureMap[widget.itemKey] != null) {
structureMap[widget.itemKey].childKeys.forEach(
(k) => childList.add(
DraggableWidget(k, item.position),
),
);
} else {
structureMap[widget.itemKey] = Window<List<Key>>(
title: 'App',
key: widget.itemKey,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
position: Offset(0, 0),
color: Colors.blue,
childKeys: []);
}
item = Provider.of<Data>(context).structureMap[widget.itemKey];
return Positioned(
top: item.position.dx,
left: item.position.dy,
child: DragTarget(
builder:
(buildContext, List<Window<List<Key>>> candidateData, rejectData) {
return Listener(
onPointerDown: (PointerDownEvent event) {},
onPointerUp: (PointerUpEvent event) {
setState(() {
item.position = Offset(item.position.dx + tempDelta.dx,
item.position.dy + tempDelta.dy);
tempDelta = Offset(0, 0);
});
},
onPointerMove: (PointerMoveEvent event) {
tempDelta = Offset((event.delta.dy + tempDelta.dx),
(event.delta.dx + tempDelta.dy));
},
child: Draggable(
childWhenDragging: Container(),
feedback: Container(
color: item.color,
height: item.size.height,
width: item.size.width,
),
child: Column(children: [
Text(item.title),
Container(
color: item.color,
height: item.size.height,
width: item.size.width,
child: ItemStackBuilder(widget.itemKey, item.position),
),
]),
data: item),
);
},
),
);
}
}
itemStackBuilder.dart
class ItemStackBuilder extends StatelessWidget {
final Key itemKey;
final Offset itemPosition;
ItemStackBuilder(this.itemKey, this.itemPosition);
#override
Widget build(BuildContext context) {
Map<Key, Window<List<Key>>> structureMap =
Provider.of<Data>(context).structureMap;
if (structureMap[itemKey] == null) {
structureMap[itemKey] = Window(size: Size(20, 20), childKeys: []);
}
return Stack(overflow: Overflow.visible, children: [
...stackItems(context),
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
List<Widget> stackItems(BuildContext context) {
List<Key> childKeyList =
Provider.of<Data>(context).structureMap[itemKey].childKeys;
var stackItemDraggable;
List<Widget> stackItemsList = [];
if (childKeyList == null || childKeyList.length < 1) {
stackItemsList = [Container()];
} else {
for (int i = 0; i < childKeyList.length; i++) {
stackItemDraggable = DraggableWidget(childKeyList[i], itemPosition);
stackItemsList.add(stackItemDraggable);
}
}
return stackItemsList;
}
}
When I want to move the Draggable item on top, the underlying Stack moves.
I tried it with a Listener widget and was able to detect all RenderBoxes inside the Stack.
But how can I select the specific Draggable and/or disable all the other layers? Is it a better idea to forget about Draggables and do it all with Positioned and GestureDetector?
Ok, it was my mistake not of the framework:
on itemStackBuilder.dart I used an additional Container to size the Stack. I was not able to recognise, because color was transparent:
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
After deleting this part, all works fine for now.