我有一个反向的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
的底部,但是我希望不走那条路。
有什么想法吗?
答案 0 :(得分:1)
请参见下面的屏幕录像和代码:
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;
}
答案 1 :(得分:0)
也许我误解了你的问题。但是为什么不删除reverse: true
,因为它默认为false
答案 2 :(得分:0)
SingleChildScrollView(
child : ListView.builer(
shrinkWrap: true,
reverse: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return something;
},
)
);
禁用ListView
的滚动并在顶部添加SingleChildScrollView
。
希望这对您有用。
答案 3 :(得分:0)
StoreConnector<_ViewModel, List<Message>>(
converter: (store) {
// check mark ,reverse data list
if (isReverse) return store.state.dialogList;
return store.state.dialogList.reversed.toList();
},
builder: (context, dialogs) {
// Add a callback when UI render after. then change it direction;
WidgetsBinding.instance.addPostFrameCallback((t) {
// check it's items could be scroll
bool newMark = _listViewController.position.maxScrollExtent > 0;
if (isReverse != newMark) { // need
isReverse = newMark; // rebuild listview
setState(() {});
}
});
return ListView.builder(
reverse: isReverse, // if data less, it will false now.
controller: _listViewController,
itemBuilder: (context, index) => _bubbleItem(context, dialogs[index], index),
itemCount: dialogs.length,
);
},
)
答案 4 :(得分:0)
return ListView.builder(
shrinkWrap: true,
reverse: true,
itemCount: messages.length,
itemBuilder: (context, itemIndex) {
return ConversationListItem(messages[messages.length - 1 - itemIndex]);
},
);