-
-
Save collinjackson/50172e3547e959cba77e2938f2fe5ff5 to your computer and use it in GitHub Desktop.
| // Copyright 2017, the Flutter project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/rendering.dart'; | |
| /// A widget that ensures it is always visible when focused. | |
| 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; | |
| EnsureVisibleWhenFocusedState createState() => new EnsureVisibleWhenFocusedState(); | |
| } | |
| class EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> { | |
| @override | |
| void initState() { | |
| super.initState(); | |
| widget.focusNode.addListener(_ensureVisible); | |
| } | |
| @override | |
| void dispose() { | |
| super.dispose(); | |
| widget.focusNode.removeListener(_ensureVisible); | |
| } | |
| Future<Null> _ensureVisible() async { | |
| // Wait for the keyboard to come into view | |
| // TODO: position doesn't seem to notify listeners when metrics change, | |
| // perhaps a NotificationListener around the scrollable could avoid | |
| // the need insert a delay here. | |
| await new Future.delayed(const Duration(milliseconds: 300)); | |
| if (!widget.focusNode.hasFocus) | |
| return; | |
| final RenderObject object = context.findRenderObject(); | |
| final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); | |
| assert(viewport != null); | |
| ScrollableState scrollableState = Scrollable.of(context); | |
| assert(scrollableState != null); | |
| ScrollPosition position = scrollableState.position; | |
| double alignment; | |
| if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) { | |
| // Move down to the top of the viewport | |
| alignment = 0.0; | |
| } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)) { | |
| // 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, | |
| ); | |
| } | |
| Widget build(BuildContext context) => widget.child; | |
| } | |
| class MyHomePage extends StatefulWidget { | |
| MyHomePage({Key key}) : super(key: key); | |
| @override | |
| _MyHomePageState createState() => new _MyHomePageState(); | |
| } | |
| class _MyHomePageState extends State<MyHomePage> { | |
| FocusNode _focusNode = new FocusNode(); | |
| @override | |
| Widget build(BuildContext context) { | |
| return new Scaffold( | |
| appBar: new AppBar( | |
| title: new Text("Focus Example"), | |
| ), | |
| body: new Center( | |
| child: new ListView( | |
| padding: new EdgeInsets.all(20.0), | |
| children: <Widget>[ | |
| new Container(height: 800.0, color: Colors.blue.shade200), | |
| new EnsureVisibleWhenFocused( | |
| focusNode: _focusNode, | |
| child: new TextFormField( | |
| focusNode: _focusNode, | |
| decoration: new InputDecoration( | |
| hintText: 'Focus me!', | |
| ), | |
| ), | |
| ), | |
| new Container(height: 800.0, color: Colors.blue.shade200), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| void main() { | |
| runApp(new MyApp()); | |
| } | |
| class MyApp extends StatelessWidget { | |
| @override | |
| Widget build(BuildContext context) { | |
| return new MaterialApp( | |
| title: 'Flutter Demo', | |
| home: new MyHomePage(), | |
| ); | |
| } | |
| } |
We found
The argument type 'RevealedOffset' can't be assigned to the parameter type 'num'.
if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) {
// Move down to the top of the viewport
alignment = 0.0;
} else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)) {
// Move up to the bottom of the viewport
alignment = 1.0;
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;
That was helpful 🙇♂️ Thank you
Two calls on viewport.getOffsetToReveal(object, alignment) should be updated to
viewport.getOffsetToReveal(object, alignment).offset for Flutter v1.12.13+hotfix.5.
"Hello, I just used this, but it didn't work
@liumengchun A lot has changed over the last couple of years. You can easily track focus states using the Focus widget now.
Flutter 3.3.x
This is simply possible with this trick:
scrollController.animateTo( // or simpler with the jumpTo method
focusNode.offset.dy,
duration: const Duration(milliseconds: 100),
curve: Curves.ease,
);

@collinjackson how would you suggest implementing this for textfields inside a nested pageview? For instance, I have a horizontally scrolling pageview nested within a larger scaffold tree...so the viewport and scrollable determined by this code aren't sufficient.
Update
Found Scrollable.ensureVisible; this ALMOST works out of the box...but it gets messy if the alignment parameter needs to be set differently for each Scrollable - like in the case of a textfield on PageView #2.