Flutter-教你一步步还原豆瓣 03Home主页制作

Home 主页制作

源码地址: https://github.com/mumushuiding/douban

1、基本框架

添加 lib\model\subject.dart

class Subject{
  
}

添加 lib/widgets/search_text_field_widget.dart

import 'package:flutter/material.dart';

class SearchTextFieldWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    
    return Center(
      child: Text('搜索栏'),
    );
  }
  
}

修改 lib\home\home\home_page.dart

import '../../widgets/search_text_field_widget.dart';
import 'package:flutter/material.dart';
import '../model/subject.dart';
class HomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    print('build HomePage');
    return getWidget();
  }
}
var _tabs = ['动态', '推荐'];
// 返回默认的Tab控制器
DefaultTabController getWidget() {
  return DefaultTabController(
    initialIndex: 1,
    length: _tabs.length,
    child: NestedScrollView(
      headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled){
        return <Widget>[
          SliverOverlapAbsorber(
            handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
            // SliverAppBar 待完善
            child: SliverAppBar(
                // 固定在顶部
                pinned: true,
                snap: true,
                floating: true,
                expandedHeight: 100.0,
                forceElevated: false,
                titleSpacing: NavigationToolbar.kMiddleSpacing,
                backgroundColor: Colors.green,
                flexibleSpace: new FlexibleSpaceBar(  
                  collapseMode: CollapseMode.pin,
                  background: Container(
                  color: Colors.green,
                  child: SearchTextFieldWidget(
                    hintText: '影视作品中你难忘的离别',
                    margin: const EdgeInsets.only(left: 15.0, right: 15.0),
                    onTab: () {
                      Router.push(context, Router.searchPage, '影视作品中你难忘的离别');
                    },
                  ),
                  alignment: Alignment(0.0, 0.0),
                  ),
                ),
                bottom: TabBar(
                  isScrollable: true,
                  tabs: _tabs
                    .map((String name) => Container(
                          child: Text(
                            name,
                          ),
                          padding: const EdgeInsets.only(bottom: 5.0),
                        ))
                    .toList(),
                )
            )
          )
        ];
      },
      body: TabBarView(
        // tab下的内容
        children: _tabs.map((String name) {
          return SliverContainer(
            name: name,
          );
        }).toList(),
      )
    ),
  );
}
class SliverContainer extends StatefulWidget{
  final String name;
  SliverContainer({Key key, @required this.name}) : super(key:key);
  @override
  _SliverContainerState createState() => _SliverContainerState();
}

class _SliverContainerState extends State<SliverContainer>{

  @override
  void initState() {
    super.initState();
    print('init state${widget.name}');
    ///请求动态数据
    if (list == null || list.isEmpty) {
      if (_tabs[0] == widget.name) {
        requestAPI();
      } else {
        ///请求推荐数据
        requestAPI();
      }
    }
  }

  // 添加 subject
  List<Subject> list;

  void requestAPI() {
    print("请求动态数据");
  }

  @override
  Widget build(BuildContext context) {
    
    return getContentSliver(context, list);
  }
  getContentSliver(BuildContext context, List<Subject> list) {
    if (widget.name == _tabs[0]) {
      return _loginContainer(context);
    }

    print('getContentSliver');

    if (list == null || list.length == 0) {
      return Center(
        child: Text('暂无数据')
      );
    }
    // 返回SafeArea
    return Center(
        child: Text('返回SafeArea')
      );
  }
}
_loginContainer(BuildContext context){
return Text('_loginContainer');
}

运行 flutter run

最终结果:

 

2.完善AppBar搜索栏

添加 lib\router.dart

import 'package:flutter/material.dart';
class Router {
  static const searchPage = 'app://SearchPage';
  Router.push(BuildContext context, String url,dynamic params) {
    print('router push');
  }
}

修改 lib/widgets/search_text_field_widget.dart

import 'package:flutter/material.dart';

class SearchTextFieldWidget extends StatelessWidget{
  final  ValueChanged<String> onSubmitted;
  final VoidCallback onTab;
  final String hintText;
  final EdgeInsetsGeometry margin;
  SearchTextFieldWidget({Key key, this.hintText,this.onSubmitted, this.onTab, this.margin}): super(key:key);

  @override
  Widget build(BuildContext context) {
    
    return Container(
      margin: margin == null ? EdgeInsets.all(0.0):margin,
      width: MediaQuery.of(context).size.width,
      alignment: AlignmentDirectional.center,
      height: 37.0,
      decoration: BoxDecoration(
        color: Color.fromARGB(255, 237, 236, 237),
        borderRadius: BorderRadius.circular(24.0),
      ),
      child: TextField(
        onSubmitted: onSubmitted,
        onTap: onTab,
        cursorColor: Color.fromARGB(255, 0, 189, 96),
        style: TextStyle(fontSize: 17),
        decoration: InputDecoration(
          contentPadding: const EdgeInsets.all(8.0),
          border: InputBorder.none,
          hintText: hintText,
          hintStyle: TextStyle(
            fontSize: 17, color: Color.fromARGB(255, 192, 191, 191)
          ),
          prefixIcon: Icon(
            Icons.search,
            size: 25,
            color: Color.fromARGB(255, 128, 128, 128),
          )
        ),
      ),
    );
  }
  
}

修改 lib\main.dart

 
// 省略...

SearchTextFieldWidget(
                      hintText: '影视作品中你难忘的离别',
                      margin: const EdgeInsets.only(left: 15.0, right: 15.0),
                      onTab: () {
                        Router.push(context, Router.searchPage, '影视作品中你难忘的离别');
                      },
                    ),

// 省略...

运行 flutter run

最后效果图:

 

3.添加请求动态数据

1、复制lib\mock数据到你的项目, 修改 pubspec.yaml

assets:
    - assets/images/
    - mock/

2、新建 lib\http\mock_request.dart

// 模拟数据
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'API.dart';
import 'dart:convert';
class MockRequest{
  Future<dynamic> get(String action,{Map params}) async {
    return MockRequest.mock(action: getJsonName(action),params: params);
  }
  static Future<dynamic> post({String action, Map params}) async {
  return MockRequest.mock(action: action, params: params);
}
  static Future<dynamic> mock({String action,Map params}) async {
    var responseStr = await rootBundle.loadString('mock/$action.json');
    var responseJson = json.decode(responseStr);
    return responseJson;
  }
  Map<String, String> map = {
      API.IN_THEATERS: 'in_theaters',
      API.COMING_SOON: 'coming_soon',
      API.TOP_250: 'top250',
      API.WEEKLY: 'weekly',
      API.REIVIEWS: 'reviews',
    };

    getJsonName(String action) {
      return map[action];
    }
}

3、新建 lib\http\API.dart

class API{
  static const BASE_URL = 'https://api.douban.com';

  ///TOP250
  static const String TOP_250 = '/v2/movie/top250';

  ///正在热映
  static const String IN_THEATERS = '/v2/movie/in_theaters?apikey=0b2bdeda43b5688921839c8ecb20399b';

  ///即将上映
  static const String COMING_SOON = '/v2/movie/coming_soon?apikey=0b2bdeda43b5688921839c8ecb20399b';

  ///一周口碑榜
  static const String WEEKLY = '/v2/movie/weekly?apikey=0b2bdeda43b5688921839c8ecb20399b';

  ///影人条目信息
  static const String CELEBRITY = '/v2/movie/celebrity/';

  static const String REIVIEWS = '/v2/movie/subject/26266893/reviews';
}

4、修改 lib\model\subject.dart

class Subject {
  bool tag = false;
  Rating rating;
  var genres;
  var title;
  List<Cast> casts;
  var durations;
  var collect_count;
  var mainland_pubdate;
  var has_video;
  var original_title;
  var subtype;
  var directors;
  var pubdates;
  var year;
  Images images;
  var alt;
  var id;

  ///构造函数
  Subject.fromMap(Map<String, dynamic> map) {
    var rating = map['rating'];
    this.rating = Rating(rating['average'], rating['max']);
    genres = map['genres'];
    title = map['title'];
    var castMap = map['casts'];
    casts = _converCasts(castMap);
    collect_count = map['collect_count'];
    original_title = map['original_title'];
    subtype = map['subtype'];
    directors = map['directors'];
    year = map['year'];
    var img = map['images'];
    images = Images(img['small'], img['large'], img['medium']);
    alt = map['alt'];
    id = map['id'];
    durations = map['durations'];
    mainland_pubdate = map['mainland_pubdate'];
    has_video = map['has_video'];
    pubdates = map['pubdates'];
  }

  _converCasts(casts) {
    return casts.map<Cast>((item)=>Cast.fromMap(item)).toList();
  }

}
class Images {
  var small;
  var large;
  var medium;

  Images(this.small, this.large, this.medium);
}

class Rating {
  var average;
  var max;
  Rating(this.average, this.max);
}



class Cast {
  var id;
  var name_en;
  var name;
  Avatar avatars;
  var alt;
  Cast(this.avatars, this.name_en, this.name, this.alt, this.id);

  Cast.fromMap(Map<String, dynamic> map) {
    id = map['id'];
    name_en = map['name_en'];
    name = map['name'];
    alt = map['alt'];
    var tmp = map['avatars'];
    if(tmp == null){
      avatars = null;
    }else{
      avatars = Avatar(tmp['small'], tmp['large'], tmp['medium']);
    }

  }
}

class Avatar {
  var medium;
  var large;
  var small;
  Avatar(this.small, this.large, this.medium);
}

动态数据展示

修改 lib\home\home_page.dart

import '../../http/mock_request.dart';
import '../../router.dart';
import '../../widgets/search_text_field_widget.dart';
import '../../http/API.dart';
import 'package:flutter/material.dart';
import '../../model/subject.dart';
import '../../constant/constant.dart';

// 省略...

class _SliverContainerState extends State<SliverContainer>{

  @override
  void initState() {
    super.initState();
    print('init state${widget.name}');
    ///请求动态数据
    if (list == null || list.isEmpty) {
      if (_tabs[0] == widget.name) {
        requestAPI();
      } else {
        ///请求推荐数据
        requestAPI();
      }
    }
  }

  // 添加 subject
  List<Subject> list;

  void requestAPI() async{
    print("请求动态数据");
    var _request = MockRequest();
    var result = await _request.get(API.TOP_250);
    var resultList = result['subjects'];
    list = resultList.map<Subject>((item) => Subject.fromMap(item)).toList();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    
    return getContentSliver(context, list);
  }
  getContentSliver(BuildContext context, List<Subject> list) {
    if (widget.name == _tabs[0]) {
      return _loginContainer(context);
    }

    print('getContentSliver');

    if (list == null || list.length == 0) {
      return Center(
        child: Text('暂无数据')
      );
    }
    // 返回SafeArea
    return SafeArea(
      top: false,
      bottom: false,
      child: Builder(
        builder: (BuildContext context){
          return CustomScrollView(
            physics: const BouncingScrollPhysics(),
            // The "controller" and "primary" members should be left
            // unset, so that the NestedScrollView can control this
            // inner scroll view.
            // If the "controller" property is set, then this scroll
            // view will not be associated with the NestedScrollView.
            // The PageStorageKey should be unique to this ScrollView;
            // it allows the list to remember its scroll position when
            // the tab view is not on the screen.
            key: PageStorageKey<String>(widget.name),
            slivers: <Widget>[
              SliverOverlapInjector(
                // This is the flip side of the SliverOverlapAbsorber above.
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
              ),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    return getCommonItem(list, index);
                  },
                  childCount: list.length
                ),
              )
            ],
          );
        },
      ),
    );
  }
  double singleLineImgHeight = 180.0;
  double contentVideoHeight = 350.0;
  ///列表的普通单个item
   getCommonItem(List<Subject> items, int index){
     Subject item = items[index];
     bool showVideo = index == 1 || index == 3;
    return Container(
      height: showVideo ? contentVideoHeight : singleLineImgHeight,
      color: Colors.white,
      margin: const EdgeInsets.only(bottom: 10.0),
      padding: const EdgeInsets.only(
          left: Constant.MARGIN_LEFT,
          right: Constant.MARGIN_RIGHT,
          top: Constant.MARGIN_RIGHT,
          bottom: 10.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Row(
            children: <Widget>[
              CircleAvatar(
                radius: 25.0,
                backgroundImage: NetworkImage(item.casts[0].avatars.medium),
                backgroundColor: Colors.white,
              ),
              Padding(
                padding: const EdgeInsets.only(left: 10.0),
                child: Text(item.title),
              ),
              Expanded(
                child: Align(
                  child: Icon(
                    Icons.more_horiz,
                    color: Colors.grey,
                    size: 18.0,
                  ),
                  alignment: Alignment.centerRight,
                ),
              )
            ]
          ),
          // 填充多余空间
          Expanded(
            child: Container(
              child: showVideo ? getContentVideo(index) : getItemCenterImg(item),
            )
          ),
          // 底下评论点赞标志
          Padding(
            padding: EdgeInsets.only(left: 15.0, right: 15.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Image.asset(
                  Constant.ASSETS_IMG + 'ic_vote.png',
                  width: 25.0,
                  height: 25.0,
                ),
                Image.asset(
                  Constant.ASSETS_IMG +
                      'ic_notification_tv_calendar_comments.png',
                  width: 20.0,
                  height: 20.0,
                ),
                Image.asset(
                  Constant.ASSETS_IMG + 'ic_status_detail_reshare_icon.png',
                  width: 25.0,
                  height: 25.0,
                ),
              ],
            ),
          ),
          
        ]
      ),
    );
   }

   getContentVideo(int index) {
     return Container(
       child: Text('视频'),
     );
   }
   getItemCenterImg(Subject item){
     return Container(
       child: Text('图片'),
     );
   }
}


// 省略...

最后效果:

 

完善图片显示

1、新建 lib\widgets\image\radius_img.dart

import 'package:flutter/material.dart';

class RadiusImg {
  static Widget get(String imgUrl, double imgW, {double imgH, Color shadowColor, double elevation, double radius = 6.0, RoundedRectangleBorder shape}) {
    if (shadowColor == null) {
      shadowColor = Colors.transparent;
    }
    return Card(
      //影音海报
      shape:shape?? RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(radius)),
      ),
      color: shadowColor,
      clipBehavior: Clip.antiAlias,
      elevation: elevation == null ? 0.0 : 5.0,
      child: imgW == null ? Image.network(
        imgUrl,
        height: imgH,
        fit:  BoxFit.cover,
      ):Image.network(
        imgUrl,
        width: imgW,
        height: imgH,
        fit: imgH == null ? BoxFit.contain : BoxFit.cover,
      ),
    );
  }
}

2、修改 home_page.dart 中函数 getItemCenterImg(Subject item){}:

import '../../widgets/image/radius_img.dart';

getItemCenterImg(Subject item){
     return Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: <Widget>[
         Expanded(
           child: RadiusImg.get(
             item.images.large, null,
             shape: RoundedRectangleBorder(
               borderRadius:BorderRadius.only(
                 topLeft: Radius.circular(5.0),
                 bottomLeft: Radius.circular(5.0)
               ),
             )
           ),
         ),
         Expanded(
           child: RadiusImg.get(item.casts[1].avatars.medium, null, radius: 0.0),
         ),
         Expanded(
           child: RadiusImg.get(item.casts[2].avatars.medium, null,
              shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                      topRight: Radius.circular(5.0),
                      bottomRight: Radius.circular(5.0)))),
         ),
       ]
     );
   }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值