Flutter从入门到奔溃(二):撸一个个人界面

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,
  );

可以看到我们其实可以用的主要是:
1. SliverAppBar (类似于CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout)
2. SliverGridv(类似于RecyClerView或者GrideView)
3. 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使用

页面效果为:
image.png

主要代码为:

 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来实现折叠布局的话,又好像还是挺整洁的了…

白话文时间
  1. 整个视图只有一个根布局,即为CustomScrollView,以下分别实现了SliverAppBar(用于实现个人中心的头部)以及SliverFixedExtentList(用于实现头部下面的item)
  2. SliverAppBar的背景颜色为原谅绿,展开的高度为200.0,而且不是固定的(pinned),是属于可以折叠的,它的item是透明的(实际上位于左上角,但是透明,而且没有点击事件);它的child布局为一个竖直的线性布局(23333):
    1. 第一个布局是一个头像:有头像?显示头像:显示占位图
    2. 第二个布局是一个用户昵称:有昵称?显示昵称:显示’点击去登录‘
  3. SliverFixedExtentList包含了一个list,每个item的高度为50.0,它的委托为SliverChildBuilderDelegate,它的childer为一个Container
    1. Container是左对齐的,它的子元素包裹了一个InkWell,用于提供点击事件
    2. InkWell包含了一个竖直对齐的线性布局(233333),它包含了一个水平的线性布局(23333)Row以及一条分割线
      1. Row包含了左边的item文案,以及右边的箭头
      2. Divider提供了一个高度为1.0的分割线
CustomScrollView总结

整个个人中心使用CustomScrollView实现,但这个只是它的作用之一,作为一个潜力无穷的控件,它还有很多用途值得我们去发掘。

实现思路二,使用ListView多布局:

ListView介绍

相信无论是Android狗还是ios汪,对于listView都是相当熟悉的,我当年还在学校的时候,老师就说过一句话:不要小看适配器,我敢打赌你们以后肯定是要天天和适配器打交道的。,而listview&GridView&RecyclerView…肯定是手比手熟的,这里就不班门弄斧了。

代码实现

其实换汤不换药,写listview的时候老是会对照着想到android的写法,我觉得这种思路其实挺好的,可以对照着加强记忆,比如下面我就把他们拆成了:

  1. oncreateViewHolder+getItemCount
  2. 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的方式:
1. 当item为0的时候,返回个人头像信息
2. 为偶数的时候,返回一个分割线
3. 为奇数的时候,返回真正的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的(也看不出有没有复用),
这里留个疑问,慢慢解答。

猜你喜欢

转载自blog.csdn.net/weixin_28717693/article/details/81811730