// ignore_for_file: file_names import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; // import 'package:meta/meta.dart'; /// /// Helper class that ensures a Widget is visible when it has the focus /// For example, for a TextFormField when the keyboard is displayed /// /// How to use it: /// /// In the class that implements the Form, /// Instantiate a FocusNode /// FocusNode _focusNode = new FocusNode(); /// /// In the build(BuildContext context), wrap the TextFormField as follows: /// /// new EnsureVisibleWhenFocused( /// focusNode: _focusNode, /// child: new TextFormField( /// ... /// focusNode: _focusNode, /// ), /// ), /// /// Initial source code written by Collin Jackson. /// Extended (see highlighting) to cover the case when the keyboard is dismissed and the /// user clicks the TextFormField/TextField which still has the focus. /// class EnsureVisibleWhenFocused extends StatefulWidget { const EnsureVisibleWhenFocused({ Key? key, required this.child, required this.focusNode, this.curve = Curves.ease, this.duration = const Duration(milliseconds: 100), }) : super(key: key); /// The node we will monitor to determine if the child is focused final FocusNode focusNode; /// The child widget that we are wrapping final Widget child; /// The curve we will use to scroll ourselves into view. /// /// Defaults to Curves.ease. final Curve curve; /// The duration we will use to scroll ourselves into view /// /// Defaults to 100 milliseconds. final Duration duration; @override _EnsureVisibleWhenFocusedState createState() => _EnsureVisibleWhenFocusedState(); } /// /// We implement the WidgetsBindingObserver to be notified of any change to the window metrics /// class _EnsureVisibleWhenFocusedState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); widget.focusNode.addListener(_ensureVisible); WidgetsBinding.instance!.addObserver(this); } @override void dispose() { WidgetsBinding.instance!.removeObserver(this); widget.focusNode.removeListener(_ensureVisible); super.dispose(); } /// /// This routine is invoked when the window metrics have changed. /// This happens when the keyboard is open or dismissed, among others. /// It is the opportunity to check if the field has the focus /// and to ensure it is fully visible in the viewport when /// the keyboard is displayed /// @override void didChangeMetrics() { if (widget.focusNode.hasFocus) { _ensureVisible(); } } /// /// This routine waits for the keyboard to come into view. /// In order to prevent some issues if the Widget is dismissed in the /// middle of the loop, we need to check the "mounted" property /// /// This method was suggested by Peter Yuen (see discussion). /// Future _keyboardToggled() async { if (mounted) { EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets; while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) { await Future.delayed(const Duration(milliseconds: 10)); } } return; } Future _ensureVisible() async { // Wait for the keyboard to come into view await Future.any([ Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled() ]); // No need to go any further if the node has not the focus if (!widget.focusNode.hasFocus) { return; } // Find the object which has the focus final RenderObject? object = context.findRenderObject(); final RenderAbstractViewport? viewport = RenderAbstractViewport.of(object); // If we are not working in a Scrollable, skip this routine if (viewport == null) { return; } // Get the Scrollable state (in order to retrieve its offset) ScrollableState? scrollableState = Scrollable.of(context); assert(scrollableState != null); // Get its offset ScrollPosition position = scrollableState!.position; double alignment; if (position.pixels > viewport.getOffsetToReveal(object!, 0.0).offset) { // Move down to the top of the viewport alignment = 0.0; } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset) { // Move up to the bottom of the viewport alignment = 1.0; } else { // No scrolling is necessary to reveal the child return; } position.ensureVisible( object, alignment: alignment, duration: widget.duration, curve: widget.curve, ); } @override Widget build(BuildContext context) { return widget.child; } }