167 lines
4.7 KiB
Dart
167 lines
4.7 KiB
Dart
// 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<EnsureVisibleWhenFocused>
|
|
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<void> _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<void> _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;
|
|
}
|
|
}
|