unable to style text size in flutter widget - flutter

I am trying to style the field where i type in the address, I want to make my font size smaller. I have tried changing the fontsize through the various "textstyle" properties in "Inputdecoration" but had no luck. How do i achieve it?
There are also these yellow lines underscoring my text. Is it an error?
Please help me out :/ would really appreciate it~
return MaterialApp(
home: Scaffold(
body: Stack(
children: <Widget>[
GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: currentPosition,
markers: marks,
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PlacesAutocompleteField(
apiKey: 'AIzaSyDVxxxxxxxxxxxxxx',
hint: 'Search Places',
style: TextStyle(fontSize: 50.0),
inputDecoration: InputDecoration(
icon: Icon(Icons.search),
hintStyle: TextStyle(
fontSize: 50.0,
fontWeight: FontWeight.bold,
),
),
onChanged: (value) async {
placeName = value;
print(placeName);
List<Placemark> placemark =
await Geolocator().placemarkFromAddress(placeName);
print(placemark[0].position);
Set<Marker> markers =
await getMarkers(placemark[0].position);
updateUI(placemark[0].position, markers);
mapController.animateCamera(
CameraUpdate.newCameraPosition(currentPosition));
},
),
),
),
],
),
),
);
code for the PlacesAutocompleteField:
class PlacesAutocompleteField extends StatefulWidget {
/// Creates a text field like widget.
///
/// To remove the decoration entirely (including the extra padding introduced
/// by the decoration to save space for the labels), set the [decoration] to
/// null.
const PlacesAutocompleteField({
Key key,
#required this.apiKey,
this.style,
this.controller,
this.leading,
this.hint = "Search",
this.trailing,
this.trailingOnTap,
this.mode = Mode.fullscreen,
this.offset,
this.location,
this.radius,
this.language,
this.sessionToken,
this.types,
this.components,
this.strictbounds,
this.onChanged,
this.onError,
this.inputDecoration = const InputDecoration(),
}) : super(key: key);
/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController].
final TextEditingController controller;
/// Icon shown inside the field left to the text.
final Icon leading;
/// Icon shown inside the field right to the text.
final Icon trailing;
/// Callback when [trailing] is tapped on.
final VoidCallback trailingOnTap;
/// Text that is shown, when no input was done, yet.
final String hint;
final TextStyle style;
/// Your Google Maps Places API Key.
///
/// For this key the Places Web API needs to be activated. For further
/// information on how to do this, see their official documentation below.
///
/// See also:
///
/// * <https://developers.google.com/places/web-service/autocomplete>
final String apiKey;
/// The decoration to show around the text field.
///
/// By default, draws a horizontal line under the autocomplete field but can be
/// configured to show an icon, label, hint text, and error text.
///
/// Specify null to remove the decoration entirely (including the
/// extra padding introduced by the decoration to save space for the labels).
final InputDecoration inputDecoration;
/// The position, in the input term, of the last character that the service
/// uses to match predictions.
///
/// For example, if the input is 'Google' and the
/// offset is 3, the service will match on 'Goo'. The string determined by the
/// offset is matched against the first word in the input term only. For
/// example, if the input term is 'Google abc' and the offset is 3, the service
/// will attempt to match against 'Goo abc'. If no offset is supplied, the
/// service will use the whole term. The offset should generally be set to the
/// position of the text caret.
///
/// Source: https://developers.google.com/places/web-service/autocomplete
final num offset;
final Mode mode;
final String language;
final String sessionToken;
final List<String> types;
final List<Component> components;
final Location location;
final num radius;
final bool strictbounds;
/// Called when the text being edited changes.
final ValueChanged<String> onChanged;
/// Callback when autocomplete has error.
final ValueChanged<PlacesAutocompleteResponse> onError;
#override
_LocationAutocompleteFieldState createState() =>
_LocationAutocompleteFieldState();
}
class _LocationAutocompleteFieldState extends State<PlacesAutocompleteField> {
TextEditingController _controller;
TextEditingController get _effectiveController =>
widget.controller ?? _controller;
#override
void initState() {
super.initState();
if (widget.controller == null) _controller = TextEditingController();
}
#override
void didUpdateWidget(PlacesAutocompleteField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null)
_controller = TextEditingController.fromValue(oldWidget.controller.value);
else if (widget.controller != null && oldWidget.controller == null)
_controller = null;
}
Future<Prediction> _showAutocomplete() async => PlacesAutocomplete.show(
context: context,
apiKey: widget.apiKey,
offset: widget.offset,
onError: widget.onError,
mode: widget.mode,
hint: widget.hint,
language: widget.language,
sessionToken: widget.sessionToken,
components: widget.components,
location: widget.location,
radius: widget.radius,
types: widget.types,
strictbounds: widget.strictbounds,
);
void _handleTap() async {
Prediction p = await _showAutocomplete();
if (p == null) return;
setState(() {
_effectiveController.text = p.description;
if (widget.onChanged != null) {
widget.onChanged(p.description);
}
});
}
#override
Widget build(BuildContext context) {
final TextEditingController controller = _effectiveController;
var text = controller.text.isNotEmpty
? Text(
controller.text,
softWrap: true,
)
: Text(
widget.hint ?? '',
style: TextStyle(color: Colors.black38),
);
Widget child = Row(
children: <Widget>[
widget.leading ?? SizedBox(),
SizedBox(
width: 16.0,
),
Expanded(
child: text,
),
widget.trailing != null
? GestureDetector(
onTap: widget.trailingOnTap,
child: widget.trailingOnTap != null
? widget.trailing
: Icon(
widget.trailing.icon,
color: Colors.grey,
),
)
: SizedBox()
],
);
if (widget.inputDecoration != null) {
child = InputDecorator(
decoration: widget.inputDecoration,
child: child,
);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _handleTap,
child: child,
);
}
}

The reason being you are not providing any widget to your Text which has got material design, there are many ways to fix it, like you can provide Material or a basic Scaffold. The recommended way would be to use Scaffold as it can provide many basic functionalities too.
So you need to wrap your Stack in Scaffold
Scaffold(
body: Stack(...)
)
Update (for text size) :
In the build() method of PlacesAutocompleteField class, replace yours with following
var text = controller.text.isNotEmpty
? Text(
controller.text,
softWrap: true,
style: TextStyle(fontSize: 50),
)
: Text(
widget.hint ?? '',
style: TextStyle(color: Colors.black38, fontSize: 50),
);

Related

How can I Scroll to the first occurrence of a given string in a Text Widget

say I have a song lyric app and there is just one Scaffold with a Text widget that displays the entire lyric and the lyrics are written in the format
....
Chorus:
...
....
....
and I have a FAB, onClick of which I need the text to auto scroll to the text "Chorus:", this text is literally in every song, but when the verses are a about 4+, they usually go off screen, so, user usually has to manually scroll to the chorus again after each verse that's beyond the screen height, but I need this to be done automatically at the tap of a button
scroll up till the string "chorus" is in view, how would I do this in flutter
TEXT
const kTheAstronomers = '''1. Yeah, you woke up in London
At least that's what you said
I sat here scrollin'
And I saw you with your aunt
A demon on your left
An angel on your right
But you're hypocritical
Always political
Chorus:
Say you mean one thing
But you go ahead and lie
Oh, you lie-lie, lie-lie
And you say you're not the bad type
2. Oh, you posted on Twitter
Said you had other plans
But your mother, she called me
Said, "Come have lunch with the fam"
Guess you didn't tell her that
You should've called me back
I guess you changed your number or somethin\' '''
LYRIC SCREEN
#override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
child: Text(
kTheAstronomers,
style: const TextStyle(
fontSize: 30,
fontFamily: 'Montserrat',
fontWeight: FontWeight.w600,
),
),
),
),
)
floatingActionButton: FAB(onPressed: autoScrollToChorus),
,
You can create a GlobalKey and use the currentContext to scroll to the Chorus part.
final _key = GlobalKey()
Inside the autoScrollToChorus method you can add:
final context = _key.currentContext!;
await Scrollable.ensureVisible(context)
I found a way.
I had to change the way I displayed the text, instead of using one text widget, I used a ListView builder to display two texts, but before that, in initState, when my page receives the text, I split the text into a list of two separate texts, one containing the first part and the other containing from the Chorus down, then I give this list to the listview builder (you could also just use a column and create two separate widgets and just pass the scroll key to the second text, knowing it's the second part of the text)
final GlobalKey _key = GlobalKey();
void _autoScrollToChorus() async {
BuildContext context = _key.currentContext!;
await Scrollable.ensureVisible(context);
}
late List<String> lyricList;
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView.builder(
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
key: idx == 1 ? _key : null,
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
Thanks to #Priyaank I knew to use the key and scroll to a particular widget
a more advanced solution that makes it possible to hide the button when the chorus is in view USING THE SCROLLABLE_POSITIONED_LIST PACKAGE
final GlobalKey _key = GlobalKey();
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemListener = ItemPositionsListener.create();
late List<String> lyricList;
bool chorusIsVisible = true;
void _autoScrollToChorus() {
// BuildContext context = _key.currentContext!;
// await Scrollable.ensureVisible(context);
_itemScrollController.scrollTo(
index: 1,
duration: const Duration(milliseconds: 500),
alignment: 0.5
);
}
#override
initState() {
lyricList =
kTheAstronomers.split(RegExp("(?=chorus)", caseSensitive: false));
super.initState();
if(lyricList.length > 1) {
_itemListener.itemPositions.addListener(() {
chorusIsVisible = _itemListener.itemPositions.value.where((item) {
final isTopVisible = item.itemLeadingEdge >= 0;
return isTopVisible;
}
).map((item) => item.index).toList().contains(1);
setState(() {});
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ScrollablePositionedList.builder(
itemScrollController: _itemScrollController,
itemPositionsListener: _itemListener,
itemCount: lyricList.length,
itemBuilder: (context, idx) {
return Text(
lyricList[idx],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 30,
),
);
}),
),
floatingActionButton: lyricList.length > 1 && !chorusIsVisible ? FloatingActionButton(
onPressed: _autoScrollToChorus,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Chorus"),
),
) : null,
);
}
}

How to get the exact size of PopupMenuButton (or any widget)?

I need to get the PopupMenuButton's size because it has a property offset which controls where the dropdown menu is rendered on the screen, and I want this one to be rendered such that the top left of the drop down menu is aligned with the bottom left of the PopupMenuButton (see image below).
My approach now is this:
extension TextExtension on Text {
/// Calculates the size of the text inside this text widget.
/// note: this method supposes ltr direction of text, which is not always true, but it doesn't affect the size that much, so
/// keep in mind that the size returned may be approximate in some cases.
/// The text inside this widget must be non-null before calling this method.
Size getSize({TextDirection? textDirection}) {
TextPainter tp = TextPainter(
text: TextSpan(text: data),
textDirection: textDirection ?? TextDirection.ltr)
..layout();
return tp.size;
}
}
And then when I define the PopupMenuButton, I do this:
Widget _dropDownMenu({
required BuildContext context,
required String title,
required List<PopupMenuItem> items,
}) {
final text = Text(
title,
style: Theme.of(context).textTheme.bodyMedium,
);
final textSize = text.size;
return PopupMenuButton(
child: text,
itemBuilder: (context) => items,
offset: Offset(0, textSize.height),
);
}
It works, but I don't like it. I think there must be a better way to do this.
This is how it looks like right now:
I tried LayoutBuilder, but it is returning infinite width constraints.
Is there a more clean way of doing this?
It seems there is no other way except the approach I mentioned in the question, or to modify the source code of the PopupMenuButton to make it accept an OffsetBuilder as pskink mentioned. This can be done like this (and there is full working example below):
go to the PopupMenuButton source code and copy it all into a new file custom_popup_menu.dart (in this new file just remove all the imports and import them again as suggested by the IDE to fix them)
add this to anywhere top level in the file: Offset _defaultOffsetBuilder(Size size) => Offset.zero;
inside the PopupMenuButton class replace final Offset offset with
/// The button size will be passed to this function to get the offset applied
/// to the Popup Menu when it is open. The top left of the [PopupMenuButton] is considered
/// as the origin of the coordinate system of this offset.
///
/// When not set, the Popup Menu Button will be positioned directly next to
/// the button that was used to create it.
final Offset Function(Size) offsetBuilder;
inside the constructor of this class replace this.offset with this.offsetBuilder = _defaultOffsetBuilder,
in the showButtonMenu method of PopupMenuButtonState class, replace
Rect.fromPoints(
button.localToGlobal(widget.offset, ancestor: overlay),
button.localToGlobal(
button.size.bottomRight(Offset.zero) + widget.offset,
ancestor: overlay),
),
Offset.zero & overlay.size,
);
with
final offset = widget.offsetBuilder(button.size);
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(offset, ancestor: overlay),
button.localToGlobal(
button.size.bottomRight(Offset.zero) + offset,
ancestor: overlay),
),
Offset.zero & overlay.size,
);
Full Working Example:
... (imports)
import 'custom_popup_menu.dart' as pm;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => const MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage2(),
);
}
class HomePage2 extends StatelessWidget {
const HomePage2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => Scaffold(
body: Align(
alignment: const Alignment(0, -0.8),
child: Container(
decoration: BoxDecoration(border: Border.all(width: 2.0)),
child: pm.PopupMenuButton<String>(
child: const Text(
'Press Me',
style: TextStyle(color: Colors.black, fontSize: 50),
),
itemBuilder: (context) => [
_buildPopupMenuItem(),
_buildPopupMenuItem(),
_buildPopupMenuItem(),
],
color: Colors.red,
offsetBuilder: (buttonSize) => Offset(0, buttonSize.height),
),
),
),
);
pm.PopupMenuItem<String> _buildPopupMenuItem() {
return pm.PopupMenuItem(
child: Text(
'Press Me ${Random().nextInt(100)}',
style: const TextStyle(color: Colors.black, fontSize: 50),
),
onTap: () {},
);
}
}

Flutter - Show Row on extra scroll - Top of column (Like Whatsapp Archived Chats)

I want to put a row on top of a column, which will not be visible initially. It will only be visible when the scroll offset of the SingleChildScrollview is negative.
In other words, only if the user scrolls further than normal (downwards motion) will this Row show. This is an example in Whatsapp. The "Search Box" widget is not shown initially, only if you scroll up, and disappears once scroll is downwards.
UPDATE
Using #Lulupointu's answer, I was able to get to this:
The top widget shows and hides on scroll with a smooth animation.
#Hooshyar's answer also works but is less smooth and uses a different method.
Using singleChildScrollView here is what I came up with using s NotificationListener() widget, there are other solutions but this one is the simplest one:
have a bool to determine Container visibility:
bool shouldIShowTheUpperThing = false;
the have your SingleChildScrollView() wrapped with a NotificationListener() :
NotificationListener(
child: SingleChildScrollView(
child: Column(
children: [
shouldIShowTheUpperThing == false ? Row(
children: [
Container(height: 0,),
],
) : Row(
children: [
Expanded(child: Container(color: Colors.red , height: 100 , child: Text('the hidden box'),)),
],
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueGrey,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.blueAccent,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.amber,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
color: Colors.black12,
),
Container(
padding: EdgeInsets.all(130),
child: Text('data'),
),
],
),
),
onNotification: (t) {
if (t is ScrollNotification) {
if (t.metrics.pixels < 1.0) {
setState(() {
shouldIShowTheUpperThing = true;
});
}
}
return true;
});
}
Since this is a unusual scroll effect, if you want it to look good I think you need to use slivers.
Implementation
What I did was copy paste SliverToBoxAdapter and modify it.
Features:
The child is hidden when the widget is first loaded
The child is snappy
The child hide when the user scroll down again
Limitations:
If there are not enough children in the CustomScrollView to over-scroll, the child will always be visible and a weird scroll effect will appear on startup
class SliverHidedHeader extends SingleChildRenderObjectWidget {
const SliverHidedHeader({
Key? key,
required Widget child,
}) : super(key: key, child: child);
#override
RenderObject createRenderObject(BuildContext context) {
return RenderSliverHidedHeader(context: context);
}
}
class RenderSliverHidedHeader extends RenderSliverSingleBoxAdapter {
RenderSliverHidedHeader({
required BuildContext context,
RenderBox? child,
}) : _context = context,
super(child: child);
/// Whether we need to apply a correction to the scroll
/// offset during the next layout
///
///
/// This is useful to avoid the viewport to jump when we
/// insert/remove the child.
///
/// If [showChild] is true, its an insert
/// If [showChild] is false, its a removal
bool _correctScrollOffsetNextLayout = true;
/// Whether [child] should be shown
///
///
/// This is used to hide the child when the user scrolls down
bool _showChild = true;
/// The context is used to get the [Scrollable]
BuildContext _context;
#override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints;
child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
// Here are the few custom lines, which use [scrollOffsetCorrection]
// to remove the child size
//
// Note that this should only be called for correction linked with the
// insertion (NOT the removal)
if (_correctScrollOffsetNextLayout) {
geometry = SliverGeometry(scrollOffsetCorrection: childExtent);
_correctScrollOffsetNextLayout = false;
return;
}
// Subscribe a listener to the scroll notifier
// which will snap if needed
_manageSnapEffect(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
// Subscribe a listener to the scroll notifier
// which hide the child if needed
_manageInsertChild(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
paintOrigin: _showChild ? 0 : -paintedChildSize,
layoutExtent: _showChild ? null : 0,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow:
childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
setChildParentData(child!, constraints, geometry!);
}
/// Override to remove the listeners if needed
#override
void dispose() {
final _scrollPosition = Scrollable.of(_context)!.position;
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
super.dispose();
}
/// The listener which will snap if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedSnapScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for the snapping effect
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
_manageSnapEffect({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedSnapScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedSnapScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedSnapScrollNotifierListener = () => _snapScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier.addListener(_subscribedSnapScrollNotifierListener!);
}
/// Snaps if the user just stopped scrolling and the child is
/// partially visible
void _snapScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// Whether the user is currently idle (i.e not scrolling)
//
// We don't check _scrollPosition.activity.isScrolling or
// _scrollPosition.isScrollingNotifier.value because even if
// the user is holding still we don't want to start animating
//
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final isIdle = _scrollPosition.activity is IdleScrollActivity;
// Whether at least part of the child is visible
final isChildVisible = paintedChildSize > 0;
if (isIdle && isChildVisible) {
// If more than half is visible, snap to see everything
if (paintedChildSize >= childExtent / 2 && paintedChildSize != childExtent) {
_scrollPosition.animateTo(
0,
duration: Duration(milliseconds: 100),
curve: Curves.easeOut,
);
}
// If less than half is visible, snap to hide
else if (paintedChildSize < childExtent / 2 && paintedChildSize != 0) {
_scrollPosition.animateTo(
childExtent,
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
}
/// The listener which will hide the child if needed
///
///
/// We store it to be able to remove it before subscribing
/// a new one
void Function()? _subscribedInsertChildScrollNotifierListener;
/// Handles the subscription and removal of subscription to
/// the scrollable position notifier which are responsible
/// for inserting/removing the child if needed
///
///
/// This must be called at each [performLayout] to ensure that the
/// [childExtent] and [paintedChildSize] parameters are up to date
void _manageInsertChild({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
// If we were subscribed with previous value, remove the subscription
if (_subscribedInsertChildScrollNotifierListener != null) {
_scrollPosition.isScrollingNotifier
.removeListener(_subscribedInsertChildScrollNotifierListener!);
}
// We store the subscription to be able to remove it
_subscribedInsertChildScrollNotifierListener = () => _insertChildScrollNotifierListener(
childExtent: childExtent,
paintedChildSize: paintedChildSize,
);
_scrollPosition.isScrollingNotifier
.addListener(_subscribedInsertChildScrollNotifierListener!);
}
/// When [ScrollPosition.isScrollingNotifier] fires:
/// - If the viewport is at the top and the child is not visible,
/// ^ insert the child
/// - If the viewport is NOT at the top and the child is NOT visible,
/// ^ remove the child
void _insertChildScrollNotifierListener({
required double childExtent,
required double paintedChildSize,
}) {
final _scrollPosition = Scrollable.of(_context)!.position;
final isScrolling = _scrollPosition.isScrollingNotifier.value;
// If the user is still scrolling, do nothing
if (isScrolling) {
return;
}
final scrollOffset = _scrollPosition.pixels;
// If the viewport is at the top and the child is not visible,
// insert the child
//
// We use 0.1 as a small value in case the user is nearly scrolled
// all the way up
if (!_showChild && scrollOffset <= 0.1) {
_showChild = true;
_correctScrollOffsetNextLayout = true;
markNeedsLayout();
}
// There is sometimes an issue with [ClampingScrollPhysics] where
// the child is NOT shown but the scroll offset still includes [childExtent]
//
// There is no why to detect it but we always insert the child when all
// this conditions are united.
// This means that if a user as [ClampingScrollPhysics] and stops scrolling
// exactly at [childExtent], the child will be wrongfully inserted. However
// this seems a small price to pay to avoid the issue.
if (_scrollPosition.physics.containsScrollPhysicsOfType<ClampingScrollPhysics>()) {
if (!_showChild && scrollOffset == childExtent) {
_showChild = true;
markNeedsLayout();
}
}
// If the viewport is NOT at the top and the child is NOT visible,
// remove the child
if (_showChild && scrollOffset > childExtent) {
_showChild = false;
markNeedsLayout();
// We don't have to correct the scroll offset here, no idea why
}
}
}
/// An extension on [ScrollPhysics] to check if it or its
/// parent are the given [ScrollPhysics]
extension _ScrollPhysicsExtension on ScrollPhysics {
/// Check the type of this [ScrollPhysics] and its parents and return
/// true if any is of type [T]
bool containsScrollPhysicsOfType<T extends ScrollPhysics>() {
return this is T || (parent?.containsScrollPhysicsOfType<T>() ?? false);
}
}
How to use it
Use it at the top of your list of slivers:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MaterialApp(home: MyStatefulWidget()));
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverHidedHeader(
child: Container(
child: Center(child: Text('SliverAppBar')),
height: 100,
color: Colors.redAccent,
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
),
);
}
}
Other resource
Check out the pull_to_refresh package if you want a different approach to solve this issue. Beware that their code is quite complex since they also have the auto-hide feature implemented, but it's worth a look if you have time.
Solve the limitation
I'm not sure the limitation can be solved using this approach. The issue is that, for performance reason, the sliver does not know anything about the one bellow it, meaning that it's quite hard to even knowing when we are in the problematic case, let alone handle it.
try SliverAppBar and set floating and snap named params to true
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
snap: true,
floating: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('SliverAppBar'),
background: FlutterLogo(),
),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 20,
child: Center(
child: Text('Scroll to see the SliverAppBar in effect.'),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: index.isOdd ? Colors.white : Colors.black12,
height: 100.0,
child: Center(
child: Text('$index', textScaleFactor: 5),
),
);
},
childCount: 20,
),
),
],
),
);
}
}

flutter search in multiple strings

I have search field that filter my list and it works fine I just want to make small changes to it:
Logic
Back to full list when user clear search field
search also be included of ListTile->subtitle currently only search in title
Code search function is commented for better understanding
List<World> locations = [...]
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
buildSearch(),
Expanded(
child: ListView.builder(
itemCount: locations.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {},
title: Text(locations[index].location),
subtitle: Text(locations[index].country),
),
),
},
),
),
],
),
),
}
Widget buildSearch() => SearchWidget(
text: query,
hintText: 'Search for location',
onChanged: searchLocation,
);
void searchLocation(String query) async {
// currently only search in titles (need to add subtitle as well)
final newLocations = locations.where((location) {
final nameLower = location.location.toLowerCase();
final searchLower = query.toLowerCase();
return nameLower.contains(searchLower);
}).toList();
// when user clear search or remove letters list wont back to it's default
setState(() {
this.query = query;
this.locations = newLocations;
});
}
Any suggestions?
Update
here is my SearchWidget file (just in case)
class SearchWidget extends StatefulWidget {
final String text;
final ValueChanged<String> onChanged;
final String hintText;
const SearchWidget({
Key key,
this.text,
this.onChanged,
this.hintText,
}) : super(key: key);
#override
_SearchWidgetState createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
final controller = TextEditingController();
#override
Widget build(BuildContext context) {
final styleActive = TextStyle(color: Colors.black);
final styleHint = TextStyle(color: Colors.black54);
final style = widget.text.isEmpty ? styleHint : styleActive;
return Container(
height: 42,
margin: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
border: Border.all(color: Colors.black26),
),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
controller: controller,
decoration: InputDecoration(
icon: Icon(Icons.search, color: style.color),
suffixIcon: widget.text.isNotEmpty
? GestureDetector(
child: Icon(Icons.close, color: style.color),
onTap: () {
controller.clear();
widget.onChanged('');
FocusScope.of(context).requestFocus(FocusNode());
},
)
: null,
hintText: widget.hintText,
hintStyle: style,
border: InputBorder.none,
),
style: style,
onChanged: widget.onChanged,
),
);
}
}
UPDATE 2
I've managed to fix #2 in my logic and get subtitles include search results by following code (clear search issue still remains)
void searchLocation(String query) async {
final newLocations = locations.where((location) {
final nameLower = location.location.toLowerCase();
final countryLower = location.country.toLowerCase(); // added
final searchLower = query.toLowerCase();
return nameLower.contains(searchLower) || countryLower.contains(searchLower); //changed
}).toList();
setState(() {
this.query = query;
this.locations = newLocations;
});
}
Note:
I've accepted Yair Chen answer but I need to make some clarifications to address the issue:
Based on Yair Chen answer I had to create new list List<World> filteredLocations = [];
Then in my ListView.builder I've changed itemCount and child like following:
ListView.builder(
itemCount: filteredLocations.isNotEmpty ? filteredLocations.length : locations.length,
//...
child: filteredLocations.isNotEmpty ? card(... //filteredLocations[index].location//...) : Card(... //locations[index].location// ...),
This way index issue on filtered result solved and card data will get data regarding of the list they are returning from.
You can add another list called filteredLocations and then save to the filtered location the new list. The function .where() changes the original List as well which you dont want. You can shallow copy (using spread operator) the list and then create a new list that matches the query like that:
void searchLocation(String query) async {
// currently only search in titles (need to add subtitle as well)
// [...locations] copies the list to not change original list
final newLocations = [...locations].where((location) {
final nameLower = location.location.toLowerCase();
final searchLower = query.toLowerCase();
// Also adding subtitle check (whether the name contains it or the subtitle does
return nameLower.contains(searchLower) || location.subtitle.toLowerCase().contains(searchLower);
}).toList();
// when user clear search or remove letters list wont back to it's default
setState(() {
this.query = query;
this.filteredLocations = newLocations;
});
}
This way the original list will never be changed and only the filteredList will be updated. It'll also fix the issue that when the string is empty you want all the items to appear.
Good luck and let me know if you need anything else :)

I want to change the color of focused otp text field's single element in flutter

In my OTP text field, the focused color of the bottom border of a single element, or textfield is blue by default. How can i change it to orange?? Also how can i change the unfocused color of that border from black to white. That is by default, without entering any number, color of all blocks or bottom border is white??? I will highlight the property in the image.
Check out this image, if you didn't understand properly that what i am trying to say - MobileImage
Here's the code -
import 'package:flutter/material.dart';
import 'package:otp_text_field/style.dart';
class OTPTextField extends StatefulWidget {
/// Number of the OTP Fields
final int length;
/// Total Width of the OTP Text Field
final double width;
/// Width of the single OTP Field
final double fieldWidth;
/// Manage the type of keyboard that shows up
TextInputType keyboardType;
/// The style to use for the text being edited.
final TextStyle style;
/// Text Field Alignment
/// default: MainAxisAlignment.spaceBetween [MainAxisAlignment]
final MainAxisAlignment textFieldAlignment;
/// Obscure Text if data is sensitive
final bool obscureText;
/// Text Field Style for field shape.
/// default FieldStyle.underline [FieldStyle]
final FieldStyle fieldStyle;
/// Callback function, called when a change is detected to the pin.
final ValueChanged<String> onChanged;
/// Callback function, called when pin is completed.
final ValueChanged<String> onCompleted;
OTPTextField(
{Key key,
this.length = 4,
this.width = 20,
this.fieldWidth = 50,
this.keyboardType = TextInputType.number,
this.style = const TextStyle(),
this.textFieldAlignment = MainAxisAlignment.spaceBetween,
this.obscureText = false,
this.fieldStyle = FieldStyle.underline,
this.onChanged,
this.onCompleted})
: assert(length > 1);
#override
_OTPTextFieldState createState() => _OTPTextFieldState();
}
class _OTPTextFieldState extends State<OTPTextField> {
List<FocusNode> _focusNodes;
List<TextEditingController> _textControllers;
List<Widget> _textFields;
List<String> _pin;
#override
void initState() {
super.initState();
_focusNodes = List<FocusNode>(widget.length);
_textControllers = List<TextEditingController>(widget.length);
_pin = List.generate(widget.length, (int i) {
return '';
});
_textFields = List.generate(widget.length, (int i) {
return buildTextField(context, i);
});
}
#override
void dispose() {
_textControllers
.forEach((TextEditingController controller) => controller.dispose());
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
width: widget.width,
child: Row(
mainAxisAlignment: widget.textFieldAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: _textFields,
),
);
}
/// This function Build and returns individual TextField item.
///
/// * Requires a build context
/// * Requires Int position of the field
Widget buildTextField(BuildContext context, int i) {
if (_focusNodes[i] == null) _focusNodes[i] = new FocusNode();
if (_textControllers[i] == null)
_textControllers[i] = new TextEditingController();
return Container(
width: widget.fieldWidth,
child: TextField(
cursorColor: Colors.white,
controller: _textControllers[i],
keyboardType: widget.keyboardType,
textAlign: TextAlign.center,
maxLength: 1,
style: widget.style,
focusNode: _focusNodes[i],
obscureText: true,
decoration: InputDecoration(
fillColor: Colors.white,
counterText: "",
border: widget.fieldStyle == FieldStyle.box
? OutlineInputBorder(borderSide: BorderSide(width: 4.0, color: Colors.green))
: null),
onChanged: (String str) {
// Check if the current value at this position is empty
// If it is move focus to previous text field.
if (str.isEmpty) {
if (i == 0) return;
_focusNodes[i].unfocus();
_focusNodes[i - 1].requestFocus();
}
// Update the current pin
setState(() {
_pin[i] = str;
});
// Remove focus
if (str.isNotEmpty) _focusNodes[i].unfocus();
// Set focus to the next field if available
if (i + 1 != widget.length && str.isNotEmpty)
FocusScope.of(context).requestFocus(_focusNodes[i + 1]);
String currentPin = "";
_pin.forEach((String value) {
currentPin += value;
});
// if there are no null values that means otp is completed
// Call the `onCompleted` callback function provided
if (!_pin.contains(null) &&
!_pin.contains('') &&
currentPin.length == widget.length) {
widget.onCompleted(currentPin);
}
// Call the `onChanged` callback function
widget.onChanged(currentPin);
},
),
);
}
}
Your default Theme color is blue, this will be applied to your TextField. You would need to change your Theme color for your TextField to change your border color. Take note that this will only change the Theme color for TextField and not for your whole Flutter app.
child: Theme(
data: ThemeData(
primaryColor: Colors.orangeAccent,
primaryColorDark: Colors.orange,
),
child: TextField(
...
));
From the package here
Easy fix is by adding the otpFieldStyle property and edit the colors as preferred for example:
Padding(
padding: EdgeInsets.only(top:18.0),
child: OTPTextField(
length: 4,
width: MediaQuery.of(context).size.width,
textFieldAlignment: MainAxisAlignment.spaceAround,
fieldWidth: 55,
fieldStyle: FieldStyle.underline,
otpFieldStyle: OtpFieldStyle(
focusBorderColor: Colors.orange //(here)
),
outlineBorderRadius: 15,
style: TextStyle(fontSize: 17, color: Colors.white, ),
onChanged: (pin) {
print("Changed: " + pin);
},
onCompleted: (pin) {
print("Completed: " + pin);
}),
),
which has more properties as below
OtpFieldStyle(
{this.backgroundColor: Colors.transparent,
this.borderColor: Colors.black26,
this.focusBorderColor: Colors.blue,
this.disabledBorderColor: Colors.grey,
this.enabledBorderColor: Colors.black26,
this.errorBorderColor: Colors.red});