Как стать автором
Обновить

Комментарии 8

А в чем проблема использовать Stack? Текущее решение мне кажется оверинжирингом, во всяком случае, для конкретной указанной проблемы.


Во-первых, Stack все равно используется внутри buildOverlay. Во-вторых, надо оборачивать каждый TextField.


Я бы сделал что-нибудь типа такого и обернул им SingleChildScrollView:


class Wrapper extends StatelessWidget {
  const Wrapper({Key key, this.child, this.isVisible}) : super(key: key);

  final Widget child;
  final bool isVisible;

  @override
  Widget build(BuildContext context) {
    const double height = 50;
    return Stack(
      children: <Widget>[
        Padding(
            padding: EdgeInsets.only(bottom: isVisible ? height : 0),
            child: child,
          ),
        if (isVisible)
          Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              color: Colors.red,
              height: height,
            ),
          ),
      ],
    );
  }
}
Можно оставить чисто стек, но Overlay (неважно что внутри) более независимо.
Придется тогда весь экран класть в стек, это может породить дополнительные проблемы.

Текущий кейс просто пример. После написания статьи, скроллил к активному фильтру в AppBar, у фильтров динамический размер. Обернул каждый в RenderMetricsObject, сложил размеры и проскроллил при заходе на экран.

А как доскралливать на разницу в вашем решении, плашка же перекроет поле. Особенно, если поле многострочное. Или вы только о инструментах позиционирования?
— В OverlayEntry можно прокидывать такие виджеты как Positioned и другие, которые работают со Stack.
То есть не надо строить Stack в OverlayEntry если у вас 1 ребёнок
Тут и тут

— Не уверен, что скролл вниз удобен.
Скролл вверх, чтобы поле стало видимым — это понятно, но наоборот — скорее не привычно

Данный случай можно решить так
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:keyboard_visibility/keyboard_visibility.dart';

void main() => runApp(
      MaterialApp(
        home: App(),
      ),
    );

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class NextBlock extends StatelessWidget {
  const NextBlock({
    Key key,
    this.isShow,
  }) : super(key: key);

  final bool isShow;

  @override
  Widget build(BuildContext context) {
    if (!isShow) return const SizedBox();

    return ColoredBox(
      color: const Color.fromRGBO(0, 0, 0, 0.3),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: FlatButton(
          color: Colors.white,
          onPressed: () {},
          child: Text('Next'),
        ),
      ),
    );
  }
}

class _AppState extends State<App> {
  final list = List.generate(20, (index) => index.toString());
  bool _isShow = false;
  OverlayEntry _overlayEntry;

  KeyboardVisibilityNotification _keyboardListener;

  final _key = GlobalKey();
  double _height = 0;

  @override
  void initState() {
    super.initState();
    _overlayEntry = OverlayEntry(builder: _buildOverlay);
    _keyboardListener = KeyboardVisibilityNotification()
      ..addNewListener(onChange: _keyboardHandle);

    SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
      Overlay.of(context).insert(_overlayEntry);
    });
  }

  @override
  void dispose() {
    _keyboardListener.dispose();
    _overlayEntry.remove();
    super.dispose();
  }

  Widget _buildOverlay(BuildContext context) => Positioned(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 0,
        right: 0,
        child: AnimatedOpacity(
          key: _key,
          duration: const Duration(milliseconds: 200),
          opacity: _isShow ? 1.0 : 0.0,
          child: NextBlock(
            isShow: _isShow,
          ),
        ),
      );

  void _keyboardHandle(bool isVisible) {
    _isShow = isVisible;
    _overlayEntry?.markNeedsBuild();
    if (isVisible) {
      SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
        final height = _key.currentContext.size.height;
        if (height != _height) {
          setState(() => _height = height);
        }
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    return MediaQuery(
      data: _isShow
          ? mediaQuery.copyWith(
              viewInsets: mediaQuery.viewInsets.copyWith(
                bottom: mediaQuery.viewInsets.bottom + _height,
              ),
            )
          : mediaQuery,
      child: Scaffold(
        body: SingleChildScrollView(
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                children: <Widget>[
                  for (String value in list)
                    TextField(
                      decoration: InputDecoration(labelText: value),
                    )
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

1. Круто.
2. По поводу вниз — это было требование заказчика. Данный пример реальный кейс из приложения, просто UI другой. Не стал ничего менять и показал как есть.
Тогда так
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:keyboard_visibility/keyboard_visibility.dart';

void main() => runApp(
      MaterialApp(
        home: App(),
      ),
    );

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class NextBlock extends StatelessWidget {
  const NextBlock({
    Key key,
    this.isShow,
  }) : super(key: key);

  final bool isShow;

  @override
  Widget build(BuildContext context) {
    if (!isShow) return const SizedBox();

    return ColoredBox(
      color: const Color.fromRGBO(0, 0, 0, 0.3),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: FlatButton(
          color: Colors.white,
          onPressed: () {},
          child: Text('Next'),
        ),
      ),
    );
  }
}

class _AppState extends State<App> {
  final list = List.generate(20, (index) => index.toString());
  bool _isShow = false;
  OverlayEntry _overlayEntry;

  KeyboardVisibilityNotification _keyboardListener;
  FocusManager _focusScope;

  final _key = GlobalKey();
  final _scrollKey = GlobalKey();
  double _height = 0;

  @override
  void initState() {
    super.initState();
    _overlayEntry = OverlayEntry(builder: _buildOverlay);
    _keyboardListener = KeyboardVisibilityNotification()
      ..addNewListener(onChange: _keyboardHandle);

    SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
      _focusScope = context.owner.focusManager..addListener(_handleFocusChange);
      Overlay.of(context).insert(_overlayEntry);
    });
  }

  void _handleFocusChange() {
    final textFieldContext = FocusScope.of(context).focusedChild.context;

    Scrollable.ensureVisible(
      textFieldContext,
      alignment: 0.9,
      duration: const Duration(milliseconds: 400),
    );
  }

  @override
  void dispose() {
    _focusScope.removeListener(_handleFocusChange);
    _keyboardListener.dispose();
    _overlayEntry.remove();
    super.dispose();
  }

  Widget _buildOverlay(BuildContext context) => Positioned(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 0,
        right: 0,
        child: AnimatedOpacity(
          key: _key,
          duration: const Duration(milliseconds: 200),
          opacity: _isShow ? 1.0 : 0.0,
          child: NextBlock(
            isShow: _isShow,
          ),
        ),
      );

  void _keyboardHandle(bool isVisible) {
    _isShow = isVisible;
    _overlayEntry?.markNeedsBuild();
    if (isVisible) {
      SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
        final height = _key.currentContext.size.height;
        if (height != _height) {
          setState(() => _height = height);
        }
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    return MediaQuery(
      data: _isShow
          ? mediaQuery.copyWith(
              viewInsets: mediaQuery.viewInsets.copyWith(
                bottom: mediaQuery.viewInsets.bottom + _height,
              ),
            )
          : mediaQuery,
      child: Scaffold(
        body: SingleChildScrollView(
          key: _scrollKey,
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                children: <Widget>[
                  for (String value in list)
                    TextField(
                      decoration: InputDecoration(labelText: value),
                    )
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

А если у виджета текстового поля будут паддинги, контейнеры и т.д., то как тогда доскроллить в нужное место в вашем решении?

Также если нужно будет скроллить не до текстового поля, а допустим, до кнопки, то ваше решение выше не подойдет.

Только если использовать колбэк для получения контекста или GlobalKey.
Ваше решение тоже не идеально, так как нижнее поле закрыто оверлеем, хотя речь в статье об этом, а значит основная проблема не решена.
Скриншот
image


В любом случае надеюсь в библиотеке появится что-то, что бы само пробрасывало менеджер и реализация для сливеров
Согласен.
Цель библиотеки получать:
— размеры
— позицию
— разницу
Любых виджетов.

С этим она справляется.

Сам пример да, не идеален.

Или же вы о чем-то другом?
Зарегистрируйтесь на Хабре , чтобы оставить комментарий