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.
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.
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:
/// 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:
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:
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 the SfPdfViewer.
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:
Execute the application with the previous code example, and you will get the output like in the following screenshot.
For the complete working project, refer to the How to perform text search in Flutter PDF Viewer demo.
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: