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

[toc]

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

gayhub地址

前记

上面我们撸了一个登录界面,因为很简单,而且是属于入门级别。
然后我发现我中毒了...
这种布局写起来挺好玩的...
我开始鄙视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的(也看不出有没有复用),
这里留个疑问,慢慢解答。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,800评论 25 707
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 从Android 5.0...
    Rtia阅读 307,427评论 27 439
  • 两年多前,同学推荐我养发、补血、养生经典食方:黑米八宝粥。每周煮一次,美味又养身!俺生长于甜品世家...
    桔之乡阅读 1,639评论 0 6
  • 时钟 转到两点 大地张开 干涸的嘴 摆脱 暑意 把热浪推向了天 却死在一滴一滴的 夜里
    鸟人_2e7d阅读 151评论 0 1
  • 1808年5月3日的枪杀 百度百科上看完了这幅画的介绍。知道了画作背景是法国大革命时期拿破仑侵略西班牙,西班牙人民...
    薄荷草2016阅读 1,243评论 0 0