From 86e3f4ba4ce88fb3e478c2730bec059e87bfbd31 Mon Sep 17 00:00:00 2001 From: Ivan Murashov Date: Fri, 9 Mar 2018 10:57:11 +0300 Subject: [PATCH] Scroll to textfield on focus --- lib/base/base_state.dart | 89 +++++++++++++++++++++++++++++++++-- lib/screens/registration.dart | 8 +++- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/lib/base/base_state.dart b/lib/base/base_state.dart index e2e1174..48e7a80 100644 --- a/lib/base/base_state.dart +++ b/lib/base/base_state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:checker/resources.dart'; import 'package:flutter/material.dart'; @@ -7,6 +9,8 @@ import 'package:checker/screens/settings.dart'; import 'package:checker/screens/faq.dart'; import 'package:checker/strings.dart'; import 'package:checker/db.dart'; +import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart'; abstract class BaseState extends State { @@ -249,7 +253,7 @@ abstract class BaseState extends State { EdgeInsets getInputFieldContainerPadding() { const double verticalPadding = 12.0; const double horizontalPadding = 16.0; - return new EdgeInsets.only(top: verticalPadding, + return new EdgeInsets.only(top: 256.0, bottom: verticalPadding, left: horizontalPadding, right: horizontalPadding); @@ -277,8 +281,6 @@ abstract class BaseState extends State { return new Container(height: containerHeight, child: new Image.asset(Resources.getLogo(app), width: imageWidth)); } - - /// Возвращает текстовое поле, с однострочным пояснением над ним. Widget getValueWithDescription(String title, String value) { return new Container(padding: new EdgeInsets.only(left: verticalMargin, right: verticalMargin, top: 18.0), @@ -303,3 +305,84 @@ abstract class BaseState extends State { return new Container(margin: margin, height: buttonHeight, child: new Row(children: [new Expanded(child: widget)])); } } + +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 { + @override + void initState() { + super.initState(); + widget.focusNode.addListener(_ensureVisible); + } + + @override + void dispose() { + super.dispose(); + widget.focusNode.removeListener(_ensureVisible); + } + + Future _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; +} diff --git a/lib/screens/registration.dart b/lib/screens/registration.dart index 4b73676..8521752 100644 --- a/lib/screens/registration.dart +++ b/lib/screens/registration.dart @@ -22,6 +22,7 @@ class RegistrationScreen extends BaseScreen { class RegistrationScreenState extends BaseState { RegistrationScreenState(SqliteHelper helper, String app) : super(helper, app); + FocusNode _focusNode = new FocusNode(); @override Widget build(BuildContext ctx) { @@ -54,12 +55,15 @@ class RegistrationScreenState extends BaseState { @override getTextWidget() { - return new TextField( + return new EnsureVisibleWhenFocused( + focusNode: _focusNode, + child: new TextField( + focusNode: _focusNode, keyboardType: TextInputType.number, decoration: new InputDecoration.collapsed( hintText: getHintString(), hintStyle: new TextStyle(color: greyTextColor, fontSize: 16.0)), - onChanged: (text) => handleUserInput(text)); + onChanged: (text) => handleUserInput(text))); } /// Возвращает кнопку регистрации.