实现影院热映和即将上映的页面
定义网络框架获取网络数据
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:douban_flutter/model/movie_news.dart';
import 'package:html/dom.dart' as dom;
//show关键字表示只引用一点
import 'package:html/parser.dart' show parse;
//as指定固定前缀
import 'package:http/http.dart' as http;
class ApiClient {
static const String baseUrl = 'http://api.douban.com/v2/movie/';
static const String apiKey = '0b2bdeda43b5688921839c8ecb20399b';
static const String webUrl = 'https://movie.douban.com/';
var dio = ApiClient.createDio();
///从豆瓣网页上获取需要的轮播图片
Future<List<MovieNews>> getNewsList() async{
//获取的轮播图信息列表
List<MovieNews> news = [];
await http.get(webUrl).then((http.Response response){
var document = parse(response.body.toString());
// 获取所有指定类名的元素gallery-frame获取轮播图数据
List<dom.Element> items = document.getElementsByClassName('gallery-frame');
items.forEach((item) {
String cover = item.getElementsByTagName('img')[0].attributes['src'].toString();//根据网页<img/>便签获取图片
String link = item.getElementsByTagName('a')[0].attributes['href'].toString();//根据网页<a/>标签获取链接
String title = item.getElementsByTagName('h3')[0].text.toString().trim();//根据网页<h3/>标签获取标题
String summary =item.getElementsByTagName('p')[0].text.toString().trim();//根据网页<p/>标签获取段落
MovieNews movieNews = new MovieNews(title, cover, summary, link);
news.add(movieNews);
});
});
return news;
}
/// 获取影院热映电影
Future<dynamic> getNowPlayingList({int start, int count}) async {
Response<Map> response = await dio.get('in_theaters', queryParameters: {"start":start, 'count':count});
return response.data['subjects'];
}
/// 获取即将上映电影
Future<dynamic> getComingList({int start, int count}) async {
Response<Map> response = await dio.get('coming_soon', queryParameters: {"start":start, 'count':count});
return response.data['subjects'];
}
///配置请求参数
static Dio createDio() {
var options = BaseOptions(
// 请求路径,如果 `path` 以 "http(s)"开始, 则 `baseURL` 会被忽略; 否则,
//将会和baseUrl拼接出完整的的url.
baseUrl: baseUrl,
// 连接服务器超时时间,单位是毫秒.
connectTimeout: 10000,
//响应流上前后两次接受到数据的间隔,单位为毫秒。如果两次间隔超过[receiveTimeout],
receiveTimeout: 10000,
// 请求的Content-Type,默认值是[ContentType.JSON].
//如果您想以"application/x-www-form-urlencoded"格式编码请求数据,
//可以设置此选项为 `ContentType.parse("application/x-www-form-urlencoded")`, 这样[Dio]
//就会自动编码请求体.
contentType: ContentType.json,
//添加固定参数
queryParameters: {
"apikey":apiKey
}
);
return Dio(options);
}
}
构建实体存储电影列表数据
import 'MovieActor.dart';
import 'MovieImage.dart';
import 'MovieRate.dart';
/// 电影简介 item
class MovieItem {
List genres;
MovieRate rating;
String title;
String year;
MovieImage images;
String id;
String mainlandPubdate;
int collectCount;
List<MovieActor> casts;
List<MovieActor> directors;
MovieItem(
this.genres,
this.title,
this.year,
this.images,
this.id,
this.rating,
this.mainlandPubdate,
this.collectCount,
this.casts,
this.directors,
);
MovieItem.fromJson(Map data) {
id = data['id'];
images = MovieImage.fromJson(data['images']);
year = data['year'];
title = data['title'];
genres = data['genres']?.cast<String>()?.toList();
rating =MovieRate.fromJson(data['rating']);
mainlandPubdate = data['mainland_pubdate'];
collectCount =data['collect_count'];
List<MovieActor> castsData = [];
List<MovieActor> directorsData = [];
for (var i = 0; i < data['casts'].length; i++) {
castsData.add(MovieActor.fromJson(data['casts'][i]));
}
for (var i = 0; i < data['directors'].length; i++) {
directorsData.add(MovieActor.fromJson(data['directors'][i]));
}
casts = castsData;
directors = directorsData;
}
}
使用工具类将数据变成实体类
import 'package:douban_flutter/model/MovieItem.dart';
import 'package:douban_flutter/model/MoviePhoto.dart';
class MovieDataUtil {
static List<MovieItem> getMovieList(var list) {
List content = list;
List<MovieItem> movies = [];
content.forEach((data) {
movies.add(MovieItem.fromJson(data));
});
return movies;
}
static List<MoviePhoto> getPhotoList(var list) {
List content = list;
List<MoviePhoto> photos = [];
content.forEach((data) {
photos.add(MoviePhoto.fromJson(data));
});
return photos;
}
}
构建界面UI
顶部小部件
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class HomeSectionView extends StatelessWidget {
final String title;
final String action;
HomeSectionView(this.title, this.action);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
padding: EdgeInsets.fromLTRB(15, 15, 15, 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'$title',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 5,),
Container(
width: 80,
height: 2,
color: Colors.black,
)
],
),
GestureDetector(
onTap: () {
if (action == 'search') {
//AppNavigator.push(context, MovieClassifyListView());
} else {
//AppNavigator.pushMovieList(context, title, action);
}
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('全部',style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),),
SizedBox(width: 3,),
Icon(CupertinoIcons.forward, size: 14,),
],
)
)
],
)
);
}
}
显示电影item的布局
圆角背景图片
import 'package:flutter/material.dart';
class MovieCoverImage extends StatelessWidget {
final String imgUrl;
final double width;
final double height;
MovieCoverImage(this.imgUrl, {this.width, this.height});
@override
Widget build(BuildContext context) {
return Container(
child: ClipRRect(
child: Image(
//image: CachedNetworkImageProvider(imgUrl),
image: NetworkImage(imgUrl),
fit: BoxFit.cover,
width: width,
height: height,
),
borderRadius: BorderRadius.circular(3.0),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20.0)),
);
}
}
五星好评控件
import 'package:flutter/widgets.dart';
import 'dart:math' as Math;
const double kMaxRate = 5.0;
const int kNumberOfStarts = 5;
const double kSpacing = 3.0;
const double kSize = 50.0;
//五星好评控件
class StaticRatingBar extends StatelessWidget {
/// number of stars
final int count;
/// init rate
final double rate;
/// size of the starts
final double size;
final Color colorLight;
final Color colorDark;
StaticRatingBar({
double rate,
Color colorLight,
Color colorDark,
int count,
this.size: kSize,
}) : rate = rate ?? kMaxRate,
count = count ?? kNumberOfStarts,
colorDark = colorDark ?? new Color(0xffd5d5d5),
colorLight = colorLight ?? new Color(0xffFF962E);
Widget buildStar() {
return new SizedBox(
width: size * count,
height: size,
child: new CustomPaint(
painter: new _PainterStars(
size: this.size / 2,
color: colorLight,
style: PaintingStyle.fill,
strokeWidth: 0.0),
));
}
Widget buildHollowStar() {
return new SizedBox(
width: size * count,
height: size,
child: new CustomPaint(
painter: new _PainterStars(
size: this.size / 2,
color: colorDark,
style: PaintingStyle.fill,
strokeWidth: 0.0),
));
}
@override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
buildHollowStar(),
new ClipRect(
clipper: new _RatingBarClipper(width: rate * size),
child: buildStar(),
)
],
);
}
}
class _RatingBarClipper extends CustomClipper<Rect> {
final double width;
_RatingBarClipper({this.width}) : assert(width != null);
@override
Rect getClip(Size size) {
return new Rect.fromLTRB(0.0, 0.0, width, size.height);
}
@override
bool shouldReclip(_RatingBarClipper oldClipper) {
return width != oldClipper.width;
}
}
class _PainterStars extends CustomPainter {
final double size;
final Color color;
final PaintingStyle style;
final double strokeWidth;
_PainterStars({this.size, this.color, this.strokeWidth, this.style});
/// 角度转弧度公式
double degree2Radian(int degree) {
return (Math.pi * degree / 180);
}
Path createStarPath(double radius, Path path) {
double radian = degree2Radian(36); // 36为五角星的角度
double radiusIn = (radius * Math.sin(radian / 2) / Math.cos(radian)) *
1.1; // 中间五边形的半径,太正不是很好看,扩大一点点
path.moveTo((radius * Math.cos(radian / 2)), 0.0); // 此点为多边形的起点
path.lineTo((radius * Math.cos(radian / 2) + radiusIn * Math.sin(radian)),
(radius - radius * Math.sin(radian / 2)));
path.lineTo((radius * Math.cos(radian / 2) * 2),
(radius - radius * Math.sin(radian / 2)));
path.lineTo(
(radius * Math.cos(radian / 2) + radiusIn * Math.cos(radian / 2)),
(radius + radiusIn * Math.sin(radian / 2)));
path.lineTo((radius * Math.cos(radian / 2) + radius * Math.sin(radian)),
(radius + radius * Math.cos(radian)));
path.lineTo((radius * Math.cos(radian / 2)), (radius + radiusIn));
path.lineTo((radius * Math.cos(radian / 2) - radius * Math.sin(radian)),
(radius + radius * Math.cos(radian)));
path.lineTo(
(radius * Math.cos(radian / 2) - radiusIn * Math.cos(radian / 2)),
(radius + radiusIn * Math.sin(radian / 2)));
path.lineTo(0.0, (radius - radius * Math.sin(radian / 2)));
path.lineTo((radius * Math.cos(radian / 2) - radiusIn * Math.sin(radian)),
(radius - radius * Math.sin(radian / 2)));
path.lineTo((radius * Math.cos(radian / 2)), 0.0);
return path;
}
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint();
// paint.color = Colors.redAccent;
paint.strokeWidth = strokeWidth;
paint.color = color;
paint.style = style;
Path path = new Path();
double offset = strokeWidth > 0 ? strokeWidth + 2 : 0.0;
path = createStarPath(this.size - offset, path);
path = path.shift(new Offset(this.size * 2, 0.0));
path = createStarPath(this.size - offset, path);
path = path.shift(new Offset(this.size * 2, 0.0));
path = createStarPath(this.size - offset, path);
path = path.shift(new Offset(this.size * 2, 0.0));
path = createStarPath(this.size - offset, path);
path = path.shift(new Offset(this.size * 2, 0.0));
path = createStarPath(this.size - offset, path);
if (offset > 0) {
path = path.shift(new Offset(offset, offset));
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(_PainterStars oldDelegate) {
return oldDelegate.size != this.size;
}
}
item整体显示界面
每行三个电影item
import 'package:douban_flutter/model/MovieItem.dart';
import 'package:douban_flutter/util/app_color.dart';
import 'package:douban_flutter/util/screen.dart';
import 'package:douban_flutter/widget/MovieCoverImage.dart';
import 'package:douban_flutter/widget/StaticRatingBar.dart';
import 'package:flutter/material.dart';
//影院热映
class HomeMovieCoverView extends StatelessWidget {
final MovieItem movie;
HomeMovieCoverView(this.movie);
@override
Widget build(BuildContext context) {
// 单个电影的宽度
// 一行放置 3 个 电影
var width = (Screen.width - 15 * 4) / 3;
return GestureDetector(
onTap: () {
//AppNavigator.pushMovieDetail(context, movie);
},
child: Container(
width: width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
MovieCoverImage(movie.images.small, width: width, height: width / 0.75,),
SizedBox(height: 5,),
Text(
movie.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
maxLines: 1,
),
SizedBox(height: 3,),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new StaticRatingBar(size: 13.0,rate: movie.rating.average/2,),
SizedBox(width: 5,),
Text(movie.rating.average.toString(),style: TextStyle(color: AppColor.grey, fontSize: 12.0),)
],
),
],
),
),
);
}
}
整合影院热映和即将上映页面
使用Wrap组件使内部组件进行换行排列
import 'package:douban_flutter/model/MovieItem.dart';
import 'package:flutter/material.dart';
import 'HomeMovieCoverView.dart';
import 'HomeSectionView.dart';
import 'MovieComingView.dart';
class MovieThreeGridView extends StatelessWidget {
final List<MovieItem> movies;
final String title;
final String action;
MovieThreeGridView(this.movies, this.title, this.action);
@override
Widget build(BuildContext context) {
var children;
switch (title) {
case '影院热映':
print(movies.length);
children = movies.map((movie) =>
HomeMovieCoverView(movie)).toList();
break;
case '即将上映':
children = movies.map((movie) =>
MovieComingView(movie)).toList();
break;
default:
break;
}
return Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HomeSectionView(title,action),
Container(
padding: EdgeInsets.fromLTRB(15, 10, 0, 10),
/**
* Wrap按宽高自动换行布局
* spacing主轴方向的间距 水平方向的间距
* runSpacing run的间距 垂直方向的间距
*/
child: Wrap(spacing: 15, runSpacing: 20, children: children,),
),
Container(
height: 10,
color: Color(0xFFF5F5F5),
)
],
),
);
}
}
主页将获取到的数据渲染到组件上
import 'package:douban_flutter/model/movie_news.dart';
import 'package:douban_flutter/net/api_client.dart';
import 'package:douban_flutter/util/MovieDataUtil.dart';
import 'package:douban_flutter/util/app_color.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'MovieThreeGridView.dart';
import 'home_news_banner_view.dart';
class HomePage extends StatefulWidget{
@override
_HomeState createState() =>_HomeState();
}
/**
* AutomaticKeepAliveClientMixin需要用到页面保持状态,使他不销毁不重绘
*/
class _HomeState extends State<HomePage> with AutomaticKeepAliveClientMixin{
var newsList;
var nowPlayingList, comingList;
@override
void initState() {
// TODO: implement initState
super.initState();
fetchData();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
if (nowPlayingList == null) {
return new Center(
//ios菊花进度条 等数据加载结束后消失
child: new CupertinoActivityIndicator(
),
);
} else {
return Container(
//下拉刷新组件
child: RefreshIndicator(
color: AppColor.red,
onRefresh: fetchData,
child: ListView(
//表示是否将列表项包裹在AutomaticKeepAlive中
addAutomaticKeepAlives: true,
// 防止 children 被重绘,设置预加载的区域
cacheExtent: 10000,
children: <Widget>[
new NewsBannerView(newsList),
new MovieThreeGridView(nowPlayingList, '影院热映', 'in_theaters'),
new MovieThreeGridView(comingList, '即将上映', 'coming_soon'),
],
),
)
);
}
}
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
// 加载数据
Future<void> fetchData() async {
ApiClient client = new ApiClient();
List<MovieNews> news = await client.getNewsList();
var nowPlayingData = await client.getNowPlayingList(start: 0, count: 6);
var comingListData = await client.getComingList(start: 0, count: 6);
setState(() {
newsList =news2Banner(news);
comingList = MovieDataUtil.getMovieList(comingListData);
nowPlayingList = MovieDataUtil.getMovieList(nowPlayingData);
});
}
/**
* 构建新的banner实体
*/
List<NewsBanner> news2Banner(var list) {
List content = list;
List<NewsBanner> banners = [];
content.forEach((data) {
banners.add(new NewsBanner(data));
});
return banners;
}
}