Suresh Mohan
Posted on April 21, 2021
The Syncfusion Flutter PDF Viewer allows you to view PDF documents seamlessly and efficiently. When working with PDF documents, you’ll often need to search for text. In this blog, you will learn how to create a custom search toolbar and perform text search in a PDF document using the Syncfusion Flutter PDF Viewer.
Configuring the Flutter PDF Viewer widget
Please see our Flutter PDF Viewer Getting Started documentation to create a Flutter application and configure the Syncfusion Flutter PDF Viewer ( SfPdfViewer ) widget in it.
Create custom search toolbar and perform text search
Step 1 : Create a SearchToolbar widget class to display the toolbar items for search operations and a typedef for the onTap callback. The SearchToolbar widget class has the following properties:
- controller —An object used to control the SfPdfViewer.
- onTap —Called when we select or tap on any search toolbar item.
- showTooltip —Indicates whether to display the tooltip for the search toolbar items or not.
/// Signature for the [SearchToolbar.onTap] callback.
typedef SearchTapCallback = void Function(Object item);
/// SearchToolbar widget.
class SearchToolbar extends StatefulWidget {
///Describes the search toolbar constructor.
SearchToolbar({
this.controller,
this.onTap,
this.showTooltip = true,
Key key,
}) : super(key: key);
/// Indicates whether the tooltip for the search toolbar items should be shown or not.
final bool showTooltip;
/// An object that is used to control the [SfPdfViewer].
final PdfViewerController controller;
/// Called when the search toolbar item is selected.
final SearchTapCallback onTap;
@override
SearchToolbarState createState() => SearchToolbarState();
}
Step 2 : Create a SearchToolbarState class to build and display the search toolbar and its items in the user interface. We will define the functionalities for each toolbar item in this class. Our example includes the following toolbar items:
- Back button closes the search toolbar.
- Text form field entry gets the text to be searched in the document.
- Close button cancels the search progress.
- Instances information text displays the current instance index and total instances count of the searched text.
- Previous instance search navigation button navigates to the previous match instance.
- Next instance search navigation button navigates to the next match instance.
We can perform the text search operation in Flutter PDF Viewer using the searchText controller method. It takes the text to be searched and TextSearchOption as parameters. This method searches for the text and highlights all the instances of it in the document. It returns the PdfTextSearchResult object holding the result values such as total instance count, current highlighted instance index, etc.
The PdfTextSearchResult object will also help you navigate to the different instances of the searched-for text available and cancel the search operation.
Now, we are going to call the searchText method within the text form field entry’s onFieldSubmitted callback function. Also, we will implement the logic to navigate to the different text instances with the onPressed callback function of the previous and next search navigation buttons.
/// State for the SearchToolbar widget.
class SearchToolbarState extends State<SearchToolbar> {
bool _showSearchResultItems = false;
int _textLength = 0;
/// Define the focus node. To manage the life cycle, create the FocusNode in the initState method, and clean it up in the dispose method.
FocusNode _focusNode;
/// An object that is used to control the Text Form Field.
final TextEditingController _editingController = TextEditingController();
/// An object that is used to retrieve the text search result.
PdfTextSearchResult _pdfTextSearchResult = PdfTextSearchResult();
@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode?.requestFocus();
}
///Clear the text search result.
void clearSearch() {
_pdfTextSearchResult.clear();
}
@override
void dispose() {
// Clean up the focus node when the Form is disposed.
_focusNode?.dispose();
super.dispose();
}
///Display the Alert dialog to search from the beginning.
void _showSearchAlertDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
insetPadding: EdgeInsets.all(0),
title: Text('Search Result'),
content: Container(
width: 328.0,
child: Text(
'No more occurrences found. Would you like to continue to search from the beginning?')),
actions: <Widget>[
FlatButton(
child: Text('YES'),
onPressed: () {
_pdfTextSearchResult?.nextInstance();
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('NO'),
onPressed: () {
_pdfTextSearchResult?.clear();
_editingController.clear();
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Material(
color: Colors.transparent,
child: IconButton(
icon: Icon(
Icons.arrow_back,
color: Color(0x000000).withOpacity(0.54),
size: 24,
),
onPressed: () {
widget.onTap?.call('Cancel Search');
_editingController.clear();
_pdfTextSearchResult?.clear();
},
),
),
Flexible(
child: TextFormField(
style: TextStyle(
color: Color(0x000000).withOpacity(0.87), fontSize: 16),
enableInteractiveSelection: false,
focusNode: _focusNode,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
controller: _editingController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Find...',
hintStyle: TextStyle(color: Color(0x000000).withOpacity(0.34)),
),
onChanged: (text) {
if (_textLength < _editingController.value.text.length) {
_textLength = _editingController.value.text.length;
}
if (_editingController.value.text.length < _textLength) {
setState(() {
_showSearchResultItems = false;
});
}
},
onFieldSubmitted: (String value) async {
_pdfTextSearchResult =
await widget.controller.searchText(_editingController.text);
if (_pdfTextSearchResult.totalInstanceCount == 0) {
widget.onTap?.call('onSubmit');
} else {
setState(() {
_showSearchResultItems = true;
});
}
},
),
),
Visibility(
visible: _editingController.text.isNotEmpty,
child: Material(
color: Colors.transparent,
child: IconButton(
icon: Icon(
Icons.clear,
color: Color.fromRGBO(0, 0, 0, 0.54),
size: 24,
),
onPressed: () {
setState(() {
_editingController.clear();
_pdfTextSearchResult?.clear();
widget.controller.clearSelection();
_showSearchResultItems = false;
_focusNode.requestFocus();
});
widget.onTap?.call('Clear Text');
},
tooltip: widget.showTooltip ? 'Clear Text' : null,
),
),
),
Visibility(
visible: _showSearchResultItems,
child: Row(
children: [
Text(
'${_pdfTextSearchResult?.currentInstanceIndex}',
style: TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.54).withOpacity(0.87),
fontSize: 16),
),
Text(
' of ',
style: TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.54).withOpacity(0.87),
fontSize: 16),
),
Text(
'${_pdfTextSearchResult?.totalInstanceCount}',
style: TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.54).withOpacity(0.87),
fontSize: 16),
),
Material(
color: Colors.transparent,
child: IconButton(
icon: Icon(
Icons.navigate_before,
color: Color.fromRGBO(0, 0, 0, 0.54),
size: 24,
),
onPressed: () {
setState(() {
_pdfTextSearchResult?.previousInstance();
});
widget.onTap?.call('Previous Instance');
},
tooltip: widget.showTooltip ? 'Previous' : null,
),
),
Material(
color: Colors.transparent,
child: IconButton(
icon: Icon(
Icons.navigate_next,
color: Color.fromRGBO(0, 0, 0, 0.54),
size: 24,
),
onPressed: () {
setState(() {
if (_pdfTextSearchResult?.currentInstanceIndex ==
_pdfTextSearchResult?.totalInstanceCount &&
_pdfTextSearchResult?.currentInstanceIndex != 0 &&
_pdfTextSearchResult?.totalInstanceCount != 0) {
_showSearchAlertDialog(context);
} else {
widget.controller.clearSelection();
_pdfTextSearchResult?.nextInstance();
}
});
widget.onTap?.call('Next Instance');
},
tooltip: widget.showTooltip ? 'Next' : null,
),
),
],
),
),
],
);
}
}
In the previous code example, the _showSearchAlertDialog method displays the alert dialog when the full cycle of text search is completed and there exists no more occurrences of the searched text in the document.
The following are the uses of the other properties and variables in that code example:
- _showSearchResultItems represents whether to display the search result toolbar items (instances information text, Previous instance search navigation button, and Next instance search navigation button) or not. It will display these items only when there is a matching text found in the document.
- _textLength represents the text length entered in the text form field. This will help to decide whether the search result toolbar items should be displayed or not. When we edit the text after the search process, then the search result toolbar items will be hidden based on this property’s value.
- _focusNode represents the widget to be focused. Here, we are using it to control the focus of the *TextFormField * widget.
- _editingController object controls the text form field.
- _pdfTextSearchResult object holds the text search result.
Step 3: Create a** CustomSearchPdfViewer widget class and set that as the default root route of the application in the main function. Then, create a CustomSearchPdfViewerState*class to build and display the main toolbar (with title and search icon) and theSfPdfViewer*.
Here, we are going to load the SfPdfViewer with the PDF document from the network or URL and display the search toolbar when we select the search icon in the main toolbar.
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: CustomSearchPdfViewer(),
));
}
/// Represents the Homepage for navigation.
class CustomSearchPdfViewer extends StatefulWidget {
@override
CustomSearchPdfViewerState createState() => CustomSearchPdfViewerState();
}
class CustomSearchPdfViewerState extends State<CustomSearchPdfViewer> {
final PdfViewerController _pdfViewerController = PdfViewerController();
final GlobalKey<SearchToolbarState> _textSearchKey = GlobalKey();
bool _showToast;
bool _showScrollHead;
bool _showSearchToolbar;
// Ensure the entry history of text search.
LocalHistoryEntry _localHistoryEntry;
@override
void initState() {
_showToast = false;
_showScrollHead = true;
_showSearchToolbar = false;
super.initState();
}
void _ensureHistoryEntry() {
if (_localHistoryEntry == null) {
final ModalRoute<dynamic> route = ModalRoute.of(context);
if (route != null) {
_localHistoryEntry =
LocalHistoryEntry(onRemove: _handleHistoryEntryRemoved);
route.addLocalHistoryEntry(_localHistoryEntry);
}
}
}
void _handleHistoryEntryRemoved() {
_textSearchKey?.currentState?.clearSearch();
setState(() {
_showSearchToolbar = false;
_localHistoryEntry = null;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _showSearchToolbar
? AppBar(
flexibleSpace: SafeArea(
child: SearchToolbar(
key: _textSearchKey,
showTooltip: true,
controller: _pdfViewerController,
onTap: (Object toolbarItem) async {
if (toolbarItem.toString() == 'Cancel Search') {
setState(() {
_showSearchToolbar = false;
_showScrollHead = true;
if (Navigator.canPop(context)) {
Navigator.maybePop(context);
}
});
}
if (toolbarItem.toString() == 'onSubmit') {
setState(() {
_showToast = true;
});
await Future.delayed(Duration(seconds: 2));
setState(() {
_showToast = false;
});
}
},
),
),
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFAFAFA),
)
: AppBar(
title: Text(
'Syncfusion Flutter PDF Viewer',
style: TextStyle(color: Colors.black87),
),
actions: [
IconButton(
icon: Icon(
Icons.search,
color: Colors.black87,
),
onPressed: () {
setState(() {
_showScrollHead = false;
_showSearchToolbar = true;
_ensureHistoryEntry();
});
},
),
],
automaticallyImplyLeading: false,
backgroundColor: Color(0xFFFAFAFA),
),
body: Stack(
children: [
SfPdfViewer.network(
'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf',
controller: _pdfViewerController,
canShowScrollHead: _showScrollHead,
),
Visibility(
visible: _showToast,
child: Align(
alignment: Alignment.center,
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding:
EdgeInsets.only(left: 15, top: 7, right: 15, bottom: 7),
decoration: BoxDecoration(
color: Colors.grey[600],
borderRadius: BorderRadius.all(
Radius.circular(16.0),
),
),
child: Text(
'No matches found',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Roboto',
fontSize: 16,
color: Colors.white),
),
),
],
),
),
),
],
),
);
}
}
The following are the uses of other properties and variables in the previous code example:
- *_showToast * displays a toast message when any matches are found or not.
- _showScrollHead displays the scroll head when text search is in progress. In this example, we have hidden the scroll head when a text search is in progress.
- _showSearchToolbar displays the search toolbar. By default, the search toolbar will remain hidden. It will appear when we select the search icon in the main toolbar.
- _localHistoryEntry keeps the history of the text search.
- _pdfViewerController controls the instance of the SfPdfViewer.
- _textSearchKey provides access to the objects of the *SearchToolbar * widget.
Execute the application with the previous code example, and you will get the output like in the following screenshot.
Resource
For the complete working project, refer to the How to perform text search in Flutter PDF Viewer demo.
Conclusion
I hope you now know how to easily perform text search in a PDF file using the Syncfusion Flutter PDF Viewer. Please try out the example in this blog post and let us know your feedback in the comments section below.
If you are a current Syncfusion user, you can download the latest Essential Studio version from the License and Downloads page and try these features for yourself. If you aren’t a customer yet, you can try our 30-day free trial to check them out. Our other samples are available on GitHub. Also, please don’t miss our demo app in Google Play and the App Store.
If you wish to send us feedback or would like to submit any questions, please feel free to post them in the comments section below. Contact us through our support forum, Direct-Trac, or feedback portal. We are happy to assist you!
If you like this article, we think you will also like the following:
Posted on April 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.