Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created January 11, 2023 19:36
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save slightfoot/0eedffa74e53831b6a058cf88703d264 to your computer and use it in GitHub Desktop.
Save slightfoot/0eedffa74e53831b6a058cf88703d264 to your computer and use it in GitHub Desktop.
Example of overlays with search suggestions during HumpdayQ&A - 11th January 2023 - by Simon Lightfoot
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'package:flutter/material.dart';
class SearchManager {
SearchManager();
final names = <String>['Simon', 'Steve', 'Randal', 'Dalan', 'TrainOfThrought'];
Future<List<String>> performSearch(String query) async {
final results = <String>[];
for (final name in names) {
if (name.toLowerCase().contains(query.toLowerCase())) {
results.add(name);
}
}
return results;
}
}
void main() {
runApp(const SearchApp());
}
@immutable
class SearchApp extends StatefulWidget {
const SearchApp({super.key});
static SearchAppState of(BuildContext context) {
return context.findAncestorStateOfType<SearchAppState>()!;
}
@override
State<SearchApp> createState() => SearchAppState();
}
class SearchAppState extends State<SearchApp> {
final searchManager = SearchManager();
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Home(),
debugShowCheckedModeBanner: false,
);
}
}
@immutable
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Transform.scale(
alignment: Alignment.topLeft,
scale: 1.0, // embiggen
child: Material(
child: Column(
children: const [
Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
child: SearchField(),
),
ListTile(
title: Text('Blah 1'),
),
ListTile(
title: Text('Blah 2'),
),
ListTile(
title: Text('Blah 3'),
),
],
),
),
);
}
}
@immutable
class SearchField extends StatefulWidget {
const SearchField({super.key});
@override
State<SearchField> createState() => _SearchFieldState();
}
class _SearchFieldState extends State<SearchField> {
final _width = ValueNotifier<double>(0.0);
final _layerLink = LayerLink();
final _focusNode = FocusNode();
final _controller = TextEditingController();
OverlayEntry? _overlayEntry;
@override
void initState() {
super.initState();
_focusNode.addListener(_onFocusOrTextChanged);
_controller.addListener(_onFocusOrTextChanged);
}
void _onFocusOrTextChanged() {
final shouldShowOverlay = _focusNode.hasFocus && _controller.text.trim().isNotEmpty;
if (_overlayEntry != null && !shouldShowOverlay) {
_overlayEntry!.remove();
_overlayEntry = null;
} else if (_overlayEntry == null && shouldShowOverlay) {
_overlayEntry = OverlayEntry(
builder: (BuildContext builderContext) {
return SearchSuggestionsList(
layerLink: _layerLink,
controller: _controller,
width: _width,
);
},
);
Overlay.of(context).insert(_overlayEntry!);
}
}
@override
void dispose() {
_focusNode.removeListener(_onFocusOrTextChanged);
_focusNode.dispose();
_controller.removeListener(_onFocusOrTextChanged);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
scheduleMicrotask(() => _width.value = constraints.maxWidth);
return CompositedTransformTarget(
link: _layerLink,
child: TextField(
focusNode: _focusNode,
controller: _controller,
),
);
});
}
}
@immutable
class SearchSuggestionsList extends StatefulWidget {
const SearchSuggestionsList({
super.key,
required this.layerLink,
required this.controller,
required this.width,
});
final LayerLink layerLink;
final TextEditingController controller;
final ValueNotifier<double> width;
@override
State<SearchSuggestionsList> createState() => _SearchSuggestionsListState();
}
class _SearchSuggestionsListState extends State<SearchSuggestionsList> {
late SearchManager searchManager;
late Future<List<String>> results;
@override
void initState() {
super.initState();
searchManager = SearchApp.of(context).searchManager;
widget.controller.addListener(_onTextChanged);
_onTextChanged();
}
void _onTextChanged() {
final text = widget.controller.text.trim();
setState(() {
results = searchManager.performSearch(text);
});
}
@override
void dispose() {
widget.controller.removeListener(_onTextChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return CompositedTransformFollower(
link: widget.layerLink,
followerAnchor: Alignment.topLeft,
targetAnchor: Alignment.bottomLeft,
child: Align(
alignment: Alignment.topLeft,
child: ValueListenableBuilder(
valueListenable: widget.width,
builder: (BuildContext context, double width, Widget? child) {
return SizedBox(
width: width,
child: child,
);
},
child: IntrinsicHeight(
child: Material(
type: MaterialType.card,
elevation: 8.0,
child: FutureBuilder(
future: results,
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.connectionState != ConnectionState.done &&
snapshot.hasData == false) {
return const Center(child: CircularProgressIndicator());
}
final theme = Theme.of(context);
final results = snapshot.requireData;
return SingleChildScrollView(
child: Column(
children: [
if (results.isEmpty) //
ListTile(
title: Text(
'No results found',
style: TextStyle(
color: theme.colorScheme.error,
),
),
),
for (final result in results) //
ListTile(
onTap: () {
print(result);
},
title: Text(result),
),
],
));
},
),
),
),
),
),
);
}
}
@Apliarte
Copy link

Thanks

@subramanian42
Copy link

setting showWhenUnlinked to false under CompositedTransformFollower helps avoid OverlayEntry from showing up at the top left when scrolling.

Thank you simon for this awesome search overlay.

@JasonHairston
Copy link

JasonHairston commented Mar 16, 2023

Thanks :) After reading your post, I discovered a useful resource at https://www.topwritersreview.com/reviews/smartcustomwriting/ This website gives in-depth reviews of several essay writing services, like smartcustomwriting, to help me choose the finest one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment