-
-
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(), | |
); | |
} | |
} |
Thanks @collinjackson and @boeledi. You are awesome!
This class breaks my tests with error:
A Timer is still pending even after the widget tree was disposed.
@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.
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,
);
You may use the WidgetsBindingObserver and in particular the method didChangeMetrics to solve the issue.
Here are the modifications to be applied to the solution provided earlier by Collin:
I wrote an article to fully cover the topic: link
Great solution, Collin !