Skip to content

Instantly share code, notes, and snippets.

@collinjackson collinjackson/main.dart
Last active Jul 16, 2019

Embed
What would you like to do?
Demonstrates scrolling a focused widget into view
// 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(),
);
}
}
@theobouwman

This comment has been minimized.

Copy link

commented Jul 21, 2017

I cant get it to work...

@ryanchavez

This comment has been minimized.

Copy link

commented Nov 30, 2017

Thanks for this!

I did notice, on Android, that when the TextFormField has focus and the keyboard is dismissed, clicking on the TextFormField will cause the keyboard to appear again, but it doesn't get scrolled into view.

@boeledi

This comment has been minimized.

Copy link

commented Apr 28, 2018

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:

///
/// 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();
}
}
...

I wrote an article to fully cover the topic: link

Great solution, Collin !

@figengungor

This comment has been minimized.

Copy link

commented Aug 10, 2018

Thanks @collinjackson and @boeledi. You are awesome!

@tudor07

This comment has been minimized.

Copy link

commented Aug 24, 2018

This class breaks my tests with error:
A Timer is still pending even after the widget tree was disposed.

@CosmicPangolin

This comment has been minimized.

Copy link

commented Feb 20, 2019

@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.

@abinashranjan

This comment has been minimized.

Copy link

commented May 23, 2019

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;

@RounakTadvi

This comment has been minimized.

Copy link

commented Jun 6, 2019

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;

@ncuillery-youitv

This comment has been minimized.

Copy link

commented Jun 17, 2019

That was helpful 🙇 Thank you

@MahdiPishguy

This comment has been minimized.

Copy link

commented Jun 20, 2019

simple screen shot to know what happen with this source code

device-2019-06-20-115600

device-2019-06-20-121137

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.