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
Related
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.
i'm trying to come up with best way to draw a floor plan in flutter, something like these images, but it would be for regals in one concrete shop, instead of plan of shopping centre with multiple shops.
floor plan 1
floor plan 2
i decided rectangles would be sufficient enough for now and i have multiple ideas on how to execute, but no idea which one is the best. or maybe there is even better one i have not thought of
1. using custom painter
regals have attributes: ax, ay, bx, by, so they go from point a (left bottom) to b (right upper)
code like this
final rect = Rect.fromPoints(
Offset(regal.ax.toDouble(), size.height - regal.ay.toDouble()),
Offset(regal.bx.toDouble(), size.height - regal.by.toDouble()),
);
this is good because it is flexible, there is pretty much unlimited range of options, but using CustomPainter is a bit buggy in my case, alongside with Transform and GestureDetector it bugs out sometimes and instead of clicking on "buttons" you need to track where user clicked, ehm, tapped.
2. using gridView?
i dont have thought this thru as much as first option, but big plus would be using styled buttons as regals, instead of rectangles.
possible problems would be button sizing, if one regal would be times bigger than others.
regal attributes would be order on x axis, order on y axis, x flex (for example 3 as 3 times of base size), y flex
i think i have not thought of the best solution yet.
what would it be?
Here is a quick playground using a Stack of Regals who are just Containers in this quick implementation under 250 lines of code.
Click the FloatActionButton to create random Regal. Then, you can define the position of each Regal and its Size, within the limit of the Floor Plan and Max/min Regal Size.
In this quick implementation, the position of a Regal can be defined both with Gestures or Sliders; while its size can only be defined using the sliders.
Package Dependencies
Riverpod (Flutter Hooks flavor) for State Management
Freezed for Domain classes immutability
Full Source Code (222 lines)
import 'dart:math' show Random;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66478145.floor_plan.freezed.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
),
);
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final regals = useProvider(regalsProvider.state);
return Scaffold(
body: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
Container(
width: kFloorSize.width,
height: kFloorSize.height,
color: Colors.amber.shade100),
...regals
.map(
(regal) => Positioned(
top: regal.offset.dy,
left: regal.offset.dx,
child: GestureDetector(
child: RegalWidget(regal: regal),
),
),
)
.toList(),
],
),
const SizedBox(width: 16.0),
RegalProperties(),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read(regalsProvider).createRegal(),
child: Icon(Icons.add),
),
);
}
}
class RegalWidget extends HookWidget {
final Regal regal;
const RegalWidget({Key key, this.regal}) : super(key: key);
#override
Widget build(BuildContext context) {
final _previousOffset = useState<Offset>(null);
final _refOffset = useState<Offset>(null);
return GestureDetector(
onTap: () => context.read(selectedRegalIdProvider).state = regal.id,
onPanStart: (details) {
_previousOffset.value = regal.offset;
_refOffset.value = details.localPosition;
},
onPanUpdate: (details) => context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: _previousOffset.value +
details.localPosition -
_refOffset.value),
),
child: Container(
width: regal.size.width,
height: regal.size.height,
color: regal.color,
),
);
}
}
class RegalProperties extends HookWidget {
#override
Widget build(BuildContext context) {
final regal = useProvider(selectedRegalProvider);
return Padding(
padding: EdgeInsets.all(16.0),
child: regal == null
? Text('Click a Regal to start')
: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('WIDTH'),
Slider(
min: kRegalMinSize.width,
max: kRegalMaxSize.width,
value: regal.size.width,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(value, regal.size.height)),
),
),
const SizedBox(height: 16.0),
Text('HEIGHT'),
Slider(
min: kRegalMinSize.height,
max: kRegalMaxSize.height,
value: regal.size.height,
onChanged: (value) => context
.read(regalsProvider)
.updateRegal(
regal.copyWith(size: Size(regal.size.width, value)),
),
),
const SizedBox(height: 16.0),
Text('LEFT'),
Slider(
min: 0,
max: kFloorSize.width - regal.size.width,
value: regal.offset.dx,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(value, regal.offset.dy)),
),
),
const SizedBox(height: 16.0),
Text('TOP'),
Slider(
min: 0,
max: kFloorSize.height - regal.size.height,
value: regal.offset.dy,
onChanged: (value) =>
context.read(regalsProvider).updateRegal(
regal.copyWith(
offset: Offset(regal.offset.dx, value)),
),
),
],
),
),
);
}
}
final selectedRegalIdProvider = StateProvider<String>((ref) => null);
final selectedRegalProvider = Provider<Regal>((ref) {
final selectedId = ref.watch(selectedRegalIdProvider).state;
final regals = ref.watch(regalsProvider.state);
return regals.firstWhereOrNull((regal) => regal.id == selectedId);
});
final regalsProvider =
StateNotifierProvider<RegalsNotifier>((ref) => RegalsNotifier());
class RegalsNotifier extends StateNotifier<List<Regal>> {
final Size floorSize;
final Size maxSize;
RegalsNotifier({
this.floorSize = const Size(600, 400),
this.maxSize = const Size(100, 100),
List<Regal> state,
}) : super(state ?? []);
void createRegal() {
state = [...state, Regal.random];
print(state.last);
}
void updateRegal(Regal updated) {
state = state.map((r) => r.id == updated.id ? updated : r).toList();
}
}
#freezed
abstract class Regal implements _$Regal {
const factory Regal({
String id,
Color color,
Offset offset,
Size size,
}) = _Regal;
static Regal get random {
final rnd = Random();
return Regal(
id: DateTime.now().millisecondsSinceEpoch.toString(),
color: Color(0xff555555 + rnd.nextInt(0x777777)),
offset: Offset(
rnd.nextDouble() * (kFloorSize.width - kRegalMaxSize.width),
rnd.nextDouble() * (kFloorSize.height - kRegalMaxSize.height),
),
size: Size(
kRegalMinSize.width +
rnd.nextDouble() * (kRegalMaxSize.width - kRegalMinSize.width),
kRegalMinSize.height +
rnd.nextDouble() * (kRegalMaxSize.height - kRegalMinSize.height),
),
);
}
}
// CONFIG
const kFloorSize = Size(600, 400);
const kRegalMinSize = Size(10, 10);
const kRegalMaxSize = Size(200, 200);
I'm trying to make a an autocomplete Textfield that decide whether to show the list of suggestions up or down respect to the textfield depending on where there is more space in the screen. Like this:
AutocompleteTextFieldExample
To decide in which direction visualize it I need to know how much space is covered by the keyboard, so I use: "MediaQuery.of(context).viewPadding.bottom"
However "MediaQuery.of(context).viewPadding.bottom" returns always 0.0 if I set resizeToAvoidBottomInset: true.
So I need to set it false. But doing like that the textfields are covered by the keyboard when on focus, as you can see in this two images:
Screen
Screen with focus on the bottom textfield
This is my textFieldCode so far:
class _AutoCompleteTextFieldState extends State<AutoCompleteTextField> {
final double _maxSuggestionBoxDimension = 300;
OverlayEntry _overlayEntry;
final LayerLink _layerLink = LayerLink();
final FocusNode _focusNode = FocusNode();
final TextEditingController _textEditingController =
new TextEditingController();
// it contains the listTiles utilized by the overlay
// the items are changed inside the
List<ListTile> listTiles = [];
#override
void initState() {
super.initState();
// it inserts or remove the suggestions container depending on the focus of the textfield
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
this._overlayEntry.remove();
}
});
// if text is not empty it updates the suggestion in the suggestions box
_textEditingController.addListener(() {
// move this logic inside updateSuggestinoContainer
if (_textEditingController.text.isNotEmpty) {
updateSuggestionContainer(_textEditingController.text);
} else {
listTiles = [];
_overlayEntry.markNeedsBuild();
}
});
}
// it modify the suggestion container depending on the input text (in the textfield),
// it works in 3 part
// - defining the List of string which rappresent the suggestions that will be in the suggestions container
// - create and assign the new List of ListTile that will be used to build the suggestions container
// - mark _overlayEntry as dirty to rebuild and
void updateSuggestionContainer(String text) {
List<String> stringSuggestions = getStringSuggestions(text);
listTiles = getListTileSuggestion(stringSuggestions);
_overlayEntry.markNeedsBuild();
}
// it defines what suggestion to visualize
List<String> getStringSuggestions(String text) {
return widget.suggestions
.where((possibleSuggestion) =>
possibleSuggestion.substring(0, text.length) == text)
.toList();
}
// it define how to visualize each single suggestion
List<ListTile> getListTileSuggestion(List<String> suggestions) {
List<ListTile> listTiles = [];
for (String suggestion in suggestions) {
listTiles.add(ListTile(title: Text(suggestion)));
}
return listTiles;
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
Offset position = renderBox.localToGlobal(Offset.zero);
Future.delayed(Duration(seconds: 3)).then((_) {
print("${_showBottom(position.dy)}");
});
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(
0.0,
_showBottom(position.dy)
? min(size.height + 5.0, _maxSuggestionBoxDimension)
: max(-listTiles.length * 56.0 - size.height + 40.0,
-_maxSuggestionBoxDimension)),
child: Material(
child: Container(
height: min(
listTiles.length * 56.0, _maxSuggestionBoxDimension),
//duration: Duration(milliseconds: 150),
color: Colors.white,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView(
reverse: _showBottom(position.dy) ? false : true,
children: listTiles.isNotEmpty
? ListTile.divideTiles(
context: context, tiles: listTiles)
.toList()
: List()),
),
),
),
),
));
}
// controll whetever the suggestion should be shown above or bottom the textField
bool _showBottom(double textFieldCordinateY) {
print("textFieldCordinateY = $textFieldCordinateY");
print(
"MediaQuery.of(context).size.height = ${MediaQuery.of(context).size.height}");
print(
"MediaQuery.of(context).viewInsets.bottom = ${MediaQuery.of(context).viewInsets.bottom}");
print(
"MediaQuery.of(context).viewPadding.bottom = ${MediaQuery.of(context).viewPadding.bottom}");
// TODO
// consider also the top viewInsets
if ((MediaQuery.of(context).size.height -
MediaQuery.of(context).viewInsets.bottom) /
2 >
textFieldCordinateY)
return true;
else
return false;
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: TextFormField(
//scrollPadding: EdgeInsets.all(500),
controller: _textEditingController,
focusNode: this._focusNode,
decoration: InputDecoration(suffixIcon: Icon(Icons.arrow_drop_down)),
),
);
}
}
This is the build in my screen code:
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(title: Text("Titolo")),
body: ListView(children: [
SizedBox(height: 15),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField(["Ape", "Areoplano", "Austronauta"])),
SizedBox(height: 200),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField([
"Biliardo",
"Bufu",
"Bamba",
"Bici",
"Bambolina",
"Busta",
"Bella",
"Basta",
"Balza"
])),
SizedBox(height: 200),
Container(
padding: EdgeInsets.all(20),
child: AutoCompleteTextField(["Ciru", "Capanna", "Casa"]),
),
]),
);
}
Thank you in advance.
any way to add indicator to BottomNavigatorBarItem like this image?
This package should be able to help you achieve it.
You can use a TabBar instead of a BottomNavigationBar using a custom decoration:
class TopIndicator extends Decoration {
#override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TopIndicatorBox();
}
}
class _TopIndicatorBox extends BoxPainter {
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Paint _paint = Paint()
..color = Colors.lightblue
..strokeWidth = 5
..isAntiAlias = true;
canvas.drawLine(offset, Offset(cfg.size!.width + offset.dx, 0), _paint);
}
}
Then pass the decoration to the TapBar using TapBar(indicator: TopIndicator ...).
To use the TabBar as the Scaffold.bottomNavigationBar, you will most likely want to wrap it in a Material to apply a background color:
Scaffold(
bottomNavigationBar: Material(
color: Colors.white,
child: TabBar(
indicator: TopIndicator(),
tabs: const <Widget>[
Tab(icon: Icon(Icons.home_outlined), text: 'Reward'),
...
],
),
),
...
)
Thanks Ara Kurghinyan for the original idea.
I've had the same problem and all the packages I found seem to require raw IconData, which makes it impossible to use widget functionality like number badges (e.g. the number of unread chat messages).
I came up with my own little solution; first, I made a widget to display the actual indicators:
class TabIndicators extends StatelessWidget {
final int _numTabs;
final int _activeIdx;
final Color _activeColor;
final Color _inactiveColor;
final double _padding;
final double _height;
const TabIndicators({
required int numTabs,
required int activeIdx,
required Color activeColor,
required double padding,
required double height,
Color inactiveColor = const Color(0x00FFFFFF),
Key? key }) :
_numTabs = numTabs,
_activeIdx = activeIdx,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_padding = padding,
_height = height,
super(key: key);
#override
Widget build(BuildContext context) {
final elements = <Widget>[];
for(var i = 0; i < _numTabs; ++i) {
elements.add(
Expanded(child:
Padding(
padding: EdgeInsets.symmetric(horizontal: _padding),
child: Container(color: i == _activeIdx ? _activeColor : _inactiveColor),
)
)
);
}
return
SizedBox(
height: _height,
child: Row(
mainAxisSize: MainAxisSize.max,
children: elements,
),
);
}
}
This can be prepended to the actual BottomNavigationBar like this:
bottomNavigationBuilder: (context, tabsRouter) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TabIndicators(
activeIdx: tabsRouter.activeIndex,
activeColor: Theme.of(context).primaryColor,
numTabs: 4,
padding: 25,
height: 4,
),
BottomNavigationBar(...
This works perfectly well for me, but to make it look decent, you'd have to set the BottomNavigationBar's elevation to zero, otherwise there's still a faint horizontal line between the indicators and the icons.
I have a TextField (not a Text) widget that must remain on one line. I want to reduce it's font size if the text entered is too large for the TextField box, ie shrink it if it overflows. How can I do this?
I have written some code like this in a stateful component
if (textLength < 32) {
newAutoTextVM.fontSize = 35.0;
} else if (textLength < 42) {
newAutoTextVM.fontSize = 25.0;
In the view
fontSize: 25.0,
but it isn't very intelligent, it doesn't cope with resizing, also, because the font size isn't monospaced (courier etc), different characters take up different amounts of space.
Use a TextPainter to calculate the width of your text. Use a GlobalKey to get the size of your widget (A LayoutBuilder might be better to handle screen rotation).
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: Home()));
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
const textFieldPadding = EdgeInsets.all(8.0);
const textFieldTextStyle = TextStyle(fontSize: 30.0);
class _HomeState extends State<Home> {
final TextEditingController _controller = TextEditingController();
final GlobalKey _textFieldKey = GlobalKey();
double _textWidth = 0.0;
double _fontSize = textFieldTextStyle.fontSize;
#override
void initState() {
super.initState();
_controller.addListener(_onTextChanged);
}
void _onTextChanged() {
// substract text field padding to get available space
final inputWidth = _textFieldKey.currentContext.size.width - textFieldPadding.horizontal;
// calculate width of text using text painter
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: _controller.text,
style: textFieldTextStyle,
),
);
textPainter.layout();
var textWidth = textPainter.width;
var fontSize = textFieldTextStyle.fontSize;
// not really efficient and doesn't find the perfect size, but you got all you need!
while (textWidth > inputWidth && fontSize > 1.0) {
fontSize -= 0.5;
textPainter.text = TextSpan(
text: _controller.text,
style: textFieldTextStyle.copyWith(fontSize: fontSize),
);
textPainter.layout();
textWidth = textPainter.width;
}
setState(() {
_textWidth = textPainter.width;
_fontSize = fontSize;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Autosize TextField'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
key: _textFieldKey,
controller: _controller,
decoration: InputDecoration(
border: InputBorder.none,
fillColor: Colors.orange,
filled: true,
contentPadding: textFieldPadding,
),
style: textFieldTextStyle.copyWith(fontSize: _fontSize),
),
Text('Text width:'),
Container(
padding: textFieldPadding,
color: Colors.orange,
child: Row(
children: <Widget>[
Container(width: _textWidth, height: 20.0, color: Colors.blue),
],
),
)
],
),
),
);
}
}
I have searched through the docs and found a couple of solutions that could come at your help:
L̶o̶o̶k̶ ̶a̶t̶ ̶t̶h̶e̶ ̶o̶f̶f̶i̶c̶i̶a̶l̶ ̶d̶o̶c̶s̶[̶1̶]̶,̶ ̶i̶n̶ ̶p̶a̶r̶t̶i̶c̶u̶l̶a̶r̶e̶ ̶a̶t̶ ̶t̶h̶e̶s̶e̶ ̶p̶r̶o̶p̶e̶r̶t̶i̶e̶s̶:̶ ̶ ̶m̶a̶x̶L̶i̶n̶e̶s̶,̶ ̶o̶v̶e̶r̶f̶l̶o̶w̶ ̶a̶n̶d̶ ̶s̶o̶f̶t̶W̶r̶a̶p̶ (These are TextBox properties, not TextFields)
Have a look at this thread where they suggest to wrap the TextBox/TextFeld with a Flexible Widget
Depending on the rest of your code one of these solutions could be better, try tweaking around.
Hope it helps.