From 5ce1375c29e4287dfcc8267b7170c6183fb59d43 Mon Sep 17 00:00:00 2001 From: Kun Chen Date: Thu, 21 Oct 2021 18:28:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=87=86=E5=A4=87=E5=86=99=E5=8D=B3?= =?UTF-8?q?=E5=B0=86=E4=B8=8A=E6=98=A0=E7=9A=84=E5=8D=A1=E7=89=87=EF=BC=8C?= =?UTF-8?q?=E8=AF=84=E5=88=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- douban_app/lib/assets/font/CusIcons.dart | 6 -- .../lib/components/star_rate/index.dart | 19 +++++ douban_app/lib/components/top_tabs/index.dart | 1 - .../lib/models/book_av/movie_model.dart | 51 ++++++++++++ douban_app/lib/views/book/index.dart | 12 ++- .../views/book/tab_pages/movie/ad_area.dart | 20 +++++ .../movie/coming_soon/coming_soon_card.dart | 43 ++++++++++ .../tab_pages/movie/coming_soon/index.dart | 51 ++++++++++++ .../lib/views/book/tab_pages/movie/index.dart | 23 ++++-- .../book/tab_pages/movie/popular/index.dart | 80 +++++++++++++++++++ .../book/tab_pages/movie/popular/tabs.dart | 78 ++++++++++++++++++ .../tab_pages/movie/popular/video_card.dart | 51 ++++++++++++ .../tab_pages/movie/popular/video_list.dart | 33 ++++++++ .../book/tab_pages/movie/top_banner.dart | 13 ++- 14 files changed, 462 insertions(+), 19 deletions(-) delete mode 100644 douban_app/lib/assets/font/CusIcons.dart create mode 100644 douban_app/lib/components/star_rate/index.dart delete mode 100644 douban_app/lib/components/top_tabs/index.dart create mode 100644 douban_app/lib/models/book_av/movie_model.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/ad_area.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/coming_soon/coming_soon_card.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/coming_soon/index.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/popular/index.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/popular/tabs.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/popular/video_card.dart create mode 100644 douban_app/lib/views/book/tab_pages/movie/popular/video_list.dart diff --git a/douban_app/lib/assets/font/CusIcons.dart b/douban_app/lib/assets/font/CusIcons.dart deleted file mode 100644 index a8fad98..0000000 --- a/douban_app/lib/assets/font/CusIcons.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -class CusIcons { - static const IconData movie = - const IconData(0xe6bb, fontFamily: "cusIcon", matchTextDirection: true); -} diff --git a/douban_app/lib/components/star_rate/index.dart b/douban_app/lib/components/star_rate/index.dart new file mode 100644 index 0000000..b625ad1 --- /dev/null +++ b/douban_app/lib/components/star_rate/index.dart @@ -0,0 +1,19 @@ +import 'package:flutter/cupertino.dart'; + +/// 五角星评级组件 +class StartRate extends StatefulWidget { + final double rate; + StartRate({Key key, this.rate}) : super(key: key); + + @override + _StartRateState createState() => _StartRateState(); +} + +class _StartRateState extends State { + @override + Widget build(BuildContext context) { + return Container( + child: Text("TODO:评分:${widget.rate}"), + ); + } +} diff --git a/douban_app/lib/components/top_tabs/index.dart b/douban_app/lib/components/top_tabs/index.dart deleted file mode 100644 index 8b13789..0000000 --- a/douban_app/lib/components/top_tabs/index.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/douban_app/lib/models/book_av/movie_model.dart b/douban_app/lib/models/book_av/movie_model.dart new file mode 100644 index 0000000..55dde23 --- /dev/null +++ b/douban_app/lib/models/book_av/movie_model.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; + +class VideoCardModel { + /// 是否已经加入想看 + bool isInterested; + + /// 电影封面地址 + String imgUrl; + + /// 电影名称 + String name; + + /// 电影评分 + double rate; + VideoCardModel({ + @required this.name, + this.isInterested = false, + this.imgUrl = "", + this.rate = 0.0, + }); + + Map toMap() { + return { + 'isInterested': isInterested, + 'imgUrl': imgUrl, + 'name': name, + 'rate': rate, + }; + } + + factory VideoCardModel.fromMap(Map map) { + return VideoCardModel( + isInterested: map['isInterested'], + imgUrl: map['imgUrl'], + name: map['name'], + rate: map['rate'], + ); + } + + String toJson() => json.encode(toMap()); + + factory VideoCardModel.fromJson(String source) => + VideoCardModel.fromMap(json.decode(source)); + + @override + String toString() { + return 'VideoCardModel(isInterested: $isInterested, imgUrl: $imgUrl, name: $name, rate: $rate)'; + } +} diff --git a/douban_app/lib/views/book/index.dart b/douban_app/lib/views/book/index.dart index 246d68b..aea3772 100644 --- a/douban_app/lib/views/book/index.dart +++ b/douban_app/lib/views/book/index.dart @@ -1,3 +1,4 @@ +import 'package:douban_app/components/round_cap_underline_tab_indicator.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:douban_app/components/top_search/index.dart'; import 'package:douban_app/views/book/tab_pages/movie/index.dart'; @@ -41,8 +42,9 @@ class _BookState extends State with SingleTickerProviderStateMixin { // indicatorColor: Color.fromARGB(255, 25, 25, 25), // indicatorWeight: 2, indicatorSize: TabBarIndicatorSize.label, - indicator: UnderlineTabIndicator( - borderSide: BorderSide(color: Colors.red, width: 2)), + // 定制tab下划线,圆角 + indicator: RoundCapUnderlineTabIndicator( + borderSide: BorderSide(width: 3.w, color: Colors.black)), labelColor: Color.fromARGB(255, 25, 25, 25), unselectedLabelColor: Color.fromARGB(255, 129, 129, 129), onTap: (index) { @@ -60,7 +62,11 @@ class _BookState extends State with SingleTickerProviderStateMixin { ), ), body: Container( - color: Colors.white, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.w, color: Color.fromRGBO(224, 224, 224, 1))), + color: Colors.white), child: TabBarView( controller: this._tabController, children: this.tabsViews))); } diff --git a/douban_app/lib/views/book/tab_pages/movie/ad_area.dart b/douban_app/lib/views/book/tab_pages/movie/ad_area.dart new file mode 100644 index 0000000..6baf88c --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/ad_area.dart @@ -0,0 +1,20 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class ADArea extends StatelessWidget { + const ADArea({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: Colors.green.shade400, + borderRadius: BorderRadius.all(Radius.circular(6.w))), + width: 1.sw, + height: 80.w, + child: Text("广告区域"), + ); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/coming_soon/coming_soon_card.dart b/douban_app/lib/views/book/tab_pages/movie/coming_soon/coming_soon_card.dart new file mode 100644 index 0000000..29cf706 --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/coming_soon/coming_soon_card.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +typedef OnTapFunc = void Function(); + +class ComingSoonCard extends StatelessWidget { + final String cartTitle; + final String cardSubTitle; + final List imgUrlList; + final OnTapFunc onTapFunc; + + const ComingSoonCard( + {Key key, + this.cartTitle, + this.cardSubTitle, + this.imgUrlList = const [], + this.onTapFunc}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + this.onTapFunc(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6.w), + border: Border.all(color: Color.fromRGBO(223, 223, 223, 1)), + boxShadow: [ + BoxShadow( + color: Color.fromRGBO(253, 253, 253, 1), + offset: Offset.zero, + blurRadius: 8.w, + ) + ]), + child: Text(cardSubTitle), + ), + ); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/coming_soon/index.dart b/douban_app/lib/views/book/tab_pages/movie/coming_soon/index.dart new file mode 100644 index 0000000..99a4072 --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/coming_soon/index.dart @@ -0,0 +1,51 @@ +import 'package:douban_app/views/book/tab_pages/movie/coming_soon/coming_soon_card.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter/cupertino.dart'; + +class ComingSoonList extends StatefulWidget { + ComingSoonList({Key key}) : super(key: key); + + @override + _ComingSoonListState createState() => _ComingSoonListState(); +} + +class _ComingSoonListState extends State { + int internalCount = 8; + int internationalCount = 10; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ComingSoonCard( + cartTitle: "国内即将上映", + cardSubTitle: "近期有${internalCount}部热门电影", + imgUrlList: [ + "https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2692433481.webp", + "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2684036997.webp", + "https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2700138245.webp" + ], + onTapFunc: () { + print("tap 国内即将上映"); + }, + ), + ComingSoonCard( + cartTitle: "全球值得期待", + cardSubTitle: "近期有${internationalCount}部热门电影", + imgUrlList: [ + "https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2692433481.webp", + "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2684036997.webp", + "https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2700138245.webp" + ], + onTapFunc: () { + print("tap 全球值得期待"); + }, + ) + ], + ), + ); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/index.dart b/douban_app/lib/views/book/tab_pages/movie/index.dart index 558135f..d08fbc6 100644 --- a/douban_app/lib/views/book/tab_pages/movie/index.dart +++ b/douban_app/lib/views/book/tab_pages/movie/index.dart @@ -1,10 +1,21 @@ +import 'package:douban_app/views/book/tab_pages/movie/ad_area.dart'; +import 'package:douban_app/views/book/tab_pages/movie/coming_soon/index.dart'; +import 'package:douban_app/views/book/tab_pages/movie/popular/index.dart'; import 'package:douban_app/views/book/tab_pages/movie/top_banner.dart'; import 'package:flutter/material.dart'; -Widget TabMovie() { - return ListView( - children: [ - TopBanner(), - ], - ); +class TabMovie extends StatefulWidget { + TabMovie({Key key}) : super(key: key); + + @override + _TabMovieState createState() => _TabMovieState(); +} + +class _TabMovieState extends State { + @override + Widget build(BuildContext context) { + return ListView( + children: [TopBanner(), PopularMovie(), ComingSoonList(), ADArea()], + ); + } } diff --git a/douban_app/lib/views/book/tab_pages/movie/popular/index.dart b/douban_app/lib/views/book/tab_pages/movie/popular/index.dart new file mode 100644 index 0000000..358644c --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/popular/index.dart @@ -0,0 +1,80 @@ +import 'package:douban_app/views/book/tab_pages/movie/popular/tabs.dart'; +import 'package:douban_app/views/book/tab_pages/movie/popular/video_list.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter/cupertino.dart'; + +class PopularMovie extends StatefulWidget { + @override + _PopularMovieState createState() => _PopularMovieState(); +} + +class _PopularMovieState extends State { + int allCount = 28; + int activeIndex = 0; + + var hotShowData = [ + {"name": '很长的电影名称,要多长有多长'}, + {"name": '热映名称1'}, + {"name": '热映名称2'}, + {"name": '热映名称3'} + ]; + + var popularData = [ + {"name": '热门很长的电影名称,要多长有多长'}, + {"name": '热门电影名称1'}, + {"name": '热门电影名称2'}, + {"name": '热门电影名称3'} + ]; + var activeList; + + @override + void initState() { + super.initState(); + activeList = activeIndex == 0 ? hotShowData : popularData; + } + + @override + Widget build(BuildContext context) { + return Container( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CusTabs( + defaultActiveIndex: activeIndex, + onTabChange: (int index) { + setState(() { + activeIndex = index; + activeList = activeIndex == 0 ? hotShowData : popularData; + }); + }, + labelList: ["影院热映", "豆瓣热门"], + ), + TextButton( + onPressed: () => setState(() { + print("TODO: 跳转 tabIndex 为 ${activeIndex} 的详情"); + }), + child: Row( + children: [ + Text( + "全部 ${allCount}", + style: TextStyle( + color: Colors.black, fontWeight: FontWeight.bold), + ), + Icon(Icons.chevron_right_outlined, color: Colors.black) + ], + ), + ) + ], + ), + ), + VideoList(activeList) + ], + )); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/popular/tabs.dart b/douban_app/lib/views/book/tab_pages/movie/popular/tabs.dart new file mode 100644 index 0000000..756c91d --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/popular/tabs.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +typedef TabChangeFunc = void Function(int); + +class CusTabs extends StatefulWidget { + // 切换tab的回调 + final TabChangeFunc onTabChange; + // 默认激活的tab + final int defaultActiveIndex; + // tab的list + final List labelList; + + CusTabs( + {this.onTabChange, + this.defaultActiveIndex = 0, + this.labelList = const ["默认标题"]}); + + @override + _CusTabsState createState() => _CusTabsState(); +} + +class _CusTabsState extends State { + int activeIndex; + @override + void initState() { + super.initState(); + activeIndex = widget.defaultActiveIndex; + } + + _buildTabs() { + List tabs = []; + double fontHeight = 18.sp; + var labelList = widget.labelList; + for (int i = 0; i < labelList.length; i++) { + //title + tabs.add(InkWell( + onTap: () { + // 修改状态,重新渲染 + setState(() => activeIndex = i); + // 调用回调函数 + if (widget.onTabChange != null) { + widget.onTabChange(activeIndex); + } + }, + child: Text( + labelList[i], + style: activeIndex == i + ? TextStyle(fontSize: fontHeight, fontWeight: FontWeight.w600) + : TextStyle(fontSize: fontHeight, color: Colors.grey), + ))); + + // 加分隔线 + if (i + 1 < labelList.length) { + tabs.add(Container( + margin: EdgeInsets.symmetric(horizontal: 4.w), + height: fontHeight, + child: VerticalDivider( + color: Colors.grey, + width: 1.w, + ), + )); + } + } + return tabs; + } + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildTabs(), + )); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/popular/video_card.dart b/douban_app/lib/views/book/tab_pages/movie/popular/video_card.dart new file mode 100644 index 0000000..ef00045 --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/popular/video_card.dart @@ -0,0 +1,51 @@ +import 'package:douban_app/components/star_rate/index.dart'; +import 'package:douban_app/models/book_av/movie_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class VideoCard extends StatefulWidget { + final VideoCardModel videoCardModel; + final double containerWidth; + VideoCard( + {Key key, @required this.videoCardModel, this.containerWidth = 0.24}) + : super(key: key); + + @override + _VideoCardState createState() => _VideoCardState(); +} + +class _VideoCardState extends State { + @override + Widget build(BuildContext context) { + var videoCardModel = widget.videoCardModel; + var width = widget.containerWidth; + return Container( + margin: EdgeInsets.only(right: 10.w), + width: width.sw, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: Image.network( + videoCardModel.imgUrl ?? + "https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2687443734.webp", + height: 0.355.sw, + fit: BoxFit.cover, + ), + ), + Text( + videoCardModel.name, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontWeight: FontWeight.bold), + ), + StartRate( + rate: videoCardModel.rate ?? 0.0, + ) + ], + ), + ); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/popular/video_list.dart b/douban_app/lib/views/book/tab_pages/movie/popular/video_list.dart new file mode 100644 index 0000000..3086ec9 --- /dev/null +++ b/douban_app/lib/views/book/tab_pages/movie/popular/video_list.dart @@ -0,0 +1,33 @@ +import 'package:douban_app/models/book_av/movie_model.dart'; +import 'package:douban_app/views/book/tab_pages/movie/popular/video_card.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class VideoList extends StatefulWidget { + final List> listMap; + VideoList(this.listMap, {Key key}) : super(key: key); + + @override + _VideoListState createState() => _VideoListState(); +} + +class _VideoListState extends State { + /// 将map转化为VideoCardModel list + getMovieList(List> listMap, {int limit = 10}) { + return listMap.sublist(0, listMap.length > limit ? limit : null).map((e) { + return VideoCard(videoCardModel: VideoCardModel.fromMap(e)); + }).toList(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 180.w, + margin: EdgeInsets.symmetric(horizontal: 20.w), + child: ListView( + scrollDirection: Axis.horizontal, + children: getMovieList(widget.listMap), + ), + ); + } +} diff --git a/douban_app/lib/views/book/tab_pages/movie/top_banner.dart b/douban_app/lib/views/book/tab_pages/movie/top_banner.dart index 2ddf3e6..89f9589 100644 --- a/douban_app/lib/views/book/tab_pages/movie/top_banner.dart +++ b/douban_app/lib/views/book/tab_pages/movie/top_banner.dart @@ -16,18 +16,25 @@ List _getBanner(List itemList) { .map((item) => DecoratedBox( decoration: BoxDecoration( color: Colors.white, + boxShadow: [ + BoxShadow( + color: Color.fromRGBO(250, 250, 250, 1), + offset: Offset(2.0, 2.0), + blurRadius: 4.0.w) + ], border: Border.all( color: Color.fromRGBO(223, 223, 223, 1), width: 1.w, ), - borderRadius: BorderRadius.all(Radius.circular(12.r))), + borderRadius: BorderRadius.all(Radius.circular(6.r))), child: InkWell( onTap: () => { //TODO: 点击跳转事件 print("tap") }, child: Container( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), + width: 0.18.sw, + padding: EdgeInsets.symmetric(vertical: 4.w), child: Column( children: [ Icon( @@ -58,6 +65,6 @@ Widget TopBanner() { ], ); return Container( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 24.w), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 14.w), child: Wrap(alignment: WrapAlignment.spaceBetween, children: children)); } -- Gitee