一步一步完成Flutter应用开发-掘金App首页

1,415 阅读3分钟

写一个仿掘金app主页的效果,这里面不涉及到网络请求,直接是页面的铺设。

构建页面

想要实现效果首先需要把页面构建出来,然后在研究动画的效果,

tutieshi_640x1343_5s.gif

上半部分

效果:

Simulator Screen Shot - iPhone 11 - 2021-03-15 at 10.33.01.png

把这个抽离成一个组件:home_top.dart文件

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class HomeTopPage extends StatefulWidget {
  HomeTopPage({Key key, this.onSearchPress, this.onTagPress}) : super(key: key);

  final Function onTagPress;
  final Function onSearchPress;

  @override
  _HomeTopPageState createState() => _HomeTopPageState();
}

class _HomeTopPageState extends State<HomeTopPage> {
  String currentString = '推荐';

  tagButton({title = '标签'}) {
    return FlatButton(
        onPressed: () {
          setState(() {
            currentString = title;
          });
        },
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(title,
                style: currentString == title
                    ? TextStyle(fontSize: 20, color: Colors.blue)
                    : TextStyle(fontSize: 18, color: Colors.grey)),
            Offstage(
              offstage: currentString != title,
              child: Container(
                height: 2,
                width: 40,
                decoration: BoxDecoration(color: Colors.blue),
              ),
            )
          ],
        ));
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: Get.context.mediaQueryPadding.top),
      decoration: BoxDecoration(color: Colors.white),
      child: Column(
        children: [
          Container(
            width: Get.width,
            height: 40,
            child: Row(
              children: [
                Expanded(
                    child: InkWell(
                  onTap: () {
                    widget.onSearchPress();
                  },
                  child: Container(
                    margin: EdgeInsets.only(left: 10),
                    height: 36,
                    decoration: BoxDecoration(
                      color: Color.fromRGBO(235, 242, 244, 1),
                      borderRadius: BorderRadius.all(Radius.circular(5)),
                    ),
                    child: Row(
                      children: [
                        Padding(padding: EdgeInsets.symmetric(horizontal: 5)),
                        Icon(
                          Icons.search,
                          size: 18,
                          color: Colors.grey,
                        ),
                        Padding(padding: EdgeInsets.symmetric(horizontal: 5)),
                        Text(
                          '搜索掘金',
                          style: TextStyle(fontSize: 18, color: Colors.grey),
                        )
                      ],
                    ),
                  ),
                )),
                FlatButton(
                    minWidth: 80,
                    onPressed: () {
                      widget.onTagPress();
                    },
                    child: Row(
                      children: [
                        Icon(
                          Icons.settings,
                          size: 18,
                          color: Colors.grey,
                        ),
                        Text('标签',
                            style: TextStyle(fontSize: 18, color: Colors.grey))
                      ],
                    ))
              ],
            ),
          ),
          Container(
            height: 50,
            width: Get.width,
            padding: EdgeInsets.only(top: 15),
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  '关注',
                  '推荐',
                  '热榜',
                  '后端',
                  '前端',
                  'Android',
                  'iOS',
                  '人工智能',
                  '开发工具',
                  '代码人生',
                  '阅读'
                ].map<Widget>((e) => tagButton(title: e)).toList(),
              ),
            ),
          )
        ],
      ),
    );
  }
}

主页面引用 曝漏两个方法onSearchPress,onTagPress, 故名思意,一个是搜索点击方法,一个是标签点击方法。

@override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Color.fromRGBO(235, 242, 244, 1),
        body: Column(
          children: [
            HomeTopPage(
              onSearchPress: () {},
              onTagPress: () {},
            )
          ],
        ));
  }

列表部分

新建home_list.dart文件。这里直接使用之前封装的ListView。传送门

Simulator Screen Shot - iPhone 11 - 2021-03-15 at 13.37.17.png 代码如下:

import 'package:flutter/material.dart';
import 'package:flutter_common_app/widget/common_list.dart';

class HomeList extends StatefulWidget {
  HomeList({Key key}) : super(key: key);

  @override
  _HomeListState createState() => _HomeListState();
}

class _HomeListState extends State<HomeList> {
  List dataList = ['1', '2', '3'];

  itemWidget(isContainUrl) {
    return Container(
      constraints: BoxConstraints(
        minHeight: 80,
      ),
      margin: EdgeInsets.only(top: 1),
      padding: EdgeInsets.symmetric(horizontal: 20),
      decoration: BoxDecoration(color: Colors.white),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(
              child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看',
                style: TextStyle(fontSize: 18),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
              Padding(
                padding: EdgeInsets.only(top: 5),
                child: Text(
                  '点赞89·阅读123·一天清晨',
                  style: TextStyle(fontSize: 14, color: Colors.grey),
                ),
              )
            ],
          )),
          Offstage(
            offstage: isContainUrl,
            child: Container(
              height: 60,
              width: 60,
              decoration: BoxDecoration(color: Colors.red),
            ),
          )
        ],
      ),
    );
  }

  hotWidget() {
    return Container(
      margin: EdgeInsets.only(top: 10),
      child: Column(
        children: [
          Container(
            decoration: BoxDecoration(color: Colors.white),
            padding: EdgeInsets.only(left: 20),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text('🔥 热门推荐',
                    style: TextStyle(fontSize: 14, color: Colors.grey)),
                FlatButton(
                    onPressed: () {},
                    child: Row(
                      children: [
                        Text('文章榜',
                            style: TextStyle(fontSize: 14, color: Colors.grey)),
                        Icon(
                          Icons.keyboard_arrow_right,
                          color: Colors.grey,
                          size: 18,
                        )
                      ],
                    ))
              ],
            ),
          ),
          itemWidget(true),
          itemWidget(true),
          itemWidget(false),
        ],
      ),
    );
  }

  listItemWidget() {
    return Container(
      constraints: BoxConstraints(
        minHeight: 80,
      ),
      margin: EdgeInsets.only(top: 5),
      padding: EdgeInsets.symmetric(horizontal: 20),
      decoration: BoxDecoration(color: Colors.white),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(padding: EdgeInsets.symmetric(vertical: 5)),
          Row(
            children: [
              Icon(
                Icons.sentiment_very_satisfied,
                size: 16,
                color: Colors.grey,
              ),
              Text(
                '  一天清晨',
                style: TextStyle(fontSize: 16),
              ),
            ],
          ),
          Padding(padding: EdgeInsets.symmetric(vertical: 5)),
          Text(
            '有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看有新工具了,赶紧来看看来看看',
            style: TextStyle(fontSize: 18),
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
          Padding(padding: EdgeInsets.symmetric(vertical: 5)),
          Text(
            '有新工具了,赶紧来看看来看看有新工具了',
            style: TextStyle(fontSize: 14, color: Colors.grey),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
          Padding(padding: EdgeInsets.symmetric(vertical: 5)),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return CommonListWiget(
      networkApi: (currentPage) async {
        Future.delayed(Duration(seconds: 2)).then((value) => {
              dataList.addAll(['1', '2'])
            });
        return dataList;
      },
      itemBuilder: (BuildContext context, int position) {
        if (position == 0) {
          return hotWidget();
        }
        return listItemWidget();
      },
    );
  }
}

同样在页面引用一下这个文件

@override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Color.fromRGBO(235, 242, 244, 1),
        body: Column(
          children: [
            HomeTopPage(
              onSearchPress: () {},
              onTagPress: () {},
            ),
            Expanded(child: HomeList()),
          ],
        ));
  }

加入动画

首先改造一下home_list.dart文件,检测一下滚动的高度

AnimationController _controller;
  ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    _scrollController
      ..addListener(() {
        setState(() {
          if (_scrollController.offset > 88) {
          // 把值传出去
            widget.onScroll(255);
          } else if (_scrollController.offset <= 0) {
            widget.onScroll(0);
          } else {
            widget.onScroll(_scrollController.offset * 255 ~/ 88);
          }
        });
      });
  }

接着改造一下home_top.dart文件,让头部随着滚动的状态变化, 定义一个alpha属性接受滑动的传值。


@override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(top: Get.context.mediaQueryPadding.top),
      decoration: BoxDecoration(color: Colors.white),
      child: Stack(
        children: [
          renderSearch(),
          Container(
            height: 40,
            width: Get.width,
            margin: EdgeInsets.only(top: 55 + (0 - 40 * widget.alpha / 255)),
            child: SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  '关注',
                  '推荐',
                  '热榜',
                  '后端',
                  '前端',
                  'Android',
                  'iOS',
                  '人工智能',
                  '开发工具',
                  '代码人生',
                  '阅读'
                ].map<Widget>((e) => tagButton(title: e)).toList(),
              ),
            ),
          )
        ],
      ),
    );
    
    renderSearch() {
    return Positioned(
        top: 0 - 40 * widget.alpha / 255,
        child: Container(
          width: Get.width,
          height: 40,
          child: Row(
            children: [
              Expanded(
                  child: InkWell(
                onTap: () {
                  widget.onSearchPress();
                },
                child: Container(
                  margin: EdgeInsets.only(left: 10),
                  height: 36,
                  decoration: BoxDecoration(
                    color: Color.fromRGBO(235, 242, 244, 1)
                        .withAlpha(255 - widget.alpha),
                    borderRadius: BorderRadius.all(Radius.circular(5)),
                  ),
                  child: Row(
                    children: [
                      Padding(padding: EdgeInsets.symmetric(horizontal: 5)),
                      Icon(
                        Icons.search,
                        size: 18,
                        color: Colors.grey.withAlpha(255 - widget.alpha),
                      ),
                      Padding(padding: EdgeInsets.symmetric(horizontal: 5)),
                      Text(
                        '搜索掘金',
                        style: TextStyle(
                            fontSize: 18,
                            color: Colors.grey.withAlpha(255 - widget.alpha)),
                      )
                    ],
                  ),
                ),
              )),
              FlatButton(
                  minWidth: 80,
                  onPressed: () {
                    widget.onTagPress();
                  },
                  child: Row(
                    children: [
                      Icon(
                        Icons.settings,
                        size: 18,
                        color: Colors.grey.withAlpha(255 - widget.alpha),
                      ),
                      Text('标签',
                          style: TextStyle(
                              fontSize: 18,
                              color: Colors.grey.withAlpha(255 - widget.alpha)))
                    ],
                  ))
            ],
          ),
        ));
  }

主页面调用

@override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Color.fromRGBO(235, 242, 244, 1),
        body: Column(
          children: [
            HomeTopPage(
              onSearchPress: () {},
              onTagPress: () {},
              alpha: _alpha,
            ),
            Expanded(child: HomeList(onScroll: (value) {
              setState(() {
                _alpha = value;
              });
            })),
          ],
        ));
  }

就ok了, 欢迎同学们来指导讨论。懂得分享才能共同进步。