Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: How to create a reverse ListView that initially fills empty space at the bottom

I have a reverse ListView.builder that grows downward but initially has only a single widget in it.

See repo: https://github.com/gmlewis/reverse_listview

It puts all the empty space above the first widget in the list due to the reverse: true, and it looks like: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/UndesiredListView.png

As more widgets are added, it looks like: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/PopulatedListView.png

I would like it to fill all empty space at the bottom instead of at the top, and look like: https://github.com/gmlewis/reverse_listview/raw/master/assets/images/DesiredListView.png Obviously, when the content starts getting long enough to fill the screen, the initial widget scrolls off the top as you would expect.

I dug into the ScrollView and Viewport classes and found the anchor and center settings but could not make it do what I want.

It seems like my only remaining option might be to move the first widget to the bottom of the AppBar but I was hoping to not go that route.

Any ideas?

like image 740
Glenn Avatar asked Jul 12 '18 15:07

Glenn


3 Answers

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

Disable the scroll of ListView and add SingleChildScrollView on top. Hope this works for you.

like image 183
Kathirva Avatar answered Oct 10 '22 08:10

Kathirva


return ListView.builder(
  shrinkWrap: true,
  reverse: true,
  itemCount: messages.length,
  itemBuilder: (context, itemIndex) {
    return ConversationListItem(messages[messages.length - 1 - itemIndex]);
  },
);
like image 1
Thiago L. Avatar answered Oct 10 '22 08:10

Thiago L.


  1. Use a SizedBox between the options container and Compose Text container.
  2. Get the exact initial height of the SizedBox after the initial build is complete with the addPostFrameCallback.
  3. Keep making the SizedBox smaller as Text is input.
  4. Use TextPainter to get the height of the Text in order to proportionally reduce the height of SizedBox.

Please see the screen recording and the code below:

app screen recording

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;
}
like image 2
bluenile Avatar answered Oct 10 '22 06:10

bluenile