Flutter: как создать обратный ListView, который изначально заполняет пустое пространство внизу

У меня есть обратный ListView.builder, который растет вниз, но изначально имеет только один виджет.

См. репозиторий: https://github.com/gmlewis/reverse_listview.

Он помещает все пустое пространство над первым виджетом в списке из-за reverse: true и выглядит так: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/UndesiredListView.png

По мере добавления дополнительных виджетов он выглядит следующим образом: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/PopulatedListView.png

Я хотел бы, чтобы он заполнял все пустое пространство внизу, а не вверху, и выглядел так: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/DesiredListView.png Очевидно, что когда содержимое становится достаточно длинным, чтобы заполнить экран, начальный виджет прокручивается сверху, как и следовало ожидать.

Я покопался в классах ScrollView и Viewport и нашел настройки anchor и center, но не смог заставить их делать то, что я хочу.

Кажется, единственным оставшимся вариантом может быть перемещение первого виджета в нижнюю часть AppBar, но я надеялся не идти по этому пути.

Любые идеи?


person Glenn    schedule 12.07.2018    source источник
comment
Я также сталкиваюсь с той же проблемой. Если бы вы нашли некоторые, пожалуйста, напишите это в комментариях.   -  person Swapnil Dhotre    schedule 23.04.2020
comment
Где вы когда-либо в состоянии решить это?   -  person user3808307    schedule 07.12.2020


Ответы (4)


  1. Используйте SizedBox между контейнером параметров и контейнером Compose Text.
  2. Получите точную начальную высоту SizedBox после завершения начальной сборки с помощью addPostFrameCallback.
  3. Продолжайте уменьшать SizedBox по мере ввода текста.
  4. Используйте TextPainter, чтобы получить высоту текста, чтобы пропорционально уменьшить высоту SizedBox.

Пожалуйста, посмотрите запись экрана и код ниже:

запись экрана приложения

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Reverse ListView Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  final TextEditingController _textController = TextEditingController();

  double _sizedBoxHeight;
  Size _screenSize;
  double _textHeight;
  final GlobalKey _redKey = GlobalKey();
  final GlobalKey _appBarKey = GlobalKey();
  double appBarHeight;

  List<Widget> widgets = [];

  Size _getSizes(GlobalKey key) {
    final RenderBox renderBoxRed = key.currentContext.findRenderObject();
    final sizeRed = renderBoxRed.size;
    return sizeRed;
  }

  num _textDetails(BuildContext context, [text = ""]) {
    final TextPainter textPainter = TextPainter(
      text: TextSpan(
        text: text,
        style: TextStyle(
            color: Colors.black,
            fontSize: Theme.of(context).textTheme.bodyText2.fontSize),
      ),
      textDirection: TextDirection.ltr,
      textScaleFactor: MediaQuery.of(context).textScaleFactor,
    )..layout(minWidth: 0, maxWidth: _screenSize.width);
    if (text.isEmpty) {
      return textPainter.size.height;
    } else {
      return textPainter.computeLineMetrics().length;
    }
  }

  _insertBlanks() async {
    final Size _appBarSize = _getSizes(_appBarKey);
    final Size _containerSize = _getSizes(_redKey);

    final int _blankLinesTotal = ((_screenSize.height -
            _appBarSize.height -
            _containerSize.height -
            60) ~/
        _textHeight);

    final double blankLinesHeight = _textHeight * _blankLinesTotal;
    _sizedBoxHeight = blankLinesHeight +
        (_screenSize.height -
            _appBarSize.height -
            _containerSize.height -
            60 -
            blankLinesHeight);
    widgets.insert(0, SizedBox(height: _sizedBoxHeight));
    setState(() {});
  }

  void _addRequest(String text, BuildContext context) {
    setState(() {
      if (_sizedBoxHeight > 0) {
        final int _numLines = _textDetails(context, text);
        _sizedBoxHeight = _sizedBoxHeight - (_textHeight * _numLines);
        widgets[widgets.length - 2] =
            SizedBox(height: _sizedBoxHeight >= 0 ? _sizedBoxHeight : 0);
      }
      widgets.insert(0, Text(text));
    });
  }

  @override
  initState() {
    super.initState();
    final Widget intro = buildHelperIntro();
    widgets.insert(0, intro);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _insertBlanks();
    });
  }

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _screenSize = MediaQuery.of(context).size;
    _textHeight = _textDetails(context);

    return Scaffold(
      appBar: AppBar(
        key: _appBarKey,
        elevation: 0.0,
        title: Text(widget.title),
      ),
      body: Container(
        color: Colors.black12, // Why doesn't this fill the full ListView?
        child: Column(
          children: <Widget>[
            Expanded(
              child: ListView.builder(
                reverse: true,
                itemBuilder: (_, index) => widgets[index],
                itemCount: widgets.length,
              ),
            ),
            const Divider(height: 1.0),
            Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ],
        ),
      ),
    );
  }

  Widget buildHelperIntro() {
    return Container(
      key: _redKey,
      color: Colors.black12,
      child: Column(
        children: <Widget>[
          Row(
            mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              Expanded(
                child: Container(
                  padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 8.0),
                  color: const Color(0xFF2196F3),
                  child: const Text(
                    "Hi! Try clicking an option below.",
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 24.0,
                      color: Colors.white,
                    ),
                  ),
                ),
              ),
            ],
          ),
          Options(
            [
              "Option number 1",
              "Option number 2",
              "Option number 3",
              "Option number 4",
              "Option number 5",
            ],
            (text) {
              _addRequest(text, context);
            },
          ),
        ],
      ),
    );
  }

  updateSubmitButton() {
    if (!mounted) {
      return;
    }
    setState(() {
      // Force evaluation of _textController state.
    });
  }

  void _handleSubmitted(String text, BuildContext context) {
    _textController.clear();
    FocusScope.of(context).requestFocus(FocusNode()); // dismiss keyboard.
    updateSubmitButton();
    _addRequest(text, context);
  }

  Widget _buildTextComposer() {
    return IconTheme(
      data: IconThemeData(color: Theme.of(context).accentColor),
      child: Container(
        height: 60.0,
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: Row(children: <Widget>[
          Flexible(
            child: Card(
              color: Colors.white,
              shape: const RoundedRectangleBorder(
                side: BorderSide(
                  color: Colors.black12,
                ),
                borderRadius: BorderRadius.all(Radius.circular(20.0)),
              ),
              child: Padding(
                padding: const EdgeInsets.all(12.0),
                child: TextField(
                  controller: _textController,
                  onChanged: (String text) {
                    updateSubmitButton();
                  },
                  onSubmitted: (text) {
                    _handleSubmitted(text, context);
                  },
                  decoration: const InputDecoration.collapsed(
                    hintText: "Start typing...",
                  ),
                ),
              ),
            ),
          ),
          Container(
              child: IconButton(
            icon: const Icon(Icons.send, color: Color(0xFF2196F3)),
            onPressed: _textController.text.length > 0
                ? () => _handleSubmitted(_textController.text, context)
                : null,
          )),
        ]),
      ),
    );
  }
}

typedef StringCallback(String text);

class Options extends StatelessWidget {
  final List<String> requests;
  final StringCallback addRequest;

  const Options(this.requests, this.addRequest);

  @override
  Widget build(BuildContext context) {
    final children = List.generate(
        2 * requests.length - 1,
        (int index) => index % 2 == 0
            ? _SingleRequest(requests[index ~/ 2], addRequest)
            : const Divider(color: Colors.black));

    return CustomPaint(
      painter: _DrawArc(),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(14.0, 8.0, 14.0, 8.0),
        child: Card(
          elevation: 10.0,
          color: Colors.white,
          shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(20.0))),
          child: Padding(
            padding: const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: children,
            ),
          ),
        ),
      ),
    );
  }
}

class _SingleRequest extends StatelessWidget {
  final String request;
  final StringCallback addRequest;

  const _SingleRequest(this.request, this.addRequest);

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      onPressed: () {
        addRequest(request);
      },
      child: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          const Padding(
            padding: const EdgeInsets.fromLTRB(4.0, 0.0, 0.0, 3.0),
            child: CircleAvatar(
              radius: 10.0,
              backgroundColor: Color(0xFF2196F3),
              child: CircleAvatar(
                radius: 6.0,
                backgroundColor: Colors.white,
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(12.0, 6.0, 0.0, 4.0),
            child: Text(
              request,
              style: const TextStyle(
                color: Colors.black,
                fontSize: 20.0,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _DrawArc extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    const padH = 52.0;
    final Paint paint = Paint()..color = const Color(0xFF2196F3);
    canvas.drawRect(Rect.fromLTWH(0.0, 0.0, size.width, padH), paint);
    final h = 0.1 * size.height;
    canvas.drawArc(Rect.fromLTWH(0.0, -0.5 * h + padH, size.width, h), 0.0,
        math.pi, false, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
person bluenile    schedule 08.12.2020
comment
Выглядит хорошо, @bluenile! Благодарю вас! - person Glenn; 08.12.2020
comment
Добро пожаловать, @Glenn. Мне понравился ваш чистый код и простой, но красивый интерфейс. - person bluenile; 08.12.2020

 SingleChildScrollView(
          child : ListView.builer(
            shrinkWrap: true,
            reverse: true,
            physics: NeverScrollableScrollPhysics(),
            itemBuilder: (BuildContext context, int index) {
              return something;
            },
          )
      );

Отключите прокрутку ListView и добавьте SingleChildScrollView сверху. Надеюсь, что это работает для вас.

person Kathirva    schedule 22.03.2019
comment
Это очень близко, Катирва, спасибо... но я хочу, чтобы он по-прежнему продвигался естественным образом и показывал последний добавленный элемент (например, окно чата), когда в _addRequest добавляются новые элементы (за нижнюю часть экрана). Как бы вы фокусировали прокрутку на последнем элементе каждый раз, когда новый элемент добавляется в список виджетов? - person Glenn; 23.03.2019

Возможно, я неправильно понимаю ваш вопрос. но почему бы вам просто не удалить reverse: true, так как по умолчанию это ложь

person Community    schedule 27.12.2018
comment
К сожалению, он ведет себя по-другому... в частности, он не отслеживает самые последние добавления в список. Кроме того, пользовательский рендеринг не сохраняется должным образом. - person Glenn; 28.12.2018

person    schedule
comment
Извините, но единственное отличие здесь от реальной демонстрации репо — это добавление shrinkWrap: true,, и это ведет себя идентично репо. - person Glenn; 30.09.2020
comment
Индекс списка элементов (элементов) теперь равен idx = items.lengh -1 - itemIndex, тогда итерация списка элементов n, n-1, n-2 до 0. ConversationListItem(messages[messages.length - 1 - itemIndex]); - person Thiago L.; 30.09.2020
comment
В репозитории нет ConversationListItem. - person Glenn; 01.10.2020