今天我们要实现一个豆瓣的Top150页面,看下豆瓣的效果。
我们实现的效果:
ListView,顾名思义,列表组件。
ListView({
Key key,
//垂直还是水平方向
Axis scrollDirection = Axis.vertical,
//列表数据是否翻转
bool reverse = false,
//控制滚动的组件,比如:将listview滚动到某个位置,此时可以使用。
ScrollController controller,
//
bool primary,
ScrollPhysics physics,
//listView的大小是否占满整个空间。false:占满,true:不占满
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
//listview的内容,子组件。
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
shrinkWrap(bool)
shrinkWrap | 效果 |
---|---|
true | |
false |
import 'package:flutter/material.dart';
import 'dart:math';
class FlutterListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: getListWidgets());
}
}
//生成listview children Widgets
List<Widget> getListWidgets() {
List<ItemData> list = List();
Random random = Random();
for (int i = 0; i < 100; i++) {
int r = random.nextInt(255);
int g = random.nextInt(255);
int b = random.nextInt(255);
list.add(ItemData(Color.fromARGB(255, r, g, b), i.toString()));
}
return list.map((item) => ListViewItem(item)).toList();
}
class ListViewItem extends StatelessWidget {
final ItemData itemData;
ListViewItem(this.itemData);
@override
Widget build(BuildContext context) {
return Container(
width: 150,
height: 70,
//ListTile可以作为listView的一种子组件类型,支持配置点击事件,一个拥有固定样式的Widget
child: ListTile(
leading: CircleAvatar(
backgroundColor: itemData.color,
child: Text(
itemData.text,
style: TextStyle(color: Colors.white),
),
),
title: Text(itemData.text),
),
);
}
}
class ItemData {
final Color color;
final String text;
ItemData(this.color, this.text);
}
ListView也支持水平方向显示,设置Axis scrollDirection = Axis.horizontal,即可。
一般我们使用静态listview,也就是listview中的children较少的时候,一般会直接使用ListView的构造函数中的children,显示Widget。如果比如加载网络数据,listView数据量较多,listview的构建方式就不能直接使用默认构造方法去设置了。这时我们通过使用ListView的builder方法。
ListView.builder
ListView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
//listView的item数量
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
})
itemBuilder
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
可以看到itemBuilder
是一个方法对象,参数为:(BuildContext context, int index)
,返回值是Widget,也就是listView的子item。
ListView.builder(
//item 的数量,subjects为我们自己的数据源。
itemCount: subjects.length,
itemBuilder: (BuildContext context, int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
return Text('Hello')
],
);
});
使用起来还是比较简单的。
下方高能能,直接开始讲如何写一个豆瓣Top150页面
import 'dart:convert' as Convert;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class DouBanListView extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return DouBanState();
}
}
class DouBanState extends State<DouBanListView> {
var subjects = [];
var itemHeight = 150.0;
requestMovieTop() async {
var httpClient = new HttpClient();
//http://api.douban.com/v2/movie/top250?start=25&count=10
var uri = new Uri.http(
'api.douban.com', '/v2/movie/top250', {'start': '0', 'count': '150'});
var request = await httpClient.getUrl(uri);
var response = await request.close();
var responseBody = await response.transform(Convert.utf8.decoder).join();
Map data = Convert.jsonDecode(responseBody);
setState(() {
subjects = data['subjects'];
});
}
@override
void initState() {
requestMovieTop();
}
@override
Widget build(BuildContext context) {
return Container(
child: getListViewContainer(),
);
}
getListViewContainer() {
if (subjects.length == 0) {
//loading
return CupertinoActivityIndicator();
}
return ListView.builder(
//item 的数量
itemCount: subjects.length,
itemBuilder: (BuildContext context, int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
numberWidget(index+1),
getItemContainerView(subjects[index]),
//下面的灰色分割线
Container(
height: 10,
color: Color.fromARGB(255, 234, 233, 234),
)
],
);
});
}
//肖申克的救赎(1993) View
getTitleView(subject) {
var title = subject['title'];
var year = subject['year'];
return Container(
child: Row(
children: <Widget>[
Icon(
Icons.play_circle_outline,
color: Colors.redAccent,
),
Text(
title,
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
Text('($year)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey))
],
),
);
}
getItemContainerView(var subject) {
var imgUrl = subject['images']['medium'];
return Container(
width: double.infinity,
padding: EdgeInsets.all(5.0),
child: Row(
children: <Widget>[
getImage(imgUrl),
Expanded(
child: getMovieInfoView(subject),
flex: 1,
)
],
),
);
}
//圆角图片
getImage(var imgUrl) {
return Container(
decoration: BoxDecoration(
image:
DecorationImage(image: NetworkImage(imgUrl), fit: BoxFit.cover),
borderRadius: BorderRadius.all(Radius.circular(5.0))),
margin: EdgeInsets.only(left: 8, top: 3, right: 8, bottom: 3),
height: itemHeight,
width: 100.0,
);
}
getStaring(var stars) {
return Row(
children: <Widget>[RatingBar(stars), Text('$stars')],
);
}
//电影标题,星标评分,演员简介Container
getMovieInfoView(var subject) {
var start = subject['rating']['average'];
return Container(
height: itemHeight,
alignment: Alignment.topLeft,
child: Column(
children: <Widget>[
getTitleView(subject),
RatingBar(start),
DescWidget(subject)
],
),
);
}
//NO.1 图标
numberWidget(var no) {
return Container(
child: Text(
'No.$no',
style: TextStyle(color: Color.fromARGB(255, 133, 66, 0)),
),
decoration: BoxDecoration(
color: Color.fromARGB(255, 255, 201, 129),
borderRadius: BorderRadius.all(Radius.circular(5.0))),
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
margin: EdgeInsets.only(left: 12, top: 10),
);
}
}
//类别、演员介绍
class DescWidget extends StatelessWidget {
var subject;
DescWidget(this.subject);
@override
Widget build(BuildContext context) {
var casts = subject['casts'];
var sb = StringBuffer();
var genres = subject['genres'];
for (var i = 0; i < genres.length; i++) {
sb.write('${genres[i]} ');
}
sb.write("/ ");
List<String> list = List.generate(
casts.length, (int index) => casts[index]['name'].toString());
for (var i = 0; i < list.length; i++) {
sb.write('${list[i]} ');
}
return Container(
child: Text(
sb.toString(),
softWrap: true,
textDirection: TextDirection.ltr,
style:
TextStyle(fontSize: 16, color: Color.fromARGB(255, 118, 117, 118)),
),
);
}
}
class RatingBar extends StatelessWidget {
double stars;
RatingBar(this.stars);
@override
Widget build(BuildContext context) {
List<Widget> startList = [];
//实心星星
var startNumber = stars ~/ 2;
//半实心星星
var startHalf = 0;
if (stars.toString().contains('.')) {
int tmp = int.parse((stars.toString().split('.')[1]));
if (tmp >= 5) {
startHalf = 1;
}
}
//空心星星
var startEmpty = 5 - startNumber - startHalf;
for (var i = 0; i < startNumber; i++) {
startList.add(Icon(
Icons.star,
color: Colors.amberAccent,
size: 18,
));
}
if (startHalf > 0) {
startList.add(Icon(
Icons.star_half,
color: Colors.amberAccent,
size: 18,
));
}
for (var i = 0; i < startEmpty; i++) {
startList.add(Icon(
Icons.star_border,
color: Colors.grey,
size: 18,
));
}
startList.add(Text(
'$stars',
style: TextStyle(
color: Colors.grey,
),
));
return Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.only(left: 0, top: 8, right: 0, bottom: 5),
child: Row(
children: startList,
),
);
}
}
最终效果:
listview点击事件
点击事件,使用的是GestureDetector
,这是Flutter中的手势处理Widget。
ListView.builder(
//item 的数量
itemCount: subjects.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(//Flutter 手势处理
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
numberWidget(index + 1),
getItemContainerView(subjects[index]),
//下面的灰色分割线
Container(
height: 10,
color: Color.fromARGB(255, 234, 233, 234),
)
],
),
),
onTap: () {
//监听点击事件
print("click item index=$index");
},
);
});
总体来说,上手ListView还是比较简单的。只不过,在listView的item的布局排列中,可能看的不是太懂。如果有不太理解的,可以看看我的前面几篇博客。这里用到的属性,布局方式,之前都有讲过。
本文代码地址
Flutter 豆瓣客户端,诚心开源
Flutter Container
Flutter SafeArea
Flutter Row Column MainAxisAlignment Expanded
Flutter Image全解析
Flutter 常用按钮总结
Flutter ListView豆瓣电影排行榜
Flutter Card
Flutter Navigator&Router(导航与路由)
OverscrollNotification不起效果引起的Flutter感悟分享
Flutter 上拉抽屉实现
Flutter 豆瓣客户端,诚心开源
Flutter 更改状态栏颜色