Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@collinjackson
Last active November 29, 2022 06:38
Show Gist options
  • Star 49 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save collinjackson/50172e3547e959cba77e2938f2fe5ff5 to your computer and use it in GitHub Desktop.
Save collinjackson/50172e3547e959cba77e2938f2fe5ff5 to your computer and use it in GitHub Desktop.
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(),
);
}
}
@boeledi
Copy link

boeledi 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
Copy link

Thanks @collinjackson and @boeledi. You are awesome!

@tudor07
Copy link

tudor07 commented Aug 24, 2018

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

@CosmicPangolin
Copy link

CosmicPangolin 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
Copy link

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
Copy link

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
Copy link

That was helpful 🙇‍♂️ Thank you

@pishguy
Copy link

pishguy 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

@rayworks
Copy link

rayworks commented Jan 8, 2020

Two calls on viewport.getOffsetToReveal(object, alignment) should be updated to
viewport.getOffsetToReveal(object, alignment).offset for Flutter v1.12.13+hotfix.5.

@liumengchun
Copy link

"Hello, I just used this, but it didn't work

@clayharris
Copy link

@liumengchun A lot has changed over the last couple of years. You can easily track focus states using the Focus widget now.

@MaMrEzO
Copy link

MaMrEzO commented Nov 27, 2022

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,
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment