[toc]
Flutter从入门到奔溃(二):撸一个个人中心界面
前记
上面我们撸了一个登录界面,因为很简单,而且是属于入门级别。
然后我发现我中毒了...
这种布局写起来挺好玩的...
我开始鄙视xml了...
上一篇遗留问题的答复
上一篇文章吐槽了一下Flutter里面listView的滑动会有点卡顿,然后到了开发群问了下大佬,最后的解决方案是:
打release包体验下什么叫纵享丝滑
具体原因可能是因为平时编译和打release包用的是不同的编译方式,所以会导致不同的效果吧。
具体使用起来感觉比weex更加顺畅!这点很满意。
个人中心界面的实现
效果展示
页面拆解
实现思路一,使用CustomScrollView:
CustomScrollView是一个很强大的控件,强大到我也只会一点点皮毛,最坑的是网上还找不到什么比较全面的资料,文档又是英文的...
而我对他的理解是它可以实现安卓中android.support.design.widget.CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+recyclerView的效果。
CustomScrollView介绍
接下来我们简单介绍下CustomScrollView的用法:
首先看下源码:
/// See also:
///
/// * [SliverList], which is a sliver that displays linear list of children.
/// * [SliverFixedExtentList], which is a more efficient sliver that displays
/// linear list of children that have the same extent along the scroll axis.
/// * [SliverGrid], which is a sliver that displays a 2D array of children.
/// * [SliverPadding], which is a sliver that adds blank space around another
/// sliver.
/// * [SliverAppBar], which is a sliver that displays a header that can expand
/// and float as the scroll view scrolls.
/// * [ScrollNotification] and [NotificationListener], which can be used to watch
/// the scroll position without using a [ScrollController].
class CustomScrollView extends ScrollView {
/// Creates a [ScrollView] that creates custom scroll effects using slivers.
///
/// If the [primary] argument is true, the [controller] must be null.
CustomScrollView({
Key key,
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap: false,
this.slivers: const <Widget>[],
}) : super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
);
可以看到我们其实可以用的主要是:
- SliverAppBar (类似于CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout)
- SliverGridv(类似于RecyClerView或者GrideView)
- SliverFixedExtentList(类似于RecyClerView或者ListView)
我们跑下官方demo代码,效果以及代码如下:
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('Demo'),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
),
),
],
);
}
很简单的可以看出每个Widget的对应属性,Android的同学真的可以理解为:AppBar,RecyclerView来加深认识。
CustomScrollView使用
页面效果为:
主要代码为:
return new CustomScrollView(reverse: false, shrinkWrap: false, slivers: <
Widget>[
new SliverAppBar(
pinned: false,
backgroundColor: Colors.green,
expandedHeight: 200.0,
iconTheme: new IconThemeData(color: Colors.transparent),
flexibleSpace: new InkWell(
onTap: () {
userAvatar == null ? debugPrint('登录') : debugPrint('用户信息');
},
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
userAvatar == null
? new Image.asset(
"images/ic_avatar_default.png",
width: 60.0,
height: 60.0,
)
: new Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: new DecorationImage(
image: new NetworkImage(userAvatar),
fit: BoxFit.cover),
border: new Border.all(
color: Colors.white, width: 2.0)),
),
new Container(
margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
child: new Text(
userName == null ? '点击头像登录' : userName,
style: new TextStyle(color: Colors.white, fontSize: 16.0),
),
)
],
)),
),
new SliverFixedExtentList(
delegate:
new SliverChildBuilderDelegate((BuildContext context, int index) {
String title = titles[index];
return new Container(
alignment: Alignment.centerLeft,
child: new InkWell(
onTap: () {
print("the is the item of $title");
},
child: new Column(
children: <Widget>[
new Padding(
padding:
const EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 15.0),
child: new Row(
children: <Widget>[
new Expanded(
child: new Text(
title,
style: titleTextStyle,
)),
rightArrowIcon
],
),
),
new Divider(
height: 1.0,
)
],
),
));
}, childCount: titles.length),
itemExtent: 50.0),
]);
代码看起来比较冗余...
但是相对起xml来实现折叠布局的话,又好像还是挺整洁的了...
白话文时间
- 整个视图只有一个根布局,即为CustomScrollView,以下分别实现了SliverAppBar(用于实现个人中心的头部)以及SliverFixedExtentList(用于实现头部下面的item)
-
SliverAppBar的背景颜色为原谅绿,展开的高度为200.0,而且不是固定的(pinned),是属于可以折叠的,它的item是透明的(实际上位于左上角,但是透明,而且没有点击事件);它的child布局为一个竖直的线性布局(23333):
- 第一个布局是一个头像:有头像?显示头像:显示占位图
- 第二个布局是一个用户昵称:有昵称?显示昵称:显示’点击去登录‘
-
SliverFixedExtentList包含了一个list,每个item的高度为50.0,它的委托为SliverChildBuilderDelegate,它的childer为一个Container
- Container是左对齐的,它的子元素包裹了一个InkWell,用于提供点击事件
- InkWell包含了一个竖直对齐的线性布局(233333),它包含了一个水平的线性布局(23333)Row以及一条分割线
- Row包含了左边的item文案,以及右边的箭头
- Divider提供了一个高度为1.0的分割线
CustomScrollView总结
整个个人中心使用CustomScrollView实现,但这个只是它的作用之一,作为一个潜力无穷的控件,它还有很多用途值得我们去发掘。
实现思路二,使用ListView多布局:
ListView介绍
相信无论是Android狗还是ios汪,对于listView都是相当熟悉的,我当年还在学校的时候,老师就说过一句话:不要小看适配器,我敢打赌你们以后肯定是要天天和适配器打交道的。,而listview&GridView&RecyclerView...肯定是手比手熟的,这里就不班门弄斧了。
代码实现
其实换汤不换药,写listview的时候老是会对照着想到android的写法,我觉得这种思路其实挺好的,可以对照着加强记忆,比如下面我就把他们拆成了:
- oncreateViewHolder+getItemCount
- onBindViewHolder
oncreateViewHolder+getItemCount(返回itemBuild以及count)
这里主要是返回item的widget以及返回item的count,这一步并不是我们要说的重点
@override
Widget build(BuildContext context) {
// return showCustomScrollView();
// 返回我们构建的listview,记得其中的count是数据源的2倍,至于为啥是两倍,看下文
var listView = new ListView.builder(
itemBuilder: (context, i) => renderRow(context,i),
itemCount: titles.length * 2,
);
return listView;
onBindViewHolder(生成每个item的widget)
这个主要是构建绘制各个item(其实就3个)的itemView
renderRow(context, i) {
final userHeaderHeight = 200.0;
if (i == 0) {
var userHeader = new Container(
height: userHeaderHeight,
color: Colors.green,
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
userAvatar == null
? new Image.asset(
"images/ic_avatar_default.png",
width: 60.0,
)
: new Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: new DecorationImage(
image: new NetworkImage(userAvatar),
fit: BoxFit.cover),
border:
new Border.all(color: Colors.white, width: 2.0)),
),
new Container(
margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
child: new Text(
userName == null ? '点击头像登录' : userName,
style: new TextStyle(color: Colors.white, fontSize: 16.0),
),
)
],
)));
return new GestureDetector(
onTap: () {
Navigator.push(context,
new MaterialPageRoute(builder: (context) => new LoginPage()));
},
child: userHeader,
);
}
--i;
if (i.isOdd) {
return new Divider(
height: 1.0,
);
}
i = i ~/ 2;
String title = titles[i];
var listItemContent = new Padding(
padding: const EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0),
child: new Row(
children: <Widget>[
new Expanded(
child: new Text(
title,
style: titleTextStyle,
)),
rightArrowIcon
],
),
);
return new InkWell(
child: listItemContent,
onTap: () {},
);
}
}
因为item的布局很简单,所以renderRow也是相当简单,总体上跟上面的CustomScrollView布局方式类似,这边就不赘述了。
可能比较注意的是一个多item的方式:
- 当item为0的时候,返回个人头像信息
- 为偶数的时候,返回一个分割线
- 为奇数的时候,返回真正的item条数
这也是为什么itemcount为真实条数2:
一个头部+真实条数+真实条数-1条分割线=真实条数2。
因为今天比较多...而且1.多了,有点头昏,写得不是很仔细,我把整个dart文件贴出来,代码都在里面了:
import 'package:flutter/material.dart';
import 'login/LoginPage.dart';
class MyInfoPage extends StatelessWidget {
static const double IMAGE_ICON_WIDTH = 30.0;
static const double ARROW_ICON_WIDTH = 16.0;
var userAvatar;
var userName;
var titles = ["我的消息", "阅读记录", "我的博客", "我的问答", "我的活动", "我的团队", "邀请好友"];
var imagePaths = [
"images/ic_my_message.png",
"images/ic_my_blog.png",
"images/ic_my_blog.png",
"images/ic_my_question.png",
"images/ic_discover_pos.png",
"images/ic_my_team.png",
"images/ic_my_recommend.png"
];
var titleTextStyle = new TextStyle(fontSize: 16.0);
var rightArrowIcon = new Image.asset(
'images/ic_arrow_right.png',
width: ARROW_ICON_WIDTH,
height: ARROW_ICON_WIDTH,
);
@override
Widget build(BuildContext context) {
// return showCustomScrollView();
var listView = new ListView.builder(
itemBuilder: (context, i) => renderRow(context,i),
itemCount: titles.length * 2,
);
return listView;
// return new CustomScrollView(reverse: false, shrinkWrap: false, slivers: <
// Widget>[
// new SliverAppBar(
// pinned: false,
// backgroundColor: Colors.green,
// expandedHeight: 200.0,
// iconTheme: new IconThemeData(color: Colors.transparent),
// flexibleSpace: new InkWell(
// onTap: () {
// userAvatar == null ? debugPrint('登录') : debugPrint('用户信息');
// },
// child: new Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: <Widget>[
// userAvatar == null
// ? new Image.asset(
// "images/ic_avatar_default.png",
// width: 60.0,
// height: 60.0,
// )
// : new Container(
// width: 60.0,
// height: 60.0,
// decoration: new BoxDecoration(
// shape: BoxShape.circle,
// color: Colors.transparent,
// image: new DecorationImage(
// image: new NetworkImage(userAvatar),
// fit: BoxFit.cover),
// border: new Border.all(
// color: Colors.white, width: 2.0)),
// ),
// new Container(
// margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
// child: new Text(
// userName == null ? '点击头像登录' : userName,
// style: new TextStyle(color: Colors.white, fontSize: 16.0),
// ),
// )
// ],
// )),
// ),
// new SliverFixedExtentList(
// delegate:
// new SliverChildBuilderDelegate((BuildContext context, int index) {
// String title = titles[index];
// return new Container(
// alignment: Alignment.centerLeft,
// child: new InkWell(
// onTap: () {
// print("the is the item of $title");
// },
// child: new Column(
// children: <Widget>[
// new Padding(
// padding:
// const EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 15.0),
// child: new Row(
// children: <Widget>[
// new Expanded(
// child: new Text(
// title,
// style: titleTextStyle,
// )),
// rightArrowIcon
// ],
// ),
// ),
// new Divider(
// height: 1.0,
// )
// ],
// ),
// ));
// }, childCount: titles.length),
// itemExtent: 50.0),
// ]);
}
renderRow(context, i) {
final userHeaderHeight = 200.0;
if (i == 0) {
var userHeader = new Container(
height: userHeaderHeight,
color: Colors.green,
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
userAvatar == null
? new Image.asset(
"images/ic_avatar_default.png",
width: 60.0,
)
: new Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
image: new DecorationImage(
image: new NetworkImage(userAvatar),
fit: BoxFit.cover),
border:
new Border.all(color: Colors.white, width: 2.0)),
),
new Container(
margin: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 0.0),
child: new Text(
userName == null ? '点击头像登录' : userName,
style: new TextStyle(color: Colors.white, fontSize: 16.0),
),
)
],
)));
return new GestureDetector(
onTap: () {
Navigator.push(context,
new MaterialPageRoute(builder: (context) => new LoginPage()));
},
child: userHeader,
);
}
--i;
if (i.isOdd) {
return new Divider(
height: 1.0,
);
}
i = i ~/ 2;
String title = titles[i];
var listItemContent = new Padding(
padding: const EdgeInsets.fromLTRB(10.0, 15.0, 10.0, 15.0),
child: new Row(
children: <Widget>[
new Expanded(
child: new Text(
title,
style: titleTextStyle,
)),
rightArrowIcon
],
),
);
return new InkWell(
child: listItemContent,
onTap: () {},
);
}
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('Demo'),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
//横轴的最大长度
maxCrossAxisExtent: 200.0,
//主轴间隔
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
//横轴间隔
childAspectRatio: 1.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
new SliverFixedExtentList(
itemExtent: 50.0,
delegate:
new SliverChildBuilderDelegate((BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
}, childCount: 10),
),
],
);
}
问题:
这种其实是有复用holder的吗?item多的时候会不会卡....
其实我无聊到刷到了1000多条item(release版本上),基本是不会卡顿,
可能没有研究源码吧,看不出咋复用holder的(也看不出有没有复用),
这里留个疑问,慢慢解答。