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