├── .gitignore ├── .metadata ├── 48.jpg ├── DoubanAPI.md ├── README.md ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── doubanapp │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── ic_launcher.png │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-ldpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_bg.png │ │ │ └── ic_launcher_fg.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets └── images │ ├── bg_books_stack_default.png │ ├── bg_music_stack_default.png │ ├── bg_person_center_default.webp │ ├── bg_videos_stack_default.png │ ├── douban_film_list.png │ ├── douban_guess.png │ ├── douban_top.png │ ├── find_movie.png │ ├── home.png │ ├── ic_action_playable_video_s.png │ ├── ic_arrow_back.png │ ├── ic_circle_yellow.png │ ├── ic_default_img_subject_movie.9.png │ ├── ic_group_check_anonymous.png │ ├── ic_group_checked_anonymous.png │ ├── ic_group_top.png │ ├── ic_info_done.png │ ├── ic_info_wish.png │ ├── ic_launcher.png │ ├── ic_me_doulist.png │ ├── ic_me_follows.png │ ├── ic_me_journal.png │ ├── ic_me_photo_album.png │ ├── ic_me_wallet.png │ ├── ic_more.png │ ├── ic_new_empty_view_default.png │ ├── ic_notification_tv_calendar_comments.png │ ├── ic_notify.png │ ├── ic_pause.png │ ├── ic_playing.png │ ├── ic_status_detail_reshare_icon.png │ ├── ic_subject_mark_added.png │ ├── ic_subject_rating_mark_wish.png │ ├── ic_tab_group_active.png │ ├── ic_tab_group_normal.png │ ├── ic_tab_home_active.png │ ├── ic_tab_home_normal.png │ ├── ic_tab_profile_active.png │ ├── ic_tab_profile_normal.png │ ├── ic_tab_shiji_active.png │ ├── ic_tab_shiji_normal.png │ ├── ic_tab_subject_active.png │ ├── ic_tab_subject_normal.png │ ├── ic_vote.png │ ├── ic_vote_normal_large.png │ ├── landscape.png │ ├── person_top_bg.jpg │ └── sofa.png ├── douya-qr.png ├── douya-release.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-20-ipad.png │ │ ├── icon-20@2x-ipad.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29-ipad.png │ │ ├── icon-29.png │ │ ├── icon-29@2x-ipad.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-57.png │ │ ├── icon-57@2x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ └── icon-83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── bean │ ├── celebrity_entity.dart │ ├── celebrity_work_entity.dart │ ├── comments_entity.dart │ ├── movie_detail_bean.dart │ ├── movie_long_comments_entity.dart │ ├── search_result_entity.dart │ ├── subject_entity.dart │ └── top_item_bean.dart ├── constant │ ├── cache_key.dart │ ├── color_constant.dart │ ├── constant.dart │ └── text_size_constant.dart ├── demo │ ├── image_colors.dart │ ├── my_scroll_view.dart │ ├── my_scrollable.dart │ └── slide_container.dart ├── generated │ └── i18n.dart ├── http │ ├── API.dart │ ├── http_request.dart │ └── mock_request.dart ├── main.dart ├── pages │ ├── container_page.dart │ ├── detail │ │ ├── detail_page.dart │ │ ├── detail_title_widget.dart │ │ ├── long_comment_widget.dart │ │ ├── look_confirm_button.dart │ │ ├── score_start.dart │ │ ├── summary_widget.dart │ │ └── tags_widget.dart │ ├── douya_top_250_list_widget.dart │ ├── group │ │ └── group_page.dart │ ├── home │ │ ├── home_app_bar.dart │ │ ├── home_page.dart │ │ └── my_home_tab_bar.dart │ ├── movie │ │ ├── book_audio_video_page.dart │ │ ├── hot_soon_movie_widget.dart │ │ ├── hot_soon_tab_bar.dart │ │ ├── movie_app_bar.dart │ │ ├── movie_list_page.dart │ │ ├── movie_page.dart │ │ ├── movie_page_new.dart │ │ ├── title_widget.dart │ │ ├── today_play_movie_widget.dart │ │ ├── top_item_widget.dart │ │ └── tv_page.dart │ ├── person │ │ └── person_center_page.dart │ ├── person_detail_page.dart │ ├── photo_hero_page.dart │ ├── search │ │ └── search_page.dart │ ├── shop_page.dart │ ├── splash │ │ └── splash_widget.dart │ ├── videos_play_page.dart │ └── web_view_page.dart ├── repository │ ├── movie_repository.dart │ └── person_detail_repository.dart ├── router.dart ├── util │ ├── pick_img_main_color.dart │ └── screen_utils.dart └── widgets │ ├── animal_photo.dart │ ├── bottom_drag_widget.dart │ ├── image │ ├── LaminatedImage.dart │ ├── cache_img_radius.dart │ ├── heart_img_widget.dart │ ├── network_img_widget.dart │ └── radius_img.dart │ ├── item_count_title.dart │ ├── loading_widget.dart │ ├── my_tab_bar_widget.dart │ ├── rating_bar.dart │ ├── search_text_field_widget.dart │ ├── subject_mark_image_widget.dart │ ├── title_bar.dart │ ├── video_progress_bar.dart │ └── video_widget.dart ├── logo.png ├── mock ├── celebrity.json ├── coming_soon.json ├── comments.json ├── group.json ├── in_theaters.json ├── reviews.json ├── subject_26266893.json ├── top250.json ├── weekly.json └── works.json ├── pubspec.yaml └── res └── values └── strings_en.arb /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a88fbc5fd987dce78e468ec45e9e841a49f422d 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /48.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/48.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-new/logo.png) 2 | 3 | 4 | [![GitHub stars](https://img.shields.io/github/stars/kaina404/FlutterDouBan.svg)](https://github.com/kaina404/FlutterDouBan/stargazers) 5 | [![GitHub forks](https://img.shields.io/github/forks/kaina404/FlutterDouBan.svg)](https://github.com/kaina404/FlutterDouBan/network) 6 | [![GitHub issues](https://img.shields.io/github/issues/kaina404/FlutterDouBan.svg)](https://github.com/kaina404/FlutterDouBan/issues) 7 | 8 | > SDK Version 9 | ```java 10 | kaina404 ~ % flutter --version 11 | Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git 12 | Framework • revision 18116933e7 (4 months ago) • 2021-10-15 10:46:35 -0700 13 | Engine • revision d3ea636dc5 14 | Tools • Dart 2.14.4 15 | kaina404 ~ % flutter doctor 16 | Doctor summary (to see all details, run flutter doctor -v): 17 | [✓] Flutter (Channel stable, 2.5.3, on macOS 11.4 20F71 darwin-arm, locale zh-Hans-CN) 18 | [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0) 19 | [✓] Xcode - develop for iOS and macOS 20 | [✓] Chrome - develop for the web 21 | [✓] Android Studio (version 2020.3) 22 | [✓] VS Code (version 1.62.3) 23 | [✓] Connected device (3 available) 24 | ``` 25 | 26 | > 如果产生其他依赖无法编译的问题,可以尝试将`pubspec.yaml`中的`dependencies`中的所有依赖的"^"去掉,重新编译尝试。 27 | ## [博客地址](https://www.jianshu.com/u/1c09737416aa) 28 | 真正的豆瓣客户端,90%还原豆瓣。首页、书影音、小组、市集及个人中心,一个不拉。项目持续更新中... 29 | # [演示预览(右键,新标签页面打开)](https://img.xuvip.top/douyademo.mp4) 30 | 31 | **如果您觉得还可以的话,给个Star白~** 32 | 33 | # 使用Flutter开发一个豆瓣App 34 | 35 | * 此项目,90%还原某瓣APP,所有UI均按照某瓣来实现。 36 | * 项目中的数据均来自豆瓣api真实有效数据 37 | * 项目中用到了几乎所有的Flutter widget 38 | * 还有两个比较大的自定义魔改源码实现特效 39 | * 大年初一也在维护的项目 40 | 41 | > APP中所有数据均为真实数据。但是默认,对于"书影音单个电影tab"的数据,使用模拟数据。因为,频繁的打开关闭APP,会频繁调用 42 | 这个接口。接口是有调用限制的,次数过于频繁,会被锁IP。如果想看真实数据,则可以进入 43 | "我的",然后打开"书影音数据来自网络"开关后,重启APP即可。 44 | 45 | # 下载地址 46 | ## [打开新页面扫码下载](https://upload-images.jianshu.io/upload_images/3884536-d9adbda0e5f61c84.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 47 | #### [Release版本下载地址](https://img.xuvip.top/douya_release.apk) 48 | 49 | 50 | ![扫一扫下载体验](https://img.xuvip.top/douya.png) 51 | 52 | 53 | 54 | 55 | 56 | #### Demo(刷不出gif图的,耐心等待一会,或者多刷几次。) 57 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/0E95A04AE84EFE31104AC8E0A5808CB9.png) 58 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/041919372752_04CCDD7BC1BDB6015935EE50DDF75C29F.png) 59 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/041919374444_0B3C7C7E29941F75D0A7C944D4E352CB7.png) 60 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/041919375761_05292CAB58428C7C77C544027FC899CC0.png) 61 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/041919380838_07B17727ACF231C6D91914D71114A96CF.png) 62 | ![](https://github.com/kaina404/FlutterDouBan/blob/master-release/041919381924_01CE541B98F565C72B75567A319271CA1.png) 63 | 64 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-12-55.gif) 65 | 66 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-13-11.gif) 67 | 68 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-17-38.gif) 69 | 70 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-17-48.gif) 71 | 72 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-18-03.gif) 73 | 74 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-18-12.gif) 75 | 76 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-18-23.gif) 77 | 78 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-30-58.gif) 79 | 80 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-31-13.gif) 81 | 82 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-32-29.gif) 83 | 84 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-32-41.gif) 85 | 86 | ![demo1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/demogif/Mar-10-2019%2014-33-02.gif) 87 | 88 | 89 | 90 | # dev-open 91 | 92 | ### 大家可以向这个分支根据豆瓣UI做设计稿,提交代码 93 | 94 | * 这个分支供广大Flutter开发者来学习Flutter 95 | * **此分支囊括了Flutter 90%的组件的基本与组合使用**,是初学者真正实践的不错选择 96 | * 在此豆芽APP的首页实现与影片详情的UI特效,基于魔改Flutter源码。有兴趣可以看看 97 | * 大家想实战自己的Flutter能力,可以将某瓣APP作为设计稿,完成需求,并提交 98 | * 每位开发者提交的代码,我都会在文档中进行备注 99 | 100 | #### 注意!!!建议使用模拟数据(mock_request.dart) 101 | * 每个接口均有一定的调用限制 102 | * *大家pull下的代码,进行测试调试时,对于特定接口,返回的特定数据。尽量不要每次求请求一次。可以请求到一次真实数据后,转成json保存 103 | 到本地。然后,自己调试开发的时候,使用这个模拟数据即可。* [可参考mock_request.dart] 104 | 105 | 106 | 107 | # 未来版本计划(欢迎Flutter爱好者前来认领) 108 | 109 | **欢迎Flutter爱好者共同完成** 110 | 111 | > 涉及到UI,可参考豆瓣。 112 | 113 | ### 萌新TASK 114 | 115 | * 完成任意一个未实现的按钮 116 | * 完成任意一个未实现的页面 117 | * 优化原有Widget 118 | * 爱好者想实现但是未能实现的需求 119 | * More ... 120 | 121 | ### 进阶TASK 122 | 123 | * 优化代码 124 | * 适当缓存数据 125 | * 解决卡顿(可参考:https://flutter-io.cn/docs/testing/ui-performance) 126 | * 优化路由(可参考咸鱼方案:https://www.yuque.com/xytech/flutter/vf1dpf) 127 | 128 | ### 老手TASK 129 | 130 | * [接入rxdart](https://github.com/ReactiveX/rxdart) 131 | * [使用Fish Redux 重构](https://www.yuque.com/xytech/flutter/ycc9ni) 132 | 133 | 134 | 135 | # 对魔改源码或者喜欢翻源码的童鞋可以看看下面两个 136 | 137 | * 魔改Flutter AppBar源码实现豆瓣头部特效 138 | 139 | ![魔改Flutter AppBar源码实现豆瓣头部特效](https://github.com/kaina404/DouBanProject/blob/dev-0.1/%E4%BB%BF%E8%B1%86%E7%93%A3%E5%A4%B4%E9%83%A8.gif) 140 | 141 | * 魔改源码实现电影详情抽屉特效(GIF图如果加载不出来,多刷几次) 142 | 143 | ![抽屉特效1](https://github.com/kaina404/DouBanProject/blob/dev-0.1/part1.gif) 144 | 145 | ![抽屉特效2](https://github.com/kaina404/DouBanProject/blob/dev-0.1/part2.gif) 146 | 147 | ![抽屉特效3](https://github.com/kaina404/DouBanProject/blob/dev-0.1/part3.gif) 148 | 149 | 150 | # 页面介绍 151 | 152 | * 首页 pages/home 153 | 154 | * homo_app_bar.dart 首页导航头 155 | * home_page.dart 首页 156 | * my_home_tab_bar.dart 首页tab 157 | 158 | * 书影音 pages/movie 159 | 160 | * book_audio_video_page.dart 书影音页面 161 | * detail_page.dart 影片、电视详情页面 162 | * person_detail_page.dart 演员页面介绍 163 | * ... 页面都有注释 164 | 165 | * 小组 pages/group 166 | 167 | * 市集 shop_page.dart 168 | * 市集的数据使用两个webview 169 | 170 | * 我的 page/person 171 | 172 | 173 | 174 | # 更新记录 175 | 176 | * dev-0.1 177 | * 魔改源码实现电影详情抽屉特效 178 | * 魔改Flutter AppBar源码实现豆瓣头部特效 179 | * 优化页面逻辑 180 | * 优化加载速度 181 | 182 | * master 183 | * 基本网络请求框架、UI框架 184 | * 已经填入了"最为复杂的电影TAB页面"、影视详情页面、小组页面(UI与豆瓣一致,数据使用的是热映榜) 185 | * 页面上下滑动 186 | * 页面上下+左右滑动 187 | * 数据加载 188 | * TAB页面滑动 189 | * .... 190 | * 作为基本版本,一些逻辑不够完善,有一些bug。 191 | 192 | # 默认条约 193 | 194 | 此项目仅供大家交流沟通使用,不得用于任何商业以及利益活动。由此引起的责任,跟我无关。谢谢! 195 | 196 | # **如果您觉得还可以的话,给个Star白~** 197 | 198 | # Thanks 199 | 200 | 201 | 202 | ## Getting Started 203 | 204 | This project is a starting point for a Flutter application. 205 | 206 | A few resources to get you started if this is your first Flutter project: 207 | 208 | - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) 209 | - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) 210 | 211 | For help getting started with Flutter, view our 212 | [online documentation](https://flutter.io/docs), which offers tutorials, 213 | samples, guidance on mobile development, and a full API reference. 214 | 215 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.doubanapp" 37 | minSdkVersion 19 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/doubanapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.doubanapp; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bg.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_fg.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/images/bg_books_stack_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/bg_books_stack_default.png -------------------------------------------------------------------------------- /assets/images/bg_music_stack_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/bg_music_stack_default.png -------------------------------------------------------------------------------- /assets/images/bg_person_center_default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/bg_person_center_default.webp -------------------------------------------------------------------------------- /assets/images/bg_videos_stack_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/bg_videos_stack_default.png -------------------------------------------------------------------------------- /assets/images/douban_film_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/douban_film_list.png -------------------------------------------------------------------------------- /assets/images/douban_guess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/douban_guess.png -------------------------------------------------------------------------------- /assets/images/douban_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/douban_top.png -------------------------------------------------------------------------------- /assets/images/find_movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/find_movie.png -------------------------------------------------------------------------------- /assets/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/home.png -------------------------------------------------------------------------------- /assets/images/ic_action_playable_video_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_action_playable_video_s.png -------------------------------------------------------------------------------- /assets/images/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_arrow_back.png -------------------------------------------------------------------------------- /assets/images/ic_circle_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_circle_yellow.png -------------------------------------------------------------------------------- /assets/images/ic_default_img_subject_movie.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_default_img_subject_movie.9.png -------------------------------------------------------------------------------- /assets/images/ic_group_check_anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_group_check_anonymous.png -------------------------------------------------------------------------------- /assets/images/ic_group_checked_anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_group_checked_anonymous.png -------------------------------------------------------------------------------- /assets/images/ic_group_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_group_top.png -------------------------------------------------------------------------------- /assets/images/ic_info_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_info_done.png -------------------------------------------------------------------------------- /assets/images/ic_info_wish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_info_wish.png -------------------------------------------------------------------------------- /assets/images/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_launcher.png -------------------------------------------------------------------------------- /assets/images/ic_me_doulist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_me_doulist.png -------------------------------------------------------------------------------- /assets/images/ic_me_follows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_me_follows.png -------------------------------------------------------------------------------- /assets/images/ic_me_journal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_me_journal.png -------------------------------------------------------------------------------- /assets/images/ic_me_photo_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_me_photo_album.png -------------------------------------------------------------------------------- /assets/images/ic_me_wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_me_wallet.png -------------------------------------------------------------------------------- /assets/images/ic_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_more.png -------------------------------------------------------------------------------- /assets/images/ic_new_empty_view_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_new_empty_view_default.png -------------------------------------------------------------------------------- /assets/images/ic_notification_tv_calendar_comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_notification_tv_calendar_comments.png -------------------------------------------------------------------------------- /assets/images/ic_notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_notify.png -------------------------------------------------------------------------------- /assets/images/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_pause.png -------------------------------------------------------------------------------- /assets/images/ic_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_playing.png -------------------------------------------------------------------------------- /assets/images/ic_status_detail_reshare_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_status_detail_reshare_icon.png -------------------------------------------------------------------------------- /assets/images/ic_subject_mark_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_subject_mark_added.png -------------------------------------------------------------------------------- /assets/images/ic_subject_rating_mark_wish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_subject_rating_mark_wish.png -------------------------------------------------------------------------------- /assets/images/ic_tab_group_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_group_active.png -------------------------------------------------------------------------------- /assets/images/ic_tab_group_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_group_normal.png -------------------------------------------------------------------------------- /assets/images/ic_tab_home_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_home_active.png -------------------------------------------------------------------------------- /assets/images/ic_tab_home_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_home_normal.png -------------------------------------------------------------------------------- /assets/images/ic_tab_profile_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_profile_active.png -------------------------------------------------------------------------------- /assets/images/ic_tab_profile_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_profile_normal.png -------------------------------------------------------------------------------- /assets/images/ic_tab_shiji_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_shiji_active.png -------------------------------------------------------------------------------- /assets/images/ic_tab_shiji_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_shiji_normal.png -------------------------------------------------------------------------------- /assets/images/ic_tab_subject_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_subject_active.png -------------------------------------------------------------------------------- /assets/images/ic_tab_subject_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_tab_subject_normal.png -------------------------------------------------------------------------------- /assets/images/ic_vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_vote.png -------------------------------------------------------------------------------- /assets/images/ic_vote_normal_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/ic_vote_normal_large.png -------------------------------------------------------------------------------- /assets/images/landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/landscape.png -------------------------------------------------------------------------------- /assets/images/person_top_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/person_top_bg.jpg -------------------------------------------------------------------------------- /assets/images/sofa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/assets/images/sofa.png -------------------------------------------------------------------------------- /douya-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/douya-qr.png -------------------------------------------------------------------------------- /douya-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/douya-release.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "20x20", 5 | "idiom": "iphone", 6 | "filename": "icon-20@2x.png", 7 | "scale": "2x" 8 | }, 9 | { 10 | "size": "20x20", 11 | "idiom": "iphone", 12 | "filename": "icon-20@3x.png", 13 | "scale": "3x" 14 | }, 15 | { 16 | "size": "29x29", 17 | "idiom": "iphone", 18 | "filename": "icon-29.png", 19 | "scale": "1x" 20 | }, 21 | { 22 | "size": "29x29", 23 | "idiom": "iphone", 24 | "filename": "icon-29@2x.png", 25 | "scale": "2x" 26 | }, 27 | { 28 | "size": "29x29", 29 | "idiom": "iphone", 30 | "filename": "icon-29@3x.png", 31 | "scale": "3x" 32 | }, 33 | { 34 | "size": "40x40", 35 | "idiom": "iphone", 36 | "filename": "icon-40@2x.png", 37 | "scale": "2x" 38 | }, 39 | { 40 | "size": "40x40", 41 | "idiom": "iphone", 42 | "filename": "icon-40@3x.png", 43 | "scale": "3x" 44 | }, 45 | { 46 | "size": "57x57", 47 | "idiom": "iphone", 48 | "filename": "icon-57.png", 49 | "scale": "1x" 50 | }, 51 | { 52 | "size": "57x57", 53 | "idiom": "iphone", 54 | "filename": "icon-57@2x.png", 55 | "scale": "2x" 56 | }, 57 | { 58 | "size": "60x60", 59 | "idiom": "iphone", 60 | "filename": "icon-60@2x.png", 61 | "scale": "2x" 62 | }, 63 | { 64 | "size": "60x60", 65 | "idiom": "iphone", 66 | "filename": "icon-60@3x.png", 67 | "scale": "3x" 68 | }, 69 | { 70 | "size": "20x20", 71 | "idiom": "ipad", 72 | "filename": "icon-20-ipad.png", 73 | "scale": "1x" 74 | }, 75 | { 76 | "size": "20x20", 77 | "idiom": "ipad", 78 | "filename": "icon-20@2x-ipad.png", 79 | "scale": "2x" 80 | }, 81 | { 82 | "size": "29x29", 83 | "idiom": "ipad", 84 | "filename": "icon-29-ipad.png", 85 | "scale": "1x" 86 | }, 87 | { 88 | "size": "29x29", 89 | "idiom": "ipad", 90 | "filename": "icon-29@2x-ipad.png", 91 | "scale": "2x" 92 | }, 93 | { 94 | "size": "40x40", 95 | "idiom": "ipad", 96 | "filename": "icon-40.png", 97 | "scale": "1x" 98 | }, 99 | { 100 | "size": "40x40", 101 | "idiom": "ipad", 102 | "filename": "icon-40@2x.png", 103 | "scale": "2x" 104 | }, 105 | { 106 | "size": "50x50", 107 | "idiom": "ipad", 108 | "filename": "icon-50.png", 109 | "scale": "1x" 110 | }, 111 | { 112 | "size": "50x50", 113 | "idiom": "ipad", 114 | "filename": "icon-50@2x.png", 115 | "scale": "2x" 116 | }, 117 | { 118 | "size": "72x72", 119 | "idiom": "ipad", 120 | "filename": "icon-72.png", 121 | "scale": "1x" 122 | }, 123 | { 124 | "size": "72x72", 125 | "idiom": "ipad", 126 | "filename": "icon-72@2x.png", 127 | "scale": "2x" 128 | }, 129 | { 130 | "size": "76x76", 131 | "idiom": "ipad", 132 | "filename": "icon-76.png", 133 | "scale": "1x" 134 | }, 135 | { 136 | "size": "76x76", 137 | "idiom": "ipad", 138 | "filename": "icon-76@2x.png", 139 | "scale": "2x" 140 | }, 141 | { 142 | "size": "83.5x83.5", 143 | "idiom": "ipad", 144 | "filename": "icon-83.5@2x.png", 145 | "scale": "2x" 146 | }, 147 | { 148 | "size": "1024x1024", 149 | "idiom": "ios-marketing", 150 | "filename": "icon-1024.png", 151 | "scale": "1x" 152 | } 153 | ], 154 | "info": { 155 | "version": 1, 156 | "author": "xjh" 157 | } 158 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | 豆芽 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | doubanapp 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIViewControllerBasedStatusBarAppearance 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/bean/search_result_entity.dart: -------------------------------------------------------------------------------- 1 | class SearchResultEntity { 2 | int total; 3 | List subjects; 4 | int count; 5 | int start; 6 | String title; 7 | 8 | SearchResultEntity({this.total, this.subjects, this.count, this.start, this.title}); 9 | 10 | SearchResultEntity.fromJson(Map json) { 11 | total = json['total']; 12 | if (json['subjects'] != null) { 13 | subjects = new List(); 14 | json['subjects'].forEach((v) { subjects.add(new SearchResultSubject.fromJson(v)); }); 15 | } 16 | count = json['count']; 17 | start = json['start']; 18 | title = json['title']; 19 | } 20 | 21 | Map toJson() { 22 | final Map data = new Map(); 23 | data['total'] = this.total; 24 | if (this.subjects != null) { 25 | data['subjects'] = this.subjects.map((v) => v.toJson()).toList(); 26 | } 27 | data['count'] = this.count; 28 | data['start'] = this.start; 29 | data['title'] = this.title; 30 | return data; 31 | } 32 | } 33 | 34 | class SearchResultSubject { 35 | SearchResultSubjectsImages images; 36 | String originalTitle; 37 | String year; 38 | List directors; 39 | SearchResultSubjectsRating rating; 40 | String alt; 41 | String title; 42 | int collectCount; 43 | bool hasVideo; 44 | List pubdates; 45 | List casts; 46 | String subtype; 47 | List genres; 48 | List durations; 49 | String mainlandPubdate; 50 | String id; 51 | 52 | SearchResultSubject({this.images, this.originalTitle, this.year, this.directors, this.rating, this.alt, this.title, this.collectCount, this.hasVideo, this.pubdates, this.casts, this.subtype, this.genres, this.durations, this.mainlandPubdate, this.id}); 53 | 54 | SearchResultSubject.fromJson(Map json) { 55 | images = json['images'] != null ? new SearchResultSubjectsImages.fromJson(json['images']) : null; 56 | originalTitle = json['original_title']; 57 | year = json['year']; 58 | if (json['directors'] != null) { 59 | directors = new List(); 60 | json['directors'].forEach((v) { directors.add(new SearchResultSubjectsDirector.fromJson(v)); }); 61 | } 62 | rating = json['rating'] != null ? new SearchResultSubjectsRating.fromJson(json['rating']) : null; 63 | alt = json['alt']; 64 | title = json['title']; 65 | collectCount = json['collect_count']; 66 | hasVideo = json['has_video']; 67 | pubdates = json['pubdates'].cast(); 68 | if (json['casts'] != null) { 69 | casts = new List(); 70 | json['casts'].forEach((v) { casts.add(new SearchResultSubjectsCast.fromJson(v)); }); 71 | } 72 | subtype = json['subtype']; 73 | genres = json['genres'].cast(); 74 | durations = json['durations'].cast(); 75 | mainlandPubdate = json['mainland_pubdate']; 76 | id = json['id']; 77 | } 78 | 79 | Map toJson() { 80 | final Map data = new Map(); 81 | if (this.images != null) { 82 | data['images'] = this.images.toJson(); 83 | } 84 | data['original_title'] = this.originalTitle; 85 | data['year'] = this.year; 86 | if (this.directors != null) { 87 | data['directors'] = this.directors.map((v) => v.toJson()).toList(); 88 | } 89 | if (this.rating != null) { 90 | data['rating'] = this.rating.toJson(); 91 | } 92 | data['alt'] = this.alt; 93 | data['title'] = this.title; 94 | data['collect_count'] = this.collectCount; 95 | data['has_video'] = this.hasVideo; 96 | data['pubdates'] = this.pubdates; 97 | if (this.casts != null) { 98 | data['casts'] = this.casts.map((v) => v.toJson()).toList(); 99 | } 100 | data['subtype'] = this.subtype; 101 | data['genres'] = this.genres; 102 | data['durations'] = this.durations; 103 | data['mainland_pubdate'] = this.mainlandPubdate; 104 | data['id'] = this.id; 105 | return data; 106 | } 107 | } 108 | 109 | class SearchResultSubjectsImages { 110 | String small; 111 | String large; 112 | String medium; 113 | 114 | SearchResultSubjectsImages({this.small, this.large, this.medium}); 115 | 116 | SearchResultSubjectsImages.fromJson(Map json) { 117 | small = json['small']; 118 | large = json['large']; 119 | medium = json['medium']; 120 | } 121 | 122 | Map toJson() { 123 | final Map data = new Map(); 124 | data['small'] = this.small; 125 | data['large'] = this.large; 126 | data['medium'] = this.medium; 127 | return data; 128 | } 129 | } 130 | 131 | class SearchResultSubjectsDirector { 132 | var name; 133 | var alt; 134 | var id; 135 | var avatars; 136 | var nameEn; 137 | 138 | SearchResultSubjectsDirector({this.name, this.alt, this.id, this.avatars, this.nameEn}); 139 | 140 | SearchResultSubjectsDirector.fromJson(Map json) { 141 | name = json['name']; 142 | alt = json['alt']; 143 | id = json['id']; 144 | avatars = json['avatars']; 145 | nameEn = json['name_en']; 146 | } 147 | 148 | Map toJson() { 149 | final Map data = new Map(); 150 | data['name'] = this.name; 151 | data['alt'] = this.alt; 152 | data['id'] = this.id; 153 | data['avatars'] = this.avatars; 154 | data['name_en'] = this.nameEn; 155 | return data; 156 | } 157 | } 158 | 159 | class SearchResultSubjectsRating { 160 | var average; 161 | var min; 162 | var max; 163 | SearchResultSubjectsRatingDetails details; 164 | String stars; 165 | 166 | SearchResultSubjectsRating({this.average, this.min, this.max, this.details, this.stars}); 167 | 168 | SearchResultSubjectsRating.fromJson(Map json) { 169 | average = json['average']; 170 | min = json['min']; 171 | max = json['max']; 172 | details = json['details'] != null ? new SearchResultSubjectsRatingDetails.fromJson(json['details']) : null; 173 | stars = json['stars']; 174 | } 175 | 176 | Map toJson() { 177 | final Map data = new Map(); 178 | data['average'] = this.average; 179 | data['min'] = this.min; 180 | data['max'] = this.max; 181 | if (this.details != null) { 182 | data['details'] = this.details.toJson(); 183 | } 184 | data['stars'] = this.stars; 185 | return data; 186 | } 187 | } 188 | 189 | class SearchResultSubjectsRatingDetails { 190 | var d1; 191 | var d2; 192 | var d3; 193 | var d4; 194 | var d5; 195 | 196 | SearchResultSubjectsRatingDetails({this.d1, this.d2, this.d3, this.d4, this.d5}); 197 | 198 | SearchResultSubjectsRatingDetails.fromJson(Map json) { 199 | d1 = json['1']; 200 | d2 = json['2']; 201 | d3 = json['3']; 202 | d4 = json['4']; 203 | d5 = json['5']; 204 | } 205 | 206 | Map toJson() { 207 | final Map data = new Map(); 208 | data['1'] = this.d1; 209 | data['2'] = this.d2; 210 | data['3'] = this.d3; 211 | data['4'] = this.d4; 212 | data['5'] = this.d5; 213 | return data; 214 | } 215 | } 216 | 217 | class SearchResultSubjectsCast { 218 | String name; 219 | var alt; 220 | var id; 221 | var avatars; 222 | String nameEn; 223 | 224 | SearchResultSubjectsCast({this.name, this.alt, this.id, this.avatars, this.nameEn}); 225 | 226 | SearchResultSubjectsCast.fromJson(Map json) { 227 | name = json['name']; 228 | alt = json['alt']; 229 | id = json['id']; 230 | avatars = json['avatars']; 231 | nameEn = json['name_en']; 232 | } 233 | 234 | Map toJson() { 235 | final Map data = new Map(); 236 | data['name'] = this.name; 237 | data['alt'] = this.alt; 238 | data['id'] = this.id; 239 | data['avatars'] = this.avatars; 240 | data['name_en'] = this.nameEn; 241 | return data; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /lib/bean/subject_entity.dart: -------------------------------------------------------------------------------- 1 | class SubjectEntity { 2 | 3 | // "subject":Object{...}, 4 | // "rank":1, 5 | // "delta":0 6 | 7 | Subject subject; 8 | var rank; 9 | var delta; 10 | 11 | SubjectEntity.fromMap(Map map){ 12 | rank = map['rank']; 13 | delta = map['delta']; 14 | var subjectMap = map['subject']; 15 | subject = Subject.fromMap(subjectMap); 16 | } 17 | } 18 | 19 | class Subject { 20 | bool tag = false; 21 | Rating rating; 22 | var genres; 23 | var title; 24 | List casts; 25 | var durations; 26 | var collect_count; 27 | var mainland_pubdate; 28 | var has_video; 29 | var original_title; 30 | var subtype; 31 | var directors; 32 | var pubdates; 33 | var year; 34 | Images images; 35 | var alt; 36 | var id; 37 | 38 | ///构造函数 39 | Subject.fromMap(Map map) { 40 | var rating = map['rating']; 41 | this.rating = Rating(rating['average'], rating['max']); 42 | genres = map['genres']; 43 | title = map['title']; 44 | var castMap = map['casts']; 45 | casts = _converCasts(castMap); 46 | collect_count = map['collect_count']; 47 | original_title = map['original_title']; 48 | subtype = map['subtype']; 49 | directors = map['directors']; 50 | year = map['year']; 51 | var img = map['images']; 52 | images = Images(img['small'], img['large'], img['medium']); 53 | alt = map['alt']; 54 | id = map['id']; 55 | durations = map['durations']; 56 | mainland_pubdate = map['mainland_pubdate']; 57 | has_video = map['has_video']; 58 | pubdates = map['pubdates']; 59 | } 60 | 61 | _converCasts(casts) { 62 | return casts.map((item)=>Cast.fromMap(item)).toList(); 63 | } 64 | 65 | } 66 | 67 | class Images { 68 | var small; 69 | var large; 70 | var medium; 71 | 72 | Images(this.small, this.large, this.medium); 73 | } 74 | 75 | class Rating { 76 | var average; 77 | var max; 78 | Rating(this.average, this.max); 79 | } 80 | 81 | 82 | 83 | class Cast { 84 | var id; 85 | var name_en; 86 | var name; 87 | Avatar avatars; 88 | var alt; 89 | Cast(this.avatars, this.name_en, this.name, this.alt, this.id); 90 | 91 | Cast.fromMap(Map map) { 92 | id = map['id']; 93 | name_en = map['name_en']; 94 | name = map['name']; 95 | alt = map['alt']; 96 | var tmp = map['avatars']; 97 | if(tmp == null){ 98 | avatars = null; 99 | }else{ 100 | avatars = Avatar(tmp['small'], tmp['large'], tmp['medium']); 101 | } 102 | 103 | } 104 | } 105 | 106 | class Avatar { 107 | var medium; 108 | var large; 109 | var small; 110 | Avatar(this.small, this.large, this.medium); 111 | } 112 | -------------------------------------------------------------------------------- /lib/bean/top_item_bean.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:doubanapp/bean/subject_entity.dart'; 3 | import 'dart:math' as math; 4 | 5 | class TopItemBean { 6 | var count;//共多少部 7 | var imgUrl;//图片url 8 | List items;//多少个电影 9 | TopItemBean(this.count, this.imgUrl, this.items); 10 | 11 | ///将周口碑榜数据转换成榜单item对应的数据类型 12 | static TopItemBean convertWeeklyBeans(List weeklyBeans){ 13 | var count = '每周五更新 · 共${math.min(weeklyBeans.length, 10)}部'; 14 | var imgUrl = weeklyBeans[0].subject.images.large; 15 | int itemCount = math.min(4, weeklyBeans.length); 16 | weeklyBeans = weeklyBeans.sublist(0, itemCount); 17 | List items = []; 18 | for(SubjectEntity bean in weeklyBeans){ 19 | items.add(Item(bean.subject.title, bean.subject.rating.average, bean.delta > 0)); 20 | } 21 | return TopItemBean(count, imgUrl, items); 22 | } 23 | 24 | ///将周热门数据转换成榜单item对应的数据类型 25 | static TopItemBean convertHotBeans(List hotBeans) { 26 | var count = '每周五更新 · 共${math.min(10, hotBeans.length)}部'; 27 | var imgUrl = hotBeans[0].images.large; 28 | int itemCount = math.min(4, hotBeans.length); 29 | hotBeans = hotBeans.sublist(0, itemCount); 30 | List items = []; 31 | for(Subject bean in hotBeans){ 32 | items.add(Item(bean.title, bean.rating.average, true)); 33 | } 34 | return TopItemBean(count, imgUrl, items); 35 | } 36 | ///将Top250数据转换成榜单item对应的数据类型 37 | static TopItemBean convertTopBeans(List hotBeans) { 38 | var count = '豆瓣榜单 · 共250部'; 39 | var imgUrl = hotBeans[0].images.large; 40 | int itemCount = math.min(4, hotBeans.length); 41 | hotBeans = hotBeans.sublist(0, itemCount); 42 | List items = []; 43 | for(Subject bean in hotBeans){ 44 | items.add(Item(bean.title, bean.rating.average, true)); 45 | } 46 | return TopItemBean(count, imgUrl, items); 47 | } 48 | 49 | } 50 | 51 | class Item { 52 | var title; //电影名称 53 | var average; //评分 54 | bool upOrDown; //热度上升还是下降 55 | 56 | Item(this.title, this.average, this.upOrDown); 57 | } 58 | -------------------------------------------------------------------------------- /lib/constant/cache_key.dart: -------------------------------------------------------------------------------- 1 | class CacheKey { 2 | static String USE_NET_DATA = 'USE_NET_DATA'; 3 | } -------------------------------------------------------------------------------- /lib/constant/color_constant.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ColorConstant { 4 | static const Color colorRed277 = Colors.redAccent; 5 | static const Color colorDefaultTitle = Color.fromARGB(255, 45, 45, 45); 6 | static const Color colorOrigin = Color.fromARGB(255, 232, 145, 60); 7 | static const Color colorDetail = Color.fromARGB(196, 197, 145, 197); 8 | static const Color ThemeGreen = Color.fromARGB(255, 0, 189, 95); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /lib/constant/constant.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Constant { 4 | static const String BASE_URL = 'api.douban.com'; 5 | 6 | static const String TOP_250 = '/v2/movie/top250'; 7 | 8 | static const String IMG_TMP1 = 9 | 'https://upload-images.jianshu.io/upload_images/3884536-b21bfc556ffcc062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'; 10 | 11 | static const String IMG_TMP2 = 12 | 'https://upload-images.jianshu.io/upload_images/3884536-bb35459fd52009d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'; 13 | 14 | static const double MARGIN_LEFT = 13.0; 15 | static const double MARGIN_RIGHT = 13.0; 16 | static const String ASSETS_IMG = 'assets/images/'; 17 | 18 | static const double TAB_BOTTOM = 8.0; 19 | 20 | static String URL_MP4_DEMO_0 = 'http://vt1.doubanio.com/201902111139/0c06a85c600b915d8c9cbdbbaf06ba9f/view/movie/M/302420330.mp4'; 21 | 22 | static String URL_MP4_DEMO_1 = 'http://vt1.doubanio.com/201903032315/702b9ad25c0da91e1c693e5e4dc5a86e/view/movie/M/302430864.mp4'; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /lib/constant/text_size_constant.dart: -------------------------------------------------------------------------------- 1 | class TextSizeConstant { 2 | ///影院热映、即将上映、豆瓣热门字体大小 3 | static const BookAudioPartTabBar = 20.0; 4 | 5 | } 6 | -------------------------------------------------------------------------------- /lib/generated/i18n.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | // ignore_for_file: non_constant_identifier_names 7 | // ignore_for_file: camel_case_types 8 | // ignore_for_file: prefer_single_quotes 9 | 10 | //This file is automatically generated. DO NOT EDIT, all your changes would be lost. 11 | class S implements WidgetsLocalizations { 12 | const S(); 13 | 14 | static const GeneratedLocalizationsDelegate delegate = 15 | GeneratedLocalizationsDelegate(); 16 | 17 | static S of(BuildContext context) => Localizations.of(context, S); 18 | 19 | @override 20 | TextDirection get textDirection => TextDirection.ltr; 21 | 22 | } 23 | 24 | class $en extends S { 25 | const $en(); 26 | } 27 | 28 | class GeneratedLocalizationsDelegate extends LocalizationsDelegate { 29 | const GeneratedLocalizationsDelegate(); 30 | 31 | List get supportedLocales { 32 | return const [ 33 | Locale("en", ""), 34 | ]; 35 | } 36 | 37 | LocaleListResolutionCallback listResolution({Locale fallback}) { 38 | return (List locales, Iterable supported) { 39 | if (locales == null || locales.isEmpty) { 40 | return fallback ?? supported.first; 41 | } else { 42 | return _resolve(locales.first, fallback, supported); 43 | } 44 | }; 45 | } 46 | 47 | LocaleResolutionCallback resolution({Locale fallback}) { 48 | return (Locale locale, Iterable supported) { 49 | return _resolve(locale, fallback, supported); 50 | }; 51 | } 52 | 53 | Locale _resolve(Locale locale, Locale fallback, Iterable supported) { 54 | if (locale == null || !isSupported(locale)) { 55 | return fallback ?? supported.first; 56 | } 57 | 58 | final Locale languageLocale = Locale(locale.languageCode, ""); 59 | if (supported.contains(locale)) { 60 | return locale; 61 | } else if (supported.contains(languageLocale)) { 62 | return languageLocale; 63 | } else { 64 | final Locale fallbackLocale = fallback ?? supported.first; 65 | return fallbackLocale; 66 | } 67 | } 68 | 69 | @override 70 | Future load(Locale locale) { 71 | final String lang = getLang(locale); 72 | if (lang != null) { 73 | switch (lang) { 74 | case "en": 75 | return SynchronousFuture(const $en()); 76 | default: 77 | // NO-OP. 78 | } 79 | } 80 | return SynchronousFuture(const S()); 81 | } 82 | 83 | @override 84 | bool isSupported(Locale locale) => 85 | locale != null && supportedLocales.contains(locale); 86 | 87 | @override 88 | bool shouldReload(GeneratedLocalizationsDelegate old) => false; 89 | } 90 | 91 | String getLang(Locale l) => l == null 92 | ? null 93 | : l.countryCode != null && l.countryCode.isEmpty 94 | ? l.languageCode 95 | : l.toString(); 96 | -------------------------------------------------------------------------------- /lib/http/http_request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert' as Convert; 3 | import 'dart:io'; 4 | import 'package:http/http.dart' as http; 5 | 6 | typedef RequestCallBack = void Function(Map data); 7 | 8 | class HttpRequest { 9 | static requestGET ( 10 | String authority, String unencodedPath, RequestCallBack callBack, 11 | [Map queryParameters]) async { 12 | try { 13 | var httpClient = new HttpClient(); 14 | //http://api.douban.com/v2/movie/top250?start=25&count=10 15 | var uri = new Uri.http(authority, unencodedPath, queryParameters); 16 | var request = await httpClient.getUrl(uri); 17 | var response = await request.close(); 18 | var responseBody = await response.transform(Convert.utf8.decoder).join(); 19 | Map data = Convert.jsonDecode(responseBody); 20 | callBack(data); 21 | } on Exception catch (e) { 22 | print(e.toString()); 23 | } 24 | } 25 | 26 | final baseUrl; 27 | 28 | HttpRequest(this.baseUrl); 29 | 30 | Future get(String uri, {Map headers}) async { 31 | try { 32 | http.Response response = await http.get(baseUrl + uri, headers: headers); 33 | final statusCode = response.statusCode; 34 | final body = response.body; 35 | print('[uri=$uri][statusCode=$statusCode][response=$body]'); 36 | var result = Convert.jsonDecode(body); 37 | return result; 38 | } on Exception catch (e) { 39 | print('[uri=$uri]exception e=${e.toString()}'); 40 | return ''; 41 | } 42 | } 43 | 44 | Future getResponseBody(String uri, {Map headers}) async { 45 | try { 46 | http.Response response = await http.get(baseUrl + uri, headers: headers); 47 | final statusCode = response.statusCode; 48 | final body = response.body; 49 | // var result = Convert.jsonDecode(body); 50 | print('[uri=$uri][statusCode=$statusCode][response=$body]'); 51 | return body; 52 | } on Exception catch (e) { 53 | print('[uri=$uri]exception e=${e.toString()}'); 54 | return null; 55 | } 56 | } 57 | 58 | Future post(String uri, dynamic body, {Map headers}) async { 59 | try { 60 | http.Response response = await http.post(baseUrl + uri, body: body, headers: headers); 61 | final statusCode = response.statusCode; 62 | final responseBody = response.body; 63 | var result = Convert.jsonDecode(responseBody); 64 | print('[uri=$uri][statusCode=$statusCode][response=$responseBody]'); 65 | return result; 66 | } on Exception catch (e) { 67 | print('[uri=$uri]exception e=${e.toString()}'); 68 | return ''; 69 | } 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/http/mock_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:doubanapp/http/API.dart'; 2 | import 'dart:async'; 3 | import 'package:flutter/services.dart' show rootBundle; 4 | import 'dart:convert'; 5 | 6 | ///模拟数据 7 | class MockRequest { 8 | 9 | Future get(String action, {Map params}) async { 10 | return mock(action: getJsonName(action), params: params); 11 | } 12 | 13 | Future post({String action, Map params}) async { 14 | return mock(action: action, params: params); 15 | } 16 | 17 | Future mock({String action, Map params}) async { 18 | var responseStr = await rootBundle.loadString('mock/$action.json'); 19 | var responseJson = json.decode(responseStr); 20 | return responseJson; 21 | } 22 | 23 | Future mock2(String action) async { 24 | var responseStr = await rootBundle.loadString('mock/$action.json'); 25 | var responseJson = json.decode(responseStr); 26 | return responseJson; 27 | } 28 | 29 | Map map = { 30 | API.IN_THEATERS: 'in_theaters', 31 | API.COMING_SOON: 'coming_soon', 32 | API.TOP_250: 'top250', 33 | API.WEEKLY: 'weekly', 34 | API.REIVIEWS: 'reviews', 35 | }; 36 | 37 | getJsonName(String action) { 38 | return map[action]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:io'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:doubanapp/widgets/bottom_drag_widget.dart'; 5 | import 'package:doubanapp/pages/splash/splash_widget.dart'; 6 | 7 | void main() { 8 | runApp(MyApp()); 9 | if (Platform.isAndroid) { 10 | //设置Android头部的导航栏透明 11 | SystemUiOverlayStyle systemUiOverlayStyle = 12 | SystemUiOverlayStyle(statusBarColor: Colors.transparent); 13 | SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 14 | } 15 | } 16 | 17 | class MyApp extends StatelessWidget { 18 | // This widget is the root of your application. 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return RestartWidget( 23 | child: MaterialApp( 24 | theme: ThemeData(backgroundColor: Colors.white), 25 | home: Scaffold( 26 | body: SplashWidget(), 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | 33 | ///这个组件用来重新加载整个child Widget的。当我们需要重启APP的时候,可以使用这个方案 34 | ///https://stackoverflow.com/questions/50115311/flutter-how-to-force-an-application-restart-in-production-mode 35 | class RestartWidget extends StatefulWidget { 36 | final Widget child; 37 | 38 | RestartWidget({Key key, @required this.child}) 39 | : assert(child != null), 40 | super(key: key); 41 | 42 | static restartApp(BuildContext context) { 43 | final _RestartWidgetState state = 44 | context.findAncestorStateOfType<_RestartWidgetState>(); 45 | state.restartApp(); 46 | } 47 | 48 | @override 49 | _RestartWidgetState createState() => _RestartWidgetState(); 50 | } 51 | 52 | class _RestartWidgetState extends State { 53 | Key key = UniqueKey(); 54 | 55 | void restartApp() { 56 | setState(() { 57 | key = UniqueKey(); 58 | }); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Container( 64 | key: key, 65 | child: widget.child, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/pages/container_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/pages/group/group_page.dart'; 3 | import 'package:doubanapp/pages/movie/book_audio_video_page.dart'; 4 | import 'package:doubanapp/pages/home/home_page.dart'; 5 | import 'package:doubanapp/pages/person/person_center_page.dart'; 6 | import 'package:doubanapp/pages/shop_page.dart'; 7 | 8 | 9 | ///这个页面是作为整个APP的最外层的容器,以Tab为基础控制每个item的显示与隐藏 10 | class ContainerPage extends StatefulWidget { 11 | ContainerPage({Key key}) : super(key: key); 12 | 13 | @override 14 | State createState() { 15 | return _ContainerPageState(); 16 | } 17 | } 18 | 19 | class _Item { 20 | String name, activeIcon, normalIcon; 21 | 22 | _Item(this.name, this.activeIcon, this.normalIcon); 23 | } 24 | 25 | class _ContainerPageState extends State { 26 | final ShopPageWidget shopPageWidget = ShopPageWidget(); 27 | List pages; 28 | 29 | final defaultItemColor = Color.fromARGB(255, 125, 125, 125); 30 | 31 | final itemNames = [ 32 | _Item('首页', 'assets/images/ic_tab_home_active.png', 33 | 'assets/images/ic_tab_home_normal.png'), 34 | _Item('书影音', 'assets/images/ic_tab_subject_active.png', 35 | 'assets/images/ic_tab_subject_normal.png'), 36 | _Item('小组', 'assets/images/ic_tab_group_active.png', 37 | 'assets/images/ic_tab_group_normal.png'), 38 | _Item('市集', 'assets/images/ic_tab_shiji_active.png', 39 | 'assets/images/ic_tab_shiji_normal.png'), 40 | _Item('我的', 'assets/images/ic_tab_profile_active.png', 41 | 'assets/images/ic_tab_profile_normal.png') 42 | ]; 43 | 44 | List itemList; 45 | 46 | @override 47 | void initState() { 48 | super.initState(); 49 | print('initState _ContainerPageState'); 50 | if(pages == null){ 51 | pages = [ 52 | HomePage(), 53 | BookAudioVideoPage(), 54 | GroupPage(), 55 | shopPageWidget, 56 | PersonCenterPage() 57 | ]; 58 | } 59 | if(itemList == null){ 60 | itemList = itemNames 61 | .map((item) => BottomNavigationBarItem( 62 | icon: Image.asset( 63 | item.normalIcon, 64 | width: 30.0, 65 | height: 30.0, 66 | ), 67 | title: Text( 68 | item.name, 69 | style: TextStyle(fontSize: 10.0), 70 | ), 71 | activeIcon: 72 | Image.asset(item.activeIcon, width: 30.0, height: 30.0))) 73 | .toList(); 74 | } 75 | 76 | } 77 | 78 | 79 | int _selectIndex = 0; 80 | 81 | //Stack(层叠布局)+Offstage组合,解决状态被重置的问题 82 | Widget _getPagesWidget(int index) { 83 | return Offstage( 84 | offstage: _selectIndex != index, 85 | child: TickerMode( 86 | enabled: _selectIndex == index, 87 | child: pages[index], 88 | ), 89 | ); 90 | } 91 | 92 | 93 | @override 94 | void didUpdateWidget(ContainerPage oldWidget) { 95 | super.didUpdateWidget(oldWidget); 96 | print('didUpdateWidget'); 97 | } 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | // Scaffold({ 102 | // Key key, 103 | // this.appBar, 104 | // this.body, 105 | // this.floatingActionButton, 106 | // this.floatingActionButtonLocation, 107 | // this.floatingActionButtonAnimator, 108 | // this.persistentFooterButtons, 109 | // this.drawer, 110 | // this.endDrawer, 111 | // this.bottomNavigationBar, 112 | // this.bottomSheet, 113 | // this.backgroundColor, 114 | // this.resizeToAvoidBottomPadding = true, 115 | // this.primary = true, 116 | // }) 117 | print('build _ContainerPageState'); 118 | return Scaffold( 119 | body: new Stack( 120 | children: [ 121 | _getPagesWidget(0), 122 | _getPagesWidget(1), 123 | _getPagesWidget(2), 124 | _getPagesWidget(3), 125 | _getPagesWidget(4), 126 | ], 127 | ), 128 | // List 129 | // @required this.icon, 130 | // this.title, 131 | // Widget activeIcon, 132 | // this.backgroundColor, 133 | backgroundColor: Color.fromARGB(255, 248, 248, 248), 134 | bottomNavigationBar: BottomNavigationBar( 135 | items: itemList, 136 | onTap: (int index) { 137 | ///这里根据点击的index来显示,非index的page均隐藏 138 | setState(() { 139 | _selectIndex = index; 140 | //这个是用来控制比较特别的shopPage中WebView不能动态隐藏的问题 141 | shopPageWidget.setShowState(pages.indexOf(shopPageWidget) == _selectIndex); 142 | }); 143 | }, 144 | //图标大小 145 | iconSize: 24, 146 | //当前选中的索引 147 | currentIndex: _selectIndex, 148 | //选中后,底部BottomNavigationBar内容的颜色(选中时,默认为主题色)(仅当type: BottomNavigationBarType.fixed,时生效) 149 | fixedColor: Color.fromARGB(255, 0, 188, 96), 150 | type: BottomNavigationBarType.fixed, 151 | ), 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/pages/detail/detail_title_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/bean/movie_detail_bean.dart'; 3 | import 'package:doubanapp/constant/constant.dart'; 4 | import 'package:doubanapp/pages/detail/look_confirm_button.dart'; 5 | 6 | class DetailTitleWidget extends StatelessWidget { 7 | final MovieDetailBean bean; 8 | final Color shadowColor; 9 | 10 | DetailTitleWidget(this.bean, this.shadowColor, {Key key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | var screenW = MediaQuery.of(context).size.width; 15 | var imgW = screenW / 4; 16 | var imgH = imgW * 421 / 297; 17 | var countries = list2String(bean.countries); 18 | var genres = list2String(bean.genres); 19 | var pubdates = list2String(bean.pubdates); 20 | var durations = list2String(bean.durations); 21 | //将按下的颜色设置较为浅色 22 | var btnPressedColor = 23 | Color.fromARGB(100, shadowColor.red, shadowColor.red, shadowColor.red); 24 | return Row( 25 | children: [ 26 | Card( 27 | //影音海报 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.all(Radius.circular(6.0)), 30 | ), 31 | color: shadowColor, 32 | clipBehavior: Clip.antiAlias, 33 | elevation: 10.0, 34 | child: Image.network( 35 | bean.images.large, 36 | width: imgW, 37 | height: imgH, 38 | fit: BoxFit.cover, 39 | ), 40 | ), 41 | Expanded( 42 | child: Padding( 43 | padding: EdgeInsets.only(left: Constant.MARGIN_LEFT), 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.start, 46 | crossAxisAlignment: CrossAxisAlignment.start, 47 | children: [ 48 | Text( 49 | bean.title, 50 | style: TextStyle( 51 | fontSize: 22.0, 52 | color: Colors.white, 53 | fontWeight: FontWeight.bold), 54 | ), 55 | Padding( 56 | padding: EdgeInsets.only(top: 5.0, bottom: 7.0), 57 | child: Text( 58 | '(${bean.year})', 59 | style: TextStyle(fontSize: 15.0, color: Colors.white), 60 | ), 61 | ), 62 | Padding( 63 | padding: EdgeInsets.only(bottom: 5.0), 64 | child: Text( 65 | '$countries/$genres/上映时间:$pubdates/片长:$durations', 66 | style: TextStyle(fontSize: 12.0, color: Colors.white70), 67 | ), 68 | ), 69 | Row( 70 | children: [ 71 | Expanded( 72 | child: LookConfirmButton( 73 | btnText: '想看', 74 | iconAsset: 'assets/images/ic_info_wish.png', 75 | defaultColor: Colors.white, 76 | pressedColor: btnPressedColor, 77 | ), 78 | ), 79 | Padding( 80 | padding: EdgeInsets.only(left: 15.0), 81 | ), 82 | Expanded( 83 | child: LookConfirmButton( 84 | btnText: '看过', 85 | iconAsset: 'assets/images/ic_info_done.png', 86 | defaultColor: Colors.white, 87 | pressedColor: btnPressedColor, 88 | ), 89 | ) 90 | ], 91 | ), 92 | ], 93 | ), 94 | ), 95 | ) 96 | ], 97 | ); 98 | } 99 | 100 | String list2String(List list) { 101 | var tmp = ''; 102 | for (String item in list) { 103 | tmp = tmp + item; 104 | } 105 | return tmp; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/pages/detail/long_comment_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../bean/movie_long_comments_entity.dart'; 3 | import '../../widgets/rating_bar.dart'; 4 | import '../../constant/constant.dart'; 5 | import 'package:doubanapp/router.dart'; 6 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 7 | 8 | ///电影长评论 9 | class LongCommentWidget extends StatelessWidget { 10 | final MovieLongCommentsEntity movieLongCommentsEntity; 11 | 12 | LongCommentWidget({Key key, @required this.movieLongCommentsEntity}) 13 | : assert(movieLongCommentsEntity != null), 14 | super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return LongCommentTabView( 19 | movieLongCommentsEntity: movieLongCommentsEntity, 20 | ); 21 | } 22 | } 23 | 24 | class LongCommentTabView extends StatefulWidget { 25 | final MovieLongCommentsEntity movieLongCommentsEntity; 26 | 27 | LongCommentTabView({Key key, @required this.movieLongCommentsEntity}) 28 | : super(key: key); 29 | 30 | @override 31 | _LongCommentTabViewState createState() => _LongCommentTabViewState(); 32 | } 33 | 34 | class _LongCommentTabViewState extends State 35 | with SingleTickerProviderStateMixin { 36 | final List list = ['影评', '话题', '讨论']; 37 | 38 | TabController controller; 39 | Color selectColor, unselectedColor; 40 | TextStyle selectStyle, unselectedStyle; 41 | 42 | @override 43 | void initState() { 44 | controller = TabController(length: list.length, vsync: this); 45 | selectColor = Colors.black; 46 | unselectedColor = Color.fromARGB(255, 117, 117, 117); 47 | selectStyle = TextStyle(fontSize: 15, color: selectColor); 48 | unselectedStyle = TextStyle(fontSize: 15, color: selectColor); 49 | super.initState(); 50 | } 51 | 52 | @override 53 | void dispose() { 54 | controller.dispose(); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Column( 61 | children: [ 62 | Container( 63 | height: 6.0, 64 | width: 45.0, 65 | margin: const EdgeInsets.only(top: 10.0), 66 | decoration: BoxDecoration( 67 | color: const Color.fromARGB(255, 214, 215, 218), 68 | borderRadius: BorderRadius.all(const Radius.circular(5.0))), 69 | ), 70 | Container( 71 | padding: const EdgeInsets.only(top: 15.0), 72 | child: TabBar( 73 | tabs: list 74 | .map((item) => Padding( 75 | padding: 76 | const EdgeInsets.only(bottom: Constant.TAB_BOTTOM), 77 | child: Text(item), 78 | )) 79 | .toList(), 80 | isScrollable: true, 81 | indicatorColor: selectColor, 82 | labelColor: selectColor, 83 | labelStyle: selectStyle, 84 | unselectedLabelColor: unselectedColor, 85 | unselectedLabelStyle: unselectedStyle, 86 | indicatorSize: TabBarIndicatorSize.label, 87 | controller: controller, 88 | ), 89 | alignment: Alignment.centerLeft, 90 | ), 91 | Expanded( 92 | child: TabBarView( 93 | children: [ 94 | ListView.builder( 95 | itemBuilder: (BuildContext context, int index) { 96 | return Column( 97 | children: [ 98 | Container( 99 | child: getItem( 100 | widget.movieLongCommentsEntity.reviews[index]), 101 | padding: const EdgeInsets.only( 102 | left: Constant.MARGIN_LEFT, 103 | right: Constant.MARGIN_RIGHT), 104 | color: Colors.white, 105 | ), 106 | Container( 107 | height: 10.0, 108 | color: Colors.transparent, 109 | ) 110 | ], 111 | ); 112 | }, 113 | physics: const ClampingScrollPhysics(), 114 | itemCount: widget.movieLongCommentsEntity.reviews.length, 115 | ), 116 | Text('话题,暂无数据~'), 117 | Text('讨论,暂无数据~') 118 | ], 119 | controller: controller, 120 | )) 121 | ], 122 | ); 123 | } 124 | 125 | Widget getItem(MovieLongCommentReviews review) { 126 | return GestureDetector( 127 | behavior: HitTestBehavior.translucent, 128 | child: Column( 129 | mainAxisAlignment: MainAxisAlignment.start, 130 | crossAxisAlignment: CrossAxisAlignment.start, 131 | children: [ 132 | Row( 133 | mainAxisAlignment: MainAxisAlignment.start, 134 | children: [ 135 | Padding( 136 | padding: 137 | const EdgeInsets.only(top: 10.0, bottom: 7.0, right: 5.0), 138 | child: CircleAvatar( 139 | radius: 10.0, 140 | backgroundImage: NetworkImage(review.author.avatar), 141 | backgroundColor: Colors.white, 142 | ), 143 | ), 144 | Padding( 145 | child: Text(review.author.name), 146 | padding: const EdgeInsets.only(right: 5.0), 147 | ), 148 | RatingBar( 149 | ((review.rating.value * 1.0) / (review.rating.max * 1.0)) * 150 | 10.0, 151 | size: 11.0, 152 | fontSize: 0.0, 153 | ) 154 | ], 155 | ), 156 | Text( 157 | review.title, 158 | style: TextStyle( 159 | fontSize: 16.0, 160 | color: Colors.black, 161 | fontWeight: FontWeight.bold), 162 | ), 163 | Padding( 164 | padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), 165 | child: Text( 166 | review.content, 167 | softWrap: true, 168 | maxLines: 3, 169 | overflow: TextOverflow.ellipsis, 170 | style: TextStyle(fontSize: 14.0, color: Color(0xff333333)), 171 | ), 172 | ), 173 | Padding( 174 | child: Text( 175 | '${getUsefulCount(review.commentsCount)}回复 · ${getUsefulCount(review.usefulCount)} 有用'), 176 | padding: const EdgeInsets.only(bottom: 10.0), 177 | ), 178 | ], 179 | ), 180 | onTap: () { 181 | Navigator.push(context, MaterialPageRoute(builder: (context) { 182 | return WebviewScaffold( 183 | url: review.shareUrl, 184 | appBar: new AppBar( 185 | backgroundColor: Colors.green, 186 | title: Row( 187 | mainAxisAlignment: MainAxisAlignment.start, 188 | children: [ 189 | Padding( 190 | padding: const EdgeInsets.only( 191 | top: 10.0, bottom: 7.0, right: 5.0), 192 | child: CircleAvatar( 193 | radius: 10.0, 194 | backgroundImage: NetworkImage(review.author.avatar), 195 | backgroundColor: Colors.white, 196 | ), 197 | ), 198 | Padding( 199 | child: Text(review.author.name), 200 | padding: const EdgeInsets.only(right: 5.0), 201 | ), 202 | ], 203 | ), 204 | ), 205 | ); 206 | })); 207 | }, 208 | ); 209 | } 210 | 211 | ///将34123转成3.4k 212 | getUsefulCount(int usefulCount) { 213 | double a = usefulCount / 1000; 214 | if (a < 1.0) { 215 | return usefulCount; 216 | } else { 217 | return '${a.toStringAsFixed(1)}k'; //保留一位小数 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lib/pages/detail/look_confirm_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | typedef VoidCallback = void Function(); 5 | 6 | ///想看、看过的按钮 7 | class LookConfirmButton extends StatefulWidget { 8 | final String btnText; 9 | final String iconAsset; 10 | final Color pressedColor; 11 | final VoidCallback onPressed; 12 | final Color defaultColor; 13 | 14 | LookConfirmButton( 15 | {Key key, 16 | @required this.btnText, 17 | @required this.iconAsset, 18 | @required this.pressedColor, 19 | @required this.defaultColor, 20 | this.onPressed}) 21 | : super(key: key); 22 | 23 | @override 24 | State createState() { 25 | return _State(defaultColor); 26 | } 27 | } 28 | 29 | class _State extends State { 30 | var _color; 31 | Color _defaultColor; 32 | 33 | _State(Color color) { 34 | _color = color; 35 | _defaultColor = color; 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return GestureDetector( 41 | child: Container( 42 | alignment: Alignment.center, 43 | height: 35.0, 44 | decoration: BoxDecoration( 45 | color: _color, 46 | borderRadius: BorderRadius.all(Radius.circular(5.0))), 47 | child: Row( 48 | mainAxisAlignment: MainAxisAlignment.center, 49 | children: [ 50 | Padding( 51 | padding: EdgeInsets.only(right: 5.0), 52 | child: Image.asset( 53 | widget.iconAsset, 54 | width: 22.0, 55 | height: 22.0, 56 | ), 57 | ), 58 | Text( 59 | widget.btnText, 60 | style: TextStyle(fontSize: 17.0, color: Colors.black), 61 | ) 62 | ], 63 | ), 64 | ), 65 | onTap: () { 66 | if (widget.onPressed != null) { 67 | widget.onPressed(); 68 | } 69 | }, 70 | onTapDown: (TapDownDetails details) { 71 | setState(() { 72 | _color = widget.pressedColor; 73 | }); 74 | }, 75 | onTapUp: (TapUpDetails details) { 76 | setState(() { 77 | _color = _defaultColor; 78 | }); 79 | }, 80 | onTapCancel: ((){ 81 | setState(() { 82 | _color = _defaultColor; 83 | }); 84 | }), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/pages/detail/score_start.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/rating_bar.dart'; 3 | 4 | class ScoreStartWidget extends StatefulWidget { 5 | final score; 6 | final p5; //五颗星的百分比 7 | final p4; 8 | final p3; 9 | final p2; 10 | final p1; 11 | 12 | ScoreStartWidget( 13 | {Key key, 14 | @required this.score, 15 | @required this.p1, 16 | @required this.p2, 17 | @required this.p3, 18 | @required this.p4, 19 | @required this.p5}) 20 | : super(key: key); 21 | 22 | @override 23 | State createState() { 24 | return _ScoreStartState(); 25 | } 26 | } 27 | 28 | class _ScoreStartState extends State { 29 | var lineW; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | lineW = MediaQuery.of(context).size.width / 3; 34 | return Container( 35 | padding: EdgeInsets.all(13.0), 36 | decoration: BoxDecoration( 37 | color: Color(0x23000000), 38 | borderRadius: BorderRadius.all(Radius.circular(10.0))), 39 | child: Column( 40 | children: [ 41 | Row( 42 | children: [ 43 | Expanded( 44 | child: Text( 45 | '豆芽评分', 46 | style: TextStyle(fontSize: 12.0, color: Colors.white), 47 | )), 48 | Icon( 49 | Icons.chevron_right, 50 | color: Color(0x66ffffff), 51 | size: 26.0, 52 | ) 53 | ], 54 | ), 55 | Row( 56 | children: [ 57 | Padding( 58 | child: Column( 59 | mainAxisAlignment: MainAxisAlignment.start, 60 | //评分、星星 61 | children: [ 62 | Text( 63 | '${widget.score}', 64 | style: TextStyle(fontSize: 30.0, color: Colors.white), 65 | ), 66 | RatingBar( 67 | widget.score, 68 | size: 11.0, 69 | fontSize: 0.0, 70 | ) 71 | ], 72 | ), 73 | padding: EdgeInsets.only(left: 30.0, right: 10.0), 74 | ), 75 | Column( 76 | //星星-百分比 77 | mainAxisAlignment: MainAxisAlignment.end, 78 | crossAxisAlignment: CrossAxisAlignment.end, 79 | children: [ 80 | startsLine(5, widget.p5), 81 | startsLine(4, widget.p4), 82 | startsLine(3, widget.p3), 83 | startsLine(2, widget.p2), 84 | startsLine(1, widget.p1), 85 | ], 86 | ), 87 | ], 88 | ) 89 | ], 90 | ), 91 | ); 92 | } 93 | 94 | Widget getStarts(int count) { 95 | List list = []; 96 | for (int i = 0; i < count; i++) { 97 | list.add(Icon( 98 | Icons.star, 99 | size: 9.0, 100 | color: Colors.white70, 101 | )); 102 | } 103 | return Row( 104 | children: list, 105 | ); 106 | } 107 | 108 | ///percent 百分比(0.1 -1.0) 109 | Widget getLine(double percent) { 110 | return Stack( 111 | children: [ 112 | Container( 113 | width: lineW, 114 | height: 7.0, 115 | decoration: BoxDecoration( 116 | color: Color(0x13000000), 117 | borderRadius: BorderRadius.all(Radius.circular(10.0))), 118 | ), 119 | Container( 120 | height: 7.0, 121 | width: lineW * percent, 122 | decoration: BoxDecoration( 123 | color: Color.fromARGB(255, 255, 170, 71), 124 | borderRadius: BorderRadius.all(Radius.circular(10.0))), 125 | ) 126 | ], 127 | ); 128 | } 129 | 130 | startsLine(int startCount, double percent) { 131 | if(percent == null || percent.isNaN){ 132 | percent = 0.0; 133 | } 134 | return Padding( 135 | padding: EdgeInsets.only(bottom: 2.0), 136 | child: Row( 137 | children: [ 138 | getStarts(startCount), 139 | Padding( 140 | padding: EdgeInsets.only(left: 5.0), 141 | ), 142 | getLine(percent) 143 | ], 144 | ), 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/pages/detail/summary_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SummaryWidget extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | // TODO: implement build 7 | return null; 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /lib/pages/detail/tags_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TagsWidget extends StatelessWidget { 4 | final List tags; 5 | 6 | TagsWidget({Key key, @required this.tags}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/pages/group/group_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/search_text_field_widget.dart'; 3 | import 'package:doubanapp/router.dart'; 4 | import 'package:doubanapp/constant/constant.dart'; 5 | import 'package:doubanapp/http/API.dart'; 6 | import 'package:doubanapp/http/http_request.dart'; 7 | import 'package:doubanapp/bean/subject_entity.dart'; 8 | import 'package:doubanapp/widgets/loading_widget.dart'; 9 | import 'package:doubanapp/widgets/image/radius_img.dart'; 10 | ///小组 11 | class GroupPage extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | String hintText = '搜索书影音 小组 日记 用户等'; 15 | return Scaffold( 16 | backgroundColor: Colors.white, 17 | body: SafeArea( 18 | child: Column( 19 | children: [ 20 | SearchTextFieldWidget( 21 | margin: EdgeInsets.all(Constant.MARGIN_RIGHT), 22 | hintText: hintText, 23 | onTab: () { 24 | MyRouter.push(context, MyRouter.searchPage, hintText); 25 | }, 26 | ), 27 | Expanded( 28 | child: _GroupWidget(), 29 | ) 30 | ], 31 | )), 32 | ); 33 | } 34 | } 35 | 36 | class _GroupWidget extends StatefulWidget { 37 | @override 38 | State createState() => _GroupWidgetState(); 39 | } 40 | 41 | var _request = HttpRequest(API.BASE_URL); 42 | 43 | class _GroupWidgetState extends State<_GroupWidget> { 44 | List list; 45 | bool loading = true; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | Future(() { 51 | return _request.get(API.IN_THEATERS); 52 | }).then((result) { 53 | var resultList = result['subjects']; 54 | setState(() { 55 | list = 56 | resultList.map((item) => Subject.fromMap(item)).toList(); 57 | loading = false; 58 | }); 59 | }); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | return LoadingWidget.containerLoadingBody(_getBody(), loading: loading); 65 | } 66 | 67 | Widget _getBody() { 68 | if (list == null) { 69 | return Container( 70 | child: Image.asset(Constant.ASSETS_IMG + 'ic_group_top.png'), 71 | ); 72 | } 73 | return ListView.builder( 74 | physics: const BouncingScrollPhysics(), 75 | itemBuilder: (BuildContext context, int index) { 76 | if (index == 0) { 77 | return Image.asset(Constant.ASSETS_IMG + 'ic_group_top.png'); 78 | } 79 | 80 | Subject bean = list[index - 1]; 81 | return Padding( 82 | padding: const EdgeInsets.only( 83 | right: Constant.MARGIN_RIGHT, left: 6.0, top: 13.0), 84 | child: _getItem(bean, index - 1), 85 | ); 86 | }, 87 | itemCount: list.length + 1, 88 | ); 89 | } 90 | 91 | Widget _getItem(Subject bean, int index) { 92 | return GestureDetector( 93 | behavior: HitTestBehavior.translucent, 94 | child: Row( 95 | children: [ 96 | RadiusImg.get(bean.images.small, 50.0, radius: 3.0), 97 | Expanded( 98 | child: Container( 99 | alignment: Alignment.topLeft, 100 | margin: const EdgeInsets.only(left: 5.0), 101 | child: Column( 102 | crossAxisAlignment: CrossAxisAlignment.start, 103 | children: [ 104 | Text( 105 | bean.title, 106 | style: 107 | TextStyle(fontSize: 17.0, fontWeight: FontWeight.bold), 108 | ), 109 | Text(bean.pubdates != null ? bean.pubdates[0] : '', style: TextStyle(fontSize: 13.0)) 110 | ], 111 | ), 112 | ), 113 | ), 114 | Padding( 115 | padding: EdgeInsets.only(right: 10.0), 116 | child: Text('${bean.collect_count}人', style: TextStyle(fontSize: 13.0),), 117 | ), 118 | GestureDetector( 119 | child: Image.asset( 120 | Constant.ASSETS_IMG + 121 | (list[index].tag 122 | ? 'ic_group_checked_anonymous.png' 123 | : 'ic_group_check_anonymous.png'), 124 | width: 25.0, 125 | height: 25.0, 126 | ), 127 | onTap: () { 128 | setState(() { 129 | list[index].tag = !list[index].tag; 130 | }); 131 | }, 132 | ) 133 | ], 134 | ), 135 | onTap: () { 136 | MyRouter.push(context, MyRouter.detailPage, bean.id); 137 | }, 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/pages/home/my_home_tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/search_text_field_widget.dart'; 3 | import 'package:doubanapp/util/screen_utils.dart'; 4 | import 'package:doubanapp/router.dart'; 5 | 6 | const double _kTabHeight = 46.0; 7 | const double _kTextAndIconTabHeight = 42.0; 8 | 9 | class HomeTabBar extends StatefulWidget implements PreferredSizeWidget { 10 | final TabBar tabBar; 11 | final double translate; 12 | 13 | HomeTabBar({Key key, this.tabBar, this.translate}) : super(key: key); 14 | 15 | @override 16 | _HomeTabBarState createState() => _HomeTabBarState(); 17 | 18 | @override 19 | Size get preferredSize { 20 | print('preferredSize'); 21 | for (Widget item in tabBar.tabs) { 22 | if (item is Tab) { 23 | final Tab tab = item; 24 | if (tab.text != null && tab.icon != null) 25 | return Size.fromHeight( 26 | _kTextAndIconTabHeight + tabBar.indicatorWeight); 27 | } 28 | } 29 | return Size.fromHeight(_kTabHeight + tabBar.indicatorWeight); 30 | } 31 | } 32 | 33 | class _HomeTabBarState extends State { 34 | double get allHeight => widget.preferredSize.height; 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | MediaQuery.of(context); 39 | var value = ScreenUtils.screenW(context) * 0.75 - 10.0; 40 | return Stack( 41 | children: [ 42 | Positioned( 43 | ///搜索框 44 | left: 15.0, 45 | right: value, 46 | top: getTop(widget.translate), 47 | child: getOpacityWidget(Container( 48 | padding: const EdgeInsets.only( 49 | top: 3.0, bottom: 3.0, right: 10.0, left: 5.0), 50 | decoration: BoxDecoration( 51 | color: const Color.fromARGB(245, 236, 236, 236), 52 | borderRadius: BorderRadius.all(Radius.circular(17.0))), 53 | child: Row( 54 | children: [ 55 | Icon( 56 | Icons.search, 57 | color: const Color.fromARGB(255, 128, 128, 129), 58 | ), 59 | Expanded( 60 | child: GestureDetector( 61 | child: Align( 62 | alignment: Alignment(1.0, 0.0), 63 | child: Text( 64 | '搜索', 65 | style: TextStyle( 66 | fontSize: 16.0, 67 | color: const Color.fromARGB(255, 192, 192, 192)), 68 | ), 69 | ), 70 | onTap: () { 71 | MyRouter.push(context, MyRouter.searchPage, '搜索流浪地球试一试'); 72 | }, 73 | ), 74 | ) 75 | ], 76 | ), 77 | )), 78 | ), 79 | Padding( 80 | padding: const EdgeInsets.only(bottom: 5.0), 81 | child: Row( 82 | children: [ 83 | Expanded( 84 | flex: 1, 85 | child: Container(), 86 | ), 87 | Expanded( 88 | flex: 3, 89 | child: widget.tabBar, 90 | ), 91 | Expanded( 92 | flex: 1, 93 | child: Container(), 94 | ), 95 | ], 96 | ), 97 | ) 98 | ], 99 | ); 100 | } 101 | 102 | double getTop(double translate) { 103 | return Tween(begin: allHeight, end: 0.0) 104 | .transform(widget.translate); 105 | } 106 | 107 | Widget getOpacityWidget(Widget child) { 108 | if (widget.translate == 1) { 109 | return child; 110 | } 111 | return Opacity( 112 | opacity: const Interval(0.0, 1.0, curve: Curves.fastOutSlowIn) 113 | .transform(widget.translate), 114 | child: child, 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/pages/movie/book_audio_video_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'dart:math' as math; 4 | import 'package:doubanapp/widgets/my_tab_bar_widget.dart'; 5 | import 'package:doubanapp/widgets/search_text_field_widget.dart'; 6 | import 'package:doubanapp/router.dart'; 7 | 8 | var titleList = ['电影', '电视', '综艺', '读书', '音乐', '同城']; 9 | 10 | List tabList; 11 | 12 | ///书影音 13 | ///包含了'电影', '电视', '综艺', '读书', '音乐', '同城' item Widget 14 | ///这个Widget是整个项目中,十分复杂的Widget之一 15 | /// 16 | class BookAudioVideoPage extends StatefulWidget { 17 | @override 18 | State createState() { 19 | return _BookAudioVideoPageState(); 20 | } 21 | } 22 | 23 | TabController _tabController; 24 | 25 | class _BookAudioVideoPageState extends State 26 | with SingleTickerProviderStateMixin { 27 | var tabBar; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | tabBar = HomePageTabBar(); 33 | tabList = getTabList(); 34 | _tabController = TabController(vsync: this, length: tabList.length); 35 | } 36 | 37 | List getTabList() { 38 | return titleList 39 | .map((item) => Text( 40 | '$item', 41 | style: TextStyle(fontSize: 15), 42 | )) 43 | .toList(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Container( 49 | color: Colors.white, 50 | child: SafeArea( 51 | child: DefaultTabController( 52 | length: titleList.length, child: _getNestedScrollView(tabBar))), 53 | ); 54 | } 55 | } 56 | 57 | Widget _getNestedScrollView(Widget tabBar) { 58 | String hintText = '用一部电影来形容你的2018'; 59 | return NestedScrollView( 60 | headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { 61 | return [ 62 | SliverToBoxAdapter( 63 | child: Container( 64 | color: Colors.white, 65 | padding: const EdgeInsets.all(10.0), 66 | child: SearchTextFieldWidget( 67 | hintText: hintText, 68 | onTab: () { 69 | MyRouter.push(context, MyRouter.searchPage, hintText); 70 | }, 71 | ), 72 | ), 73 | ), 74 | SliverPersistentHeader( 75 | floating: true, 76 | pinned: true, 77 | delegate: _SliverAppBarDelegate( 78 | maxHeight: 49.0, 79 | minHeight: 49.0, 80 | child: Container( 81 | color: Colors.white, 82 | child: tabBar, 83 | ))) 84 | ]; 85 | }, 86 | body: FlutterTabBarView( 87 | tabController: _tabController, 88 | )); 89 | } 90 | 91 | class HomePageTabBar extends StatefulWidget { 92 | HomePageTabBar({Key key}) : super(key: key); 93 | 94 | @override 95 | State createState() { 96 | return _HomePageTabBarState(); 97 | } 98 | } 99 | 100 | class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { 101 | _SliverAppBarDelegate({ 102 | @required this.minHeight, 103 | @required this.maxHeight, 104 | @required this.child, 105 | }); 106 | 107 | final double minHeight; 108 | final double maxHeight; 109 | final Widget child; 110 | 111 | @override 112 | double get minExtent => minHeight; 113 | 114 | @override 115 | double get maxExtent => math.max((minHeight ?? kToolbarHeight), minExtent); 116 | 117 | @override 118 | Widget build( 119 | BuildContext context, double shrinkOffset, bool overlapsContent) { 120 | return child; 121 | } 122 | 123 | @override 124 | bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { 125 | return maxHeight != oldDelegate.maxHeight || 126 | minHeight != oldDelegate.minHeight || 127 | child != oldDelegate.child; 128 | } 129 | } 130 | 131 | class _HomePageTabBarState extends State { 132 | Color selectColor, unselectedColor; 133 | TextStyle selectStyle, unselectedStyle; 134 | 135 | @override 136 | void initState() { 137 | super.initState(); 138 | selectColor = Colors.black; 139 | unselectedColor = Color.fromARGB(255, 117, 117, 117); 140 | selectStyle = TextStyle(fontSize: 18, color: selectColor); 141 | unselectedStyle = TextStyle(fontSize: 18, color: selectColor); 142 | } 143 | 144 | @override 145 | void dispose() { 146 | _tabController.dispose(); 147 | super.dispose(); 148 | } 149 | 150 | @override 151 | Widget build(BuildContext context) { 152 | //Tab小部件列表 153 | // List @required this.tabs, 154 | //组件选中以及动画的状态 155 | // TabController this.controller, 156 | //Tab是否可滑动(false->整个tab会把宽度填满,true-> tab包裹) 157 | // bool this.isScrollable = false, 158 | //选项卡下方的导航条的颜色 159 | // Color this.indicatorColor, 160 | //选项卡下方的导航条的线条粗细 161 | // double this.indicatorWeight = 2.0, 162 | // EdgeInsetsGeometry this.indicatorPadding = EdgeInsets.zero, 163 | // Decoration this.indicator, 164 | // TabBarIndicatorSize this.indicatorSize,导航条的长度,(tab:默认等分;label:跟标签长度一致) 165 | // Color this.labelColor,所选标签标签的颜色 166 | // TextStyle this.labelStyle,所选标签标签的文本样式 167 | // EdgeInsetsGeometry this.labelPadding,,所选标签标签的内边距 168 | // Color this.unselectedLabelColor,未选定标签标签的颜色 169 | // TextStyle this.unselectedLabelStyle,未选中标签标签的文字样式 170 | // void Function(T value) this.onTap,按下时的响应事件 171 | 172 | return Container( 173 | margin: EdgeInsets.only(top: 10.0, bottom: 10.0), 174 | child: TabBar( 175 | tabs: tabList, 176 | isScrollable: true, 177 | controller: _tabController, 178 | indicatorColor: selectColor, 179 | labelColor: selectColor, 180 | labelStyle: selectStyle, 181 | unselectedLabelColor: unselectedColor, 182 | unselectedLabelStyle: unselectedStyle, 183 | indicatorSize: TabBarIndicatorSize.label, 184 | ), 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lib/pages/movie/hot_soon_movie_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/bean/subject_entity.dart'; 3 | import 'package:doubanapp/widgets/subject_mark_image_widget.dart'; 4 | import '../../constant/constant.dart'; 5 | ///影院热映、即将上映 6 | class HotSoonMovieWidget extends StatefulWidget { 7 | final state = _HotSoonMovieWidgetState(); 8 | 9 | @override 10 | State createState() { 11 | return state; 12 | } 13 | 14 | ///设置影院热映数据 15 | void setHotMovieBeanList(List list) { 16 | state.setHotMovieBeanList(list); 17 | } 18 | } 19 | 20 | TabController _tabController; 21 | var movieCount = 16; 22 | 23 | class _HotSoonMovieWidgetState extends State 24 | with SingleTickerProviderStateMixin { 25 | Color selectColor, unselectedColor; 26 | TextStyle selectStyle, unselectedStyle; 27 | Widget tabBar; 28 | double childAspectRatio = 355.0 / 506.0; 29 | var hotCount, soonCount; //热映数量、即将上映数量、 30 | List hotMovieBeans, soonMovieBeans; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | selectColor = Color.fromARGB(255, 45, 45, 45); 36 | unselectedColor = Color.fromARGB(255, 135, 135, 135); 37 | selectStyle = TextStyle( 38 | fontSize: 20, color: selectColor, fontWeight: FontWeight.bold); 39 | unselectedStyle = TextStyle(fontSize: 20, color: unselectedColor); 40 | _tabController = TabController(vsync: this, length: 2); 41 | _tabController.addListener(listener); 42 | tabBar = TabBar( 43 | tabs: [ 44 | Padding( 45 | padding: const EdgeInsets.only(bottom: Constant.TAB_BOTTOM), 46 | child: Text('影院热映'), 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.only(bottom: Constant.TAB_BOTTOM), 50 | child: Text('即将上映'), 51 | ) 52 | ], 53 | indicatorColor: selectColor, 54 | labelColor: selectColor, 55 | labelStyle: selectStyle, 56 | unselectedLabelColor: unselectedColor, 57 | unselectedLabelStyle: unselectedStyle, 58 | indicatorSize: TabBarIndicatorSize.label, 59 | controller: _tabController, 60 | isScrollable: true, 61 | // onTap: (index) { 62 | // setState(() { 63 | // if (index == 0) { 64 | // movieCount = hotCount; 65 | // } else { 66 | // movieCount = 20; 67 | // } 68 | // }); 69 | // }, 70 | ); 71 | } 72 | 73 | void listener() { 74 | if (_tabController.indexIsChanging) { 75 | var index = _tabController.index; 76 | print("HotSoonMovieWidget index changing=$index"); 77 | setState(() { 78 | if (index == 0) { 79 | movieCount = hotCount; 80 | } else { 81 | movieCount = 20; 82 | } 83 | }); 84 | } 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | return Column( 90 | children: [ 91 | Row( 92 | children: [ 93 | Expanded( 94 | child: tabBar, 95 | flex: 1, 96 | ), 97 | Text( 98 | '全部 $movieCount > ', 99 | style: TextStyle( 100 | fontSize: 12, 101 | color: Colors.black, 102 | fontWeight: FontWeight.bold), 103 | ) 104 | ], 105 | ), 106 | // GridView.builder( 107 | // gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 108 | // crossAxisCount: 3, 109 | // crossAxisSpacing: 10.0, 110 | // mainAxisSpacing: 10.0, 111 | // childAspectRatio: childAspectRatio), 112 | // //Widget Function(BuildContext context, int index); 113 | // itemBuilder: (BuildContext context, int index) { 114 | // return SubjectMarkImageWidget(hotMovieBeans[index].images.large); 115 | // }) 116 | ], 117 | ); 118 | } 119 | 120 | @override 121 | void dispose() { 122 | _tabController.removeListener(listener); 123 | _tabController.dispose(); 124 | super.dispose(); 125 | } 126 | 127 | void setHotMovieBeanList(List list) { 128 | if (list != null) { 129 | setState(() { 130 | hotMovieBeans = list; 131 | hotCount = hotMovieBeans.length; 132 | }); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/pages/movie/hot_soon_tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/constant/text_size_constant.dart'; 3 | import 'package:doubanapp/constant/color_constant.dart'; 4 | import '../../constant/constant.dart'; 5 | typedef TabCallBack = void Function(int index); 6 | //影院热映、即将上映 tab 7 | class HotSoonTabBar extends StatefulWidget { 8 | final state = _HotSoonTabBarState(); 9 | 10 | HotSoonTabBar({Key key, TabCallBack onTabCallBack}) : super(key: key) { 11 | state.setTabCallBack(onTabCallBack); 12 | } 13 | 14 | @override 15 | State createState() { 16 | return state; 17 | } 18 | 19 | void setCount(List list) { 20 | state.setCount(list.length); 21 | } 22 | 23 | void setComingSoon(List list) { 24 | state.setComingSoonCount(list.length); 25 | } 26 | } 27 | 28 | class _HotSoonTabBarState extends State 29 | with SingleTickerProviderStateMixin { 30 | int movieCount = 0; 31 | Color selectColor, unselectedColor; 32 | TextStyle selectStyle, unselectedStyle; 33 | Widget tabBar; 34 | TabController _tabController; 35 | var hotCount, soonCount; //热映数量、即将上映数量、 36 | TabCallBack onTabCallBack; 37 | int comingSoonCount = 0; 38 | int selectIndex = 0; 39 | 40 | 41 | 42 | @override 43 | void initState() { 44 | super.initState(); 45 | selectColor = ColorConstant.colorDefaultTitle; 46 | unselectedColor = Color.fromARGB(255, 135, 135, 135); 47 | selectStyle = TextStyle( 48 | fontSize: TextSizeConstant.BookAudioPartTabBar, 49 | color: selectColor, 50 | fontWeight: FontWeight.bold); 51 | unselectedStyle = TextStyle( 52 | fontSize: TextSizeConstant.BookAudioPartTabBar, color: unselectedColor); 53 | _tabController = TabController(vsync: this, length: 2); 54 | _tabController.addListener(listener); 55 | tabBar = TabBar( 56 | tabs: [Padding( 57 | padding: const EdgeInsets.only(bottom: Constant.TAB_BOTTOM), 58 | child: Text('影院热映'), 59 | ), 60 | Padding( 61 | padding: const EdgeInsets.only(bottom: Constant.TAB_BOTTOM), 62 | child: Text('即将上映'), 63 | )], 64 | indicatorColor: selectColor, 65 | labelColor: selectColor, 66 | labelStyle: selectStyle, 67 | unselectedLabelColor: unselectedColor, 68 | unselectedLabelStyle: unselectedStyle, 69 | indicatorSize: TabBarIndicatorSize.label, 70 | controller: _tabController, 71 | isScrollable: true, 72 | // onTap: (index) { 73 | // selectIndex = index; 74 | // setState(() { 75 | // if (index == 0) { 76 | // movieCount = hotCount; 77 | // } else { 78 | // movieCount = comingSoonCount; 79 | // } 80 | // if (onTabCallBack != null) { 81 | // onTabCallBack(index); 82 | // } 83 | // }); 84 | // }, 85 | ); 86 | } 87 | 88 | 89 | void listener() { 90 | if(_tabController.indexIsChanging){ 91 | var index = _tabController.index; 92 | print("HotSoonTabBar index changing=$index"); 93 | selectIndex = index; 94 | setState(() { 95 | if (index == 0) { 96 | movieCount = hotCount; 97 | } else { 98 | movieCount = comingSoonCount; 99 | } 100 | if (onTabCallBack != null) { 101 | onTabCallBack(index); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | 108 | 109 | @override 110 | void dispose() { 111 | _tabController.removeListener(listener); 112 | _tabController.dispose(); 113 | super.dispose(); 114 | } 115 | 116 | @override 117 | Widget build(BuildContext context) { 118 | return Row( 119 | children: [ 120 | Expanded( 121 | child: tabBar, 122 | flex: 1, 123 | ), 124 | Text( 125 | '全部 $movieCount > ', 126 | style: TextStyle( 127 | fontSize: 12, color: Colors.black, fontWeight: FontWeight.bold), 128 | ) 129 | ], 130 | ); 131 | } 132 | 133 | ///影院热映数量 134 | void setCount(int count) { 135 | setState(() { 136 | this.hotCount = count; 137 | if (selectIndex == 0) { 138 | setState(() { 139 | movieCount = hotCount; 140 | }); 141 | } 142 | }); 143 | } 144 | 145 | ///即将上映数量 146 | void setComingSoonCount(int length) { 147 | setState(() { 148 | this.comingSoonCount = length; 149 | if (selectIndex == 1) { 150 | setState(() { 151 | movieCount = comingSoonCount; 152 | }); 153 | } 154 | }); 155 | } 156 | 157 | void setTabCallBack(TabCallBack onTabCallBack) { 158 | this.onTabCallBack = onTabCallBack; 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /lib/pages/movie/movie_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///院线电影 4 | class MovieListPage extends StatelessWidget { 5 | 6 | MovieListPage({Key key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/pages/movie/title_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/router.dart'; 3 | 4 | typedef TapCallback = void Function(); 5 | 6 | ///《书影业》顶部四个TAB 7 | class TitleWidget extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Row( 11 | mainAxisAlignment: MainAxisAlignment.spaceAround, 12 | children: [ 13 | _TextImgWidget( 14 | '找电影', 15 | 'assets/images/find_movie.png', 16 | tabCallBack: () { 17 | print('点击找电影'); 18 | MyRouter.push(context, MyRouter.searchPage, '找电影'); 19 | }, 20 | ), 21 | _TextImgWidget( 22 | '豆瓣榜单', 23 | 'assets/images/douban_top.png', 24 | tabCallBack: () { 25 | print('点击豆瓣榜单'); 26 | MyRouter.push(context, MyRouter.searchPage, '豆瓣榜单'); 27 | }, 28 | ), 29 | _TextImgWidget( 30 | '豆瓣猜', 31 | 'assets/images/douban_guess.png', 32 | tabCallBack: () { 33 | MyRouter.push(context, MyRouter.searchPage, '豆瓣猜'); 34 | }, 35 | ), 36 | _TextImgWidget( 37 | '豆瓣片单', 38 | 'assets/images/douban_film_list.png', 39 | tabCallBack: () { 40 | MyRouter.push(context, MyRouter.searchPage, '豆瓣片单'); 41 | }, 42 | ) 43 | ], 44 | ); 45 | } 46 | } 47 | 48 | class _TextImgWidget extends StatelessWidget { 49 | final String text; 50 | final String imgAsset; 51 | final TapCallback tabCallBack; 52 | 53 | _TextImgWidget( 54 | this.text, 55 | this.imgAsset, { 56 | Key key, 57 | this.tabCallBack, 58 | }) : super(key: key); 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | return GestureDetector( 63 | onTap: () { 64 | if (tabCallBack != null) { 65 | tabCallBack(); 66 | } 67 | }, 68 | child: Column( 69 | children: [ 70 | Image.asset( 71 | imgAsset, 72 | width: 45, 73 | height: 45, 74 | ), 75 | Text( 76 | text, 77 | style: TextStyle( 78 | fontSize: 13, 79 | color: Color.fromARGB( 80 | 255, 81 | 128, 82 | 128, 83 | 128, 84 | )), 85 | ) 86 | ], 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/pages/movie/today_play_movie_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/image/LaminatedImage.dart'; 3 | 4 | ///今日可播放电影已更新 Widget 5 | class TodayPlayMovieWidget extends StatelessWidget { 6 | final urls; 7 | final backgroundColor; 8 | TodayPlayMovieWidget(this.urls, {Key key, this.backgroundColor}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | 13 | if(urls == null || (urls.isEmpty)){ 14 | return Container(); 15 | } 16 | return Stack( 17 | alignment: AlignmentDirectional(1.0, 1.0), 18 | children: [ 19 | Stack( 20 | alignment: AlignmentDirectional.bottomStart, 21 | children: [ 22 | Container( 23 | height: 120.0, 24 | width: double.infinity, 25 | decoration: BoxDecoration( 26 | color: backgroundColor == null ? Color.fromARGB(255, 47, 22, 74) : backgroundColor, 27 | shape: BoxShape.rectangle, 28 | borderRadius: BorderRadius.all(Radius.circular(4.0))), 29 | ), 30 | Container( 31 | height: 140.0, 32 | margin: EdgeInsets.only(left: 13.0, bottom: 14.0), 33 | child: Row( 34 | children: [ 35 | Stack( 36 | alignment: Alignment.centerLeft, 37 | children: [ 38 | LaminatedImage(urls: urls, w: 90.0), 39 | Positioned( 40 | left: 90.0 / 3, 41 | child: Image.asset( 42 | 'assets/images/ic_action_playable_video_s.png', 43 | width: 30.0, 44 | height: 30.0, 45 | ), 46 | ) 47 | ], 48 | ), 49 | Expanded( 50 | child: Padding( 51 | padding: EdgeInsets.only(top: 40.0, left: 20.0), 52 | child: Column( 53 | mainAxisAlignment: MainAxisAlignment.center, 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | children: [ 56 | Text( 57 | '今日可播放电影已更新', 58 | style: TextStyle(fontSize: 15, color: Colors.white), 59 | ), 60 | Padding( 61 | padding: EdgeInsets.only(top: 6.0), 62 | child: Text( 63 | '全部 30 > ', 64 | style: 65 | TextStyle(fontSize: 13, color: Colors.white), 66 | ), 67 | ) 68 | ], 69 | ), 70 | ), 71 | ) 72 | ], 73 | ), 74 | ), 75 | ], 76 | ), 77 | Row( 78 | mainAxisAlignment: MainAxisAlignment.end, 79 | children: [ 80 | Padding( 81 | padding: EdgeInsets.only(bottom: 10.0), 82 | child: Image.asset( 83 | 'assets/images/sofa.png', 84 | width: 15.0, 85 | height: 15.0, 86 | ), 87 | ), 88 | Padding( 89 | padding: EdgeInsets.only(bottom: 10.0, right: 10.0, left: 5.0), 90 | child: Text( 91 | '看电影', 92 | style: TextStyle(fontSize: 11, color: Colors.white), 93 | ), 94 | ) 95 | ], 96 | ) 97 | ], 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/pages/movie/tv_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TVPage extends StatefulWidget { 4 | @override 5 | State createState() { 6 | return _TVPageState(); 7 | } 8 | } 9 | 10 | class _TVPageState extends State{ 11 | @override 12 | Widget build(BuildContext context) { 13 | return null; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /lib/pages/photo_hero_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/title_bar.dart'; 3 | import 'package:doubanapp/router.dart'; 4 | 5 | class PhotoHeroPage extends StatelessWidget { 6 | final String photoUrl; 7 | final double width; 8 | 9 | PhotoHeroPage({Key key, this.photoUrl, this.width}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | color: Colors.transparent, 15 | alignment: Alignment.center, 16 | child: _PhotoHero( 17 | photoUrl: photoUrl, 18 | width: width, 19 | onTap: () { 20 | Navigator.of(context).pop(); 21 | }, 22 | ), 23 | ); 24 | } 25 | } 26 | 27 | class _PhotoHero extends StatelessWidget { 28 | const _PhotoHero({Key key, this.photoUrl, this.onTap, this.width}) 29 | : super(key: key); 30 | final double width; 31 | final String photoUrl; 32 | final VoidCallback onTap; 33 | 34 | Widget build(BuildContext context) { 35 | return SizedBox( 36 | width: width, 37 | child: Hero( 38 | tag: photoUrl, 39 | child: Material( 40 | color: Colors.transparent, 41 | child: InkWell( 42 | onTap: onTap, 43 | child: Image.network( 44 | photoUrl, 45 | fit: BoxFit.contain, 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | // 54 | //class HeroAnimation extends StatelessWidget { 55 | // final String url; 56 | // 57 | // HeroAnimation(this.url, {Key key}) : super(key: key); 58 | // 59 | // Widget build(BuildContext context) { 60 | // return Scaffold( 61 | // appBar: AppBar( 62 | // title: const Text('Basic Hero Animation'), 63 | // ), 64 | // body: Center( 65 | // child: PhotoHero( 66 | // photo: url, 67 | // width: 300.0, 68 | // onTap: () { 69 | // Router.push( 70 | // context, Router.photoHero, {'photoUrl': url, 'width': 100.0}); 71 | // }), 72 | // ), 73 | // ); 74 | // } 75 | //} 76 | // 77 | //class PhotoHero extends StatelessWidget { 78 | // const PhotoHero({Key key, this.photo, this.onTap, this.width}) 79 | // : super(key: key); 80 | // 81 | // final String photo; 82 | // final VoidCallback onTap; 83 | // final double width; 84 | // 85 | // Widget build(BuildContext context) { 86 | // return SizedBox( 87 | // width: width, 88 | // child: Hero( 89 | // tag: photo, 90 | // child: Material( 91 | // color: Colors.transparent, 92 | // child: InkWell( 93 | // onTap: onTap, 94 | // child: ListView.builder(itemBuilder: (BuildContext context, int index){ 95 | // return Image.network( 96 | // photo, 97 | // fit: BoxFit.contain, 98 | // ); 99 | // }, itemCount: 20,), 100 | // ), 101 | // ), 102 | // ), 103 | // ); 104 | // } 105 | //} 106 | -------------------------------------------------------------------------------- /lib/pages/search/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/widgets/search_text_field_widget.dart'; 3 | import 'package:doubanapp/http/API.dart'; 4 | import 'package:doubanapp/bean/search_result_entity.dart'; 5 | //import 'package:doubanapp/widgets/image/cached_network_image.dart'; 6 | import 'package:doubanapp/router.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | 9 | ///搜索 10 | class SearchPage extends StatefulWidget { 11 | final String searchHintContent; 12 | 13 | ///搜索框中的默认显示内容 14 | SearchPage({Key key, this.searchHintContent = '用一部电影来形容你的2018'}) 15 | : super(key: key); 16 | 17 | @override 18 | State createState() => _SearchPageState(); 19 | } 20 | 21 | class _SearchPageState extends State { 22 | final API _api = API(); 23 | SearchResultEntity _searchResultEntity; 24 | var imgW; 25 | var imgH; 26 | bool showLoading = false; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | if (imgW == null) { 31 | imgW = MediaQuery.of(context).size.width / 7; 32 | imgH = imgW / 0.75; 33 | } 34 | if (_searchResultEntity != null && 35 | _searchResultEntity.subjects.isNotEmpty) { 36 | _searchResultEntity.subjects.sort((a, b) => (b.year.compareTo(a.year))); 37 | } 38 | return Scaffold( 39 | body: SafeArea( 40 | child: showLoading 41 | ? Center( 42 | child: CupertinoActivityIndicator(), 43 | ) 44 | : _searchResultEntity == null 45 | ? getSearchWidget() 46 | : Column( 47 | children: [ 48 | getSearchWidget(), 49 | Expanded( 50 | child: ListView.builder( 51 | itemBuilder: (BuildContext context, int index) { 52 | SearchResultSubject bean = 53 | _searchResultEntity.subjects[index]; 54 | return Padding( 55 | padding: EdgeInsets.all(10.0), 56 | child: GestureDetector( 57 | behavior: HitTestBehavior.translucent, 58 | child: _getItem(bean, index), 59 | onTap: () { 60 | MyRouter.push( 61 | context, MyRouter.detailPage, bean.id); 62 | }, 63 | ), 64 | ); 65 | }, 66 | itemCount: _searchResultEntity.subjects.length, 67 | ), 68 | ) 69 | ], 70 | )), 71 | ); 72 | } 73 | 74 | String getType(String subtype) { 75 | switch (subtype) { 76 | case 'movie': 77 | return '电影'; 78 | } 79 | return ''; 80 | } 81 | 82 | String listConvertString2(List genres) { 83 | if (genres.isEmpty) { 84 | return ''; 85 | } else { 86 | String tmp = ''; 87 | for (SearchResultSubjectsDirector item in genres) { 88 | tmp = tmp + item.name; 89 | } 90 | return tmp; 91 | } 92 | } 93 | 94 | String listConvertString(List genres) { 95 | if (genres.isEmpty) { 96 | return ''; 97 | } else { 98 | String tmp = ''; 99 | for (String item in genres) { 100 | tmp = tmp + item; 101 | } 102 | return tmp; 103 | } 104 | } 105 | 106 | Widget getSearchWidget() { 107 | return Padding( 108 | padding: 109 | EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0, bottom: 20.0), 110 | child: Row( 111 | children: [ 112 | Expanded( 113 | child: SearchTextFieldWidget( 114 | hintText: widget.searchHintContent, 115 | onSubmitted: (searchContent) { 116 | showLoading = true; 117 | _api.searchMovie(searchContent, (searchResultEntity) { 118 | setState(() { 119 | showLoading = false; 120 | _searchResultEntity = searchResultEntity; 121 | }); 122 | }); 123 | }, 124 | ), 125 | ), 126 | GestureDetector( 127 | child: Padding( 128 | padding: EdgeInsets.only(left: 10.0), 129 | child: Text( 130 | '取消', 131 | style: getStyle(Colors.green, 17.0), 132 | ), 133 | ), 134 | onTap: () { 135 | Navigator.pop(context); 136 | }, 137 | ) 138 | ], 139 | ), 140 | ); 141 | } 142 | 143 | Widget _getItem(SearchResultSubject bean, int index) { 144 | return Row( 145 | crossAxisAlignment: CrossAxisAlignment.start, 146 | children: [ 147 | Card( 148 | child: Image.network( 149 | bean.images.medium, 150 | fit: BoxFit.cover, 151 | width: imgW, 152 | height: imgH, 153 | ), 154 | clipBehavior: Clip.antiAlias, 155 | elevation: 5.0, 156 | shape: RoundedRectangleBorder( 157 | borderRadius: BorderRadius.all(Radius.circular(5.0))), 158 | ), 159 | Padding( 160 | padding: EdgeInsets.all(5.0), 161 | ), 162 | Expanded( 163 | child: Column( 164 | crossAxisAlignment: CrossAxisAlignment.start, 165 | children: [ 166 | Text( 167 | getType(bean.subtype), 168 | style: getStyle(Colors.grey, 12.0), 169 | ), 170 | Text(bean.title + '(${bean.year})', 171 | style: getStyle(Colors.black, 15.0, bold: true)), 172 | Text( 173 | '${bean.rating.average} 分 / ${listConvertString(bean.pubdates)} / ${listConvertString(bean.genres)} / ${listConvertString2(bean.directors)}', 174 | style: getStyle(Colors.grey, 13.0)) 175 | ], 176 | ), 177 | ) 178 | ], 179 | ); 180 | } 181 | 182 | TextStyle getStyle(Color color, double fontSize, {bool bold = false}) { 183 | return TextStyle( 184 | color: color, 185 | fontSize: fontSize, 186 | fontWeight: bold ? FontWeight.bold : FontWeight.normal); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/pages/shop_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 5 | import 'package:doubanapp/util/screen_utils.dart'; 6 | 7 | String url1 = 'https://flutterchina.club/'; 8 | String url2 = 'http://flutterall.com/'; 9 | bool _closed = false; 10 | bool _isShow = true; 11 | ///提供链接到一个唯一webview的单例实例,以便您可以从应用程序的任何位置控制webview 12 | final _webviewReference = FlutterWebviewPlugin(); 13 | 14 | ///市集 市集使用两个webView代替,因为豆瓣中 这个就是WebView 15 | class ShopPageWidget extends StatelessWidget { 16 | 17 | void setShowState(bool isShow) { 18 | _isShow = isShow; 19 | if(!isShow){ 20 | _closed = true; 21 | _webviewReference.hide(); 22 | _webviewReference.close(); 23 | } 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return WebViewPageWidget(); 29 | } 30 | } 31 | 32 | 33 | class WebViewPageWidget extends StatefulWidget { 34 | @override 35 | _WebViewPageWidgetState createState() => _WebViewPageWidgetState(); 36 | } 37 | 38 | 39 | class _WebViewPageWidgetState extends State 40 | with SingleTickerProviderStateMixin { 41 | var list = ['豆芽豆品', '豆芽时间']; 42 | int selectIndex = 0; 43 | Color selectColor, unselectColor; 44 | TextStyle selectStyle, unselectedStyle; 45 | TabController tabController; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | print('_ShopPageWidgetState initState'); 51 | _webviewReference.close(); 52 | tabController = new TabController(length: list.length, vsync: this); 53 | selectColor = Colors.green; 54 | unselectColor = Color.fromARGB(255, 117, 117, 117); 55 | selectStyle = TextStyle(fontSize: 18); 56 | unselectedStyle = TextStyle(fontSize: 18); 57 | _webviewReference.onUrlChanged.listen((String url) { 58 | if(url != url1 || url != url2){ 59 | print("new Url=$url"); 60 | } 61 | }); 62 | } 63 | 64 | @override 65 | void dispose() { 66 | super.dispose(); 67 | print('_ShopPageWidgetState dispose'); 68 | tabController.dispose(); 69 | _webviewReference.close(); 70 | _webviewReference.dispose(); 71 | } 72 | 73 | @override 74 | Widget build(BuildContext context) { 75 | if(!_isShow){ 76 | return Container(); 77 | } 78 | return Container( 79 | child: SafeArea( 80 | child: Column( 81 | children: [ 82 | Row( 83 | children: [ 84 | Expanded( 85 | flex: 1, 86 | child: Container(), 87 | ), 88 | Expanded( 89 | flex: 3, 90 | child: Container( 91 | padding: 92 | const EdgeInsets.only(top: 20.0), 93 | child: TabBar( 94 | tabs: list.map((item) => Text(item)).toList(), 95 | isScrollable: false, 96 | controller: tabController, 97 | indicatorColor: selectColor, 98 | labelColor: selectColor, 99 | labelStyle: selectStyle, 100 | unselectedLabelColor: unselectColor, 101 | unselectedLabelStyle: unselectedStyle, 102 | indicatorSize: TabBarIndicatorSize.label, 103 | onTap: (selectIndex) { 104 | print('select=$selectIndex'); 105 | this.selectIndex = selectIndex; 106 | print('_closed=$_closed'); 107 | _webviewReference.reloadUrl(selectIndex == 0 ? url1 : url2); 108 | }, 109 | ), 110 | ), 111 | ), 112 | Expanded( 113 | flex: 1, 114 | child: Container(), 115 | ) 116 | ], 117 | ), 118 | Expanded( 119 | child: _WebViewWidget(selectIndex == 0 ? url1 : url2), 120 | ) 121 | ], 122 | )), 123 | color: Colors.white, 124 | ); 125 | } 126 | 127 | } 128 | 129 | class _WebViewWidget extends StatefulWidget { 130 | final String url; 131 | 132 | _WebViewWidget(this.url, {Key key}) : super(key: key); 133 | 134 | @override 135 | _WebViewWidgetState createState() => _WebViewWidgetState(); 136 | } 137 | 138 | class _WebViewWidgetState extends State<_WebViewWidget> { 139 | Rect _rect; 140 | bool needFullScreen = false; 141 | @override 142 | void initState() { 143 | super.initState(); 144 | _webviewReference.close(); 145 | } 146 | 147 | @override 148 | void dispose() { 149 | super.dispose(); 150 | _webviewReference.close(); 151 | _webviewReference.dispose(); 152 | } 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | print('build widget.url=${widget.url}'); 157 | return _WebviewPlaceholder(onRectChanged: (Rect value) { 158 | if (_rect == null || _closed) { 159 | if(_rect != value){ 160 | _rect = value; 161 | } 162 | print('_webviewReference.launch'); 163 | _webviewReference.launch(widget.url, 164 | withJavascript: true, 165 | withLocalStorage: true, 166 | scrollBar: true, 167 | rect: getRect()); 168 | } else { 169 | print('_webviewReference.launch else'); 170 | if (_rect != value) { 171 | _rect = value; 172 | } 173 | _webviewReference.reloadUrl(widget.url); 174 | } 175 | }, child: const Center(child: const CircularProgressIndicator()),); 176 | } 177 | 178 | getRect() { 179 | if(needFullScreen){ 180 | return null; 181 | }else{ 182 | return Rect.fromLTRB(0.0, ScreenUtils.getStatusBarH(context) + 60.0, 183 | ScreenUtils.screenW(context), ScreenUtils.screenH(context) - 60.0); 184 | } 185 | } 186 | 187 | } 188 | 189 | class _WebviewPlaceholder extends SingleChildRenderObjectWidget { 190 | const _WebviewPlaceholder({ 191 | Key key, 192 | @required this.onRectChanged, 193 | Widget child, 194 | }) : super(key: key, child: child); 195 | 196 | final ValueChanged onRectChanged; 197 | 198 | @override 199 | RenderObject createRenderObject(BuildContext context) { 200 | return _WebviewPlaceholderRender( 201 | onRectChanged: onRectChanged, 202 | ); 203 | } 204 | 205 | @override 206 | void updateRenderObject( 207 | BuildContext context, _WebviewPlaceholderRender renderObject) { 208 | renderObject..onRectChanged = onRectChanged; 209 | } 210 | } 211 | 212 | class _WebviewPlaceholderRender extends RenderProxyBox { 213 | _WebviewPlaceholderRender({ 214 | RenderBox child, 215 | ValueChanged onRectChanged, 216 | }) : _callback = onRectChanged, 217 | super(child); 218 | 219 | ValueChanged _callback; 220 | Rect _rect; 221 | 222 | Rect get rect => _rect; 223 | 224 | set onRectChanged(ValueChanged callback) { 225 | if (callback != _callback) { 226 | _callback = callback; 227 | notifyRect(); 228 | } 229 | } 230 | 231 | void notifyRect() { 232 | if (_callback != null && _rect != null) { 233 | _callback(_rect); 234 | } 235 | } 236 | 237 | @override 238 | void paint(PaintingContext context, Offset offset) { 239 | super.paint(context, offset); 240 | final rect = offset & size; 241 | if (_rect != rect) { 242 | _rect = rect; 243 | notifyRect(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /lib/pages/splash/splash_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:doubanapp/pages/container_page.dart'; 5 | import 'package:doubanapp/util/screen_utils.dart'; 6 | import 'package:doubanapp/constant/constant.dart'; 7 | import 'package:fluttertoast/fluttertoast.dart'; 8 | ///打开APP首页 9 | class SplashWidget extends StatefulWidget { 10 | @override 11 | _SplashWidgetState createState() => _SplashWidgetState(); 12 | } 13 | 14 | class _SplashWidgetState extends State { 15 | var container = ContainerPage(); 16 | 17 | bool showAd = true; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | print('build splash'); 22 | return Stack( 23 | children: [ 24 | Offstage( 25 | child: container, 26 | offstage: showAd, 27 | ), 28 | Offstage( 29 | child: Container( 30 | color: Colors.white, 31 | child: Stack( 32 | children: [ 33 | Align( 34 | alignment: Alignment(0.0, 0.0), 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.center, 37 | children: [ 38 | CircleAvatar( 39 | radius: ScreenUtils.screenW(context) / 3, 40 | backgroundColor: Colors.white, 41 | backgroundImage: 42 | AssetImage(Constant.ASSETS_IMG + 'home.png'), 43 | ), 44 | Padding( 45 | padding: const EdgeInsets.only(top: 20.0), 46 | child: Text( 47 | '落花有意随流水,流水无心恋落花', 48 | style: TextStyle(fontSize: 15.0, color: Colors.black), 49 | ), 50 | ) 51 | ], 52 | ), 53 | ), 54 | SafeArea( 55 | child: Column( 56 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 57 | children: [ 58 | Align( 59 | alignment: Alignment(1.0, 0.0), 60 | child: Container( 61 | margin: const EdgeInsets.only(right: 30.0, top: 20.0), 62 | padding: const EdgeInsets.only( 63 | left: 10.0, right: 10.0, top: 2.0, bottom: 2.0), 64 | child: CountDownWidget( 65 | onCountDownFinishCallBack: (bool value) { 66 | if (value) { 67 | setState(() { 68 | showAd = false; 69 | }); 70 | } 71 | }, 72 | ), 73 | decoration: BoxDecoration( 74 | color: Color(0xffEDEDED), 75 | borderRadius: 76 | const BorderRadius.all(Radius.circular(10.0))), 77 | ), 78 | ), 79 | Padding( 80 | padding: const EdgeInsets.only(bottom: 40.0), 81 | child: Row( 82 | mainAxisAlignment: MainAxisAlignment.center, 83 | children: [ 84 | Image.asset( 85 | Constant.ASSETS_IMG + 'ic_launcher.png', 86 | width: 50.0, 87 | height: 50.0, 88 | ), 89 | Padding( 90 | padding: const EdgeInsets.only(left: 10.0), 91 | child: Text( 92 | 'Hi,豆芽', 93 | style: TextStyle( 94 | color: Colors.green, 95 | fontSize: 30.0, 96 | fontWeight: FontWeight.bold), 97 | ), 98 | ) 99 | ], 100 | ), 101 | ) 102 | ], 103 | )) 104 | ], 105 | ), 106 | width: ScreenUtils.screenW(context), 107 | height: ScreenUtils.screenH(context), 108 | ), 109 | offstage: !showAd, 110 | ) 111 | ], 112 | ); 113 | } 114 | } 115 | 116 | class CountDownWidget extends StatefulWidget { 117 | final onCountDownFinishCallBack; 118 | 119 | CountDownWidget({Key key, @required this.onCountDownFinishCallBack}) 120 | : super(key: key); 121 | 122 | @override 123 | _CountDownWidgetState createState() => _CountDownWidgetState(); 124 | } 125 | 126 | class _CountDownWidgetState extends State { 127 | var _seconds = 6; 128 | Timer _timer; 129 | 130 | @override 131 | void initState() { 132 | super.initState(); 133 | _startTimer(); 134 | } 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return Text( 139 | '$_seconds', 140 | style: TextStyle(fontSize: 17.0), 141 | ); 142 | } 143 | 144 | /// 启动倒计时的计时器。 145 | void _startTimer() { 146 | _timer = Timer.periodic(Duration(seconds: 1), (timer) { 147 | setState(() {}); 148 | if (_seconds <= 1) { 149 | widget.onCountDownFinishCallBack(true); 150 | _cancelTimer(); 151 | return; 152 | } 153 | _seconds--; 154 | }); 155 | } 156 | 157 | /// 取消倒计时的计时器。 158 | void _cancelTimer() { 159 | _timer?.cancel(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/pages/videos_play_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:doubanapp/widgets/video_widget.dart'; 4 | import 'package:doubanapp/bean/movie_detail_bean.dart'; 5 | 6 | class VideoPlayPage extends StatefulWidget { 7 | final List beans; 8 | 9 | VideoPlayPage(this.beans, {Key key}) : super(key: key); 10 | 11 | @override 12 | State createState() => _VideoPlayPageState(); 13 | } 14 | 15 | class _VideoPlayPageState extends State { 16 | double mediumW, mediumH; //309 X 177 17 | int _showPlayIndex = 0; 18 | VideoWidget playWidget; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | if (mediumW == null) { 23 | mediumW = MediaQuery.of(context).size.width / 4; 24 | mediumH = mediumW / 309 * 177; 25 | playWidget = VideoWidget( 26 | widget.beans[0].resource_url, 27 | previewImgUrl: widget.beans[0].medium, 28 | ); 29 | } 30 | return Scaffold( 31 | backgroundColor: Colors.black, 32 | body: SafeArea( 33 | child: Column( 34 | children: [ 35 | Container( 36 | color: Colors.black, 37 | child: Column( 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | children: [ 40 | IconButton( 41 | icon: Icon( 42 | Icons.arrow_back_ios, 43 | color: Colors.white, 44 | ), 45 | onPressed: () { 46 | Navigator.pop(context); 47 | }), 48 | playWidget 49 | ], 50 | ), 51 | ), 52 | Expanded( 53 | child: Container( 54 | color: Colors.white, 55 | child: ListView.builder( 56 | itemBuilder: (BuildContext context, int index) { 57 | if (index == 0) { 58 | return Padding( 59 | padding: EdgeInsets.all(10.0), 60 | child: Row( 61 | children: [ 62 | Expanded( 63 | child: Text( 64 | '观看预告片/片段/花絮', 65 | style: TextStyle(fontSize: 12.0), 66 | ), 67 | ), 68 | Text('${widget.beans.length}', 69 | style: TextStyle(fontSize: 12.0)) 70 | ], 71 | ), 72 | ); 73 | } 74 | return getItem(widget.beans[index - 1], index - 1); 75 | }, 76 | itemCount: widget.beans.length + 1, 77 | ), 78 | ), 79 | ) 80 | ], 81 | )), 82 | ); 83 | } 84 | 85 | Widget getItem(Blooper bean, int index) { 86 | return GestureDetector( 87 | behavior: HitTestBehavior.translucent, 88 | child: Column( 89 | children: [ 90 | Row( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | children: [ 93 | Padding( 94 | padding: EdgeInsets.all(10.0), 95 | child: Stack( 96 | children: [ 97 | CachedNetworkImage( 98 | imageUrl: bean.medium, 99 | width: mediumW, 100 | height: mediumH, 101 | fit: BoxFit.cover, 102 | ), 103 | Align( 104 | alignment: Alignment.center, 105 | child: getPlay(index), 106 | ) 107 | ], 108 | ), 109 | ), 110 | Expanded( 111 | child: Padding( 112 | padding: EdgeInsets.only(right: 10.0), 113 | child: Text( 114 | bean.title, 115 | softWrap: true, 116 | style: TextStyle(fontSize: 15.0), 117 | ), 118 | ), 119 | ), 120 | ], 121 | ), 122 | Divider() 123 | ], 124 | ), 125 | onTap: () { 126 | setState(() { 127 | _showPlayIndex = index; 128 | }); 129 | playWidget.updateUrl(bean.resource_url); 130 | }, 131 | ); 132 | } 133 | 134 | getPlay(int index) { 135 | if (index == _showPlayIndex) { 136 | return Container( 137 | width: mediumW, 138 | height: mediumH, 139 | alignment: Alignment.center, 140 | child: Icon( 141 | Icons.play_circle_outline, 142 | color: Colors.amber, 143 | ), 144 | ); 145 | } else { 146 | return Container(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/pages/web_view_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 3 | 4 | class WebViewPage extends StatelessWidget { 5 | final String url; 6 | final dynamic params; 7 | static final String TITLE = 'title'; 8 | 9 | WebViewPage(this.url, {Key key, this.params}) : super(key: key); 10 | // final _webviewReference = FlutterWebviewPlugin(); 11 | @override 12 | Widget build(BuildContext context) { 13 | // _webviewReference.close(); 14 | // _webviewReference.dispose(); 15 | 16 | return WebviewScaffold( 17 | url: url, 18 | appBar: AppBar( 19 | title: Text(params[TITLE]), 20 | backgroundColor: Colors.green, 21 | ), 22 | ); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /lib/repository/movie_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/bean/subject_entity.dart'; 3 | import 'package:doubanapp/bean/top_item_bean.dart'; 4 | import 'package:doubanapp/http/http_request.dart'; 5 | import 'dart:math' as math; 6 | //import 'package:palette_generator/palette_generator.dart'; 7 | import 'package:doubanapp/http/API.dart'; 8 | import 'package:doubanapp/http/mock_request.dart'; 9 | import 'package:palette_generator/palette_generator.dart'; 10 | import 'package:shared_preferences/shared_preferences.dart'; 11 | import 'package:doubanapp/constant/cache_key.dart'; 12 | 13 | class MovieRepository { 14 | var _request; 15 | 16 | // var _request = HttpRequest(API.BASE_URL); 17 | 18 | List hotShowBeans; //影院热映 19 | List comingSoonBeans; //即将上映 20 | List hotBeans; //豆瓣榜单 21 | List weeklyBeans; //一周口碑电影榜 22 | List top250Beans; //Top250 23 | List todayUrls; 24 | TopItemBean weeklyTopBean, weeklyHotBean, weeklyTop250Bean; 25 | Color weeklyTopColor, weeklyHotColor, weeklyTop250Color, todayPlayBg; 26 | 27 | MovieRepository( 28 | {this.hotShowBeans, 29 | this.comingSoonBeans, 30 | this.hotBeans, 31 | this.weeklyBeans, 32 | this.top250Beans, 33 | this.todayUrls, 34 | this.weeklyTopBean, 35 | this.weeklyHotBean, 36 | this.weeklyTop250Bean, 37 | this.weeklyTopColor, 38 | this.weeklyHotColor, 39 | this.weeklyTop250Color, 40 | this.todayPlayBg}); 41 | 42 | Future requestAPI() async { 43 | SharedPreferences prefs = await SharedPreferences.getInstance(); 44 | bool useNetData = prefs.getBool(CacheKey.USE_NET_DATA) ?? false; 45 | if (useNetData) { 46 | _request = HttpRequest(API.BASE_URL); 47 | } else { 48 | _request = MockRequest(); 49 | } 50 | 51 | ///影院热映 52 | var result = await _request.get(API.IN_THEATERS); 53 | var resultList = result['subjects']; 54 | hotShowBeans = 55 | resultList.map((item) => Subject.fromMap(item)).toList(); 56 | 57 | ///即将上映 58 | result = await _request.get(API.COMING_SOON); 59 | resultList = result['subjects']; 60 | comingSoonBeans = 61 | resultList.map((item) => Subject.fromMap(item)).toList(); 62 | int start = math.Random().nextInt(220); 63 | 64 | ///这里使用了下面的模拟请求 65 | 66 | if (useNetData) { 67 | result = await _request.get(API.TOP_250 + 68 | '?start=$start&count=7&apikey=0b2bdeda43b5688921839c8ecb20399b'); 69 | } else { 70 | result = await _request.get(API.TOP_250); 71 | } 72 | resultList = result['subjects']; 73 | 74 | ///豆瓣榜单 75 | hotBeans = 76 | resultList.map((item) => Subject.fromMap(item)).toList(); 77 | 78 | ///一周热门电影榜 79 | weeklyHotBean = TopItemBean.convertHotBeans(hotBeans); 80 | var paletteGenerator = await PaletteGenerator.fromImageProvider( 81 | NetworkImage(hotBeans[0].images.medium)); 82 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 83 | weeklyHotColor = (paletteGenerator.colors.toList()[0]); 84 | } 85 | 86 | ///一周口碑电影榜 87 | result = await _request.get(API.WEEKLY); 88 | resultList = result['subjects']; 89 | weeklyBeans = resultList 90 | .map((item) => SubjectEntity.fromMap(item)) 91 | .toList(); 92 | weeklyTopBean = TopItemBean.convertWeeklyBeans(weeklyBeans); 93 | paletteGenerator = await PaletteGenerator.fromImageProvider( 94 | NetworkImage(weeklyBeans[0].subject.images.medium)); 95 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 96 | weeklyTopColor = (paletteGenerator.colors.toList()[0]); 97 | } 98 | 99 | ///今日可播放电影 100 | start = math.Random().nextInt(220); 101 | 102 | ///这里使用了下面的模拟请求 103 | 104 | if (useNetData) { 105 | result = await _request.get(API.TOP_250 + 106 | '?start=$start&count=7&apikey=0b2bdeda43b5688921839c8ecb20399b'); 107 | } else { 108 | result = await _request.get(API.TOP_250); 109 | } 110 | resultList = result['subjects']; 111 | List beans = 112 | resultList.map((item) => Subject.fromMap(item)).toList(); 113 | todayUrls = []; 114 | todayUrls.add(beans[0].images.medium); 115 | todayUrls.add(beans[1].images.medium); 116 | todayUrls.add(beans[2].images.medium); 117 | paletteGenerator = 118 | await PaletteGenerator.fromImageProvider(NetworkImage(todayUrls[0])); 119 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 120 | todayPlayBg = (paletteGenerator.colors.toList()[0]); 121 | } 122 | 123 | ///豆瓣TOP250 124 | /// ///这里使用了下面的模拟请求 125 | // result = await _request.get(API.TOP_250 + '?start=0&count=5&apikey=0b2bdeda43b5688921839c8ecb20399b'); 126 | 127 | if (useNetData) { 128 | result = await _request.get(API.TOP_250 + 129 | '?start=0&count=5&apikey=0b2bdeda43b5688921839c8ecb20399b'); 130 | } else { 131 | result = await _request.get(API.TOP_250); 132 | } 133 | resultList = result['subjects']; 134 | top250Beans = 135 | resultList.map((item) => Subject.fromMap(item)).toList(); 136 | weeklyTop250Bean = TopItemBean.convertTopBeans(top250Beans); 137 | paletteGenerator = await PaletteGenerator.fromImageProvider( 138 | NetworkImage(top250Beans[0].images.medium)); 139 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 140 | weeklyTop250Color = (paletteGenerator.colors.toList()[0]); 141 | } 142 | return MovieRepository( 143 | hotShowBeans: hotShowBeans, 144 | comingSoonBeans: comingSoonBeans, 145 | hotBeans: hotBeans, 146 | weeklyBeans: weeklyBeans, 147 | top250Beans: top250Beans, 148 | todayUrls: todayUrls, 149 | weeklyTopBean: weeklyTopBean, 150 | weeklyHotBean: weeklyHotBean, 151 | weeklyTop250Bean: weeklyTop250Bean, 152 | weeklyTopColor: weeklyTopColor, 153 | weeklyHotColor: weeklyHotColor, 154 | weeklyTop250Color: weeklyTop250Color, 155 | todayPlayBg: todayPlayBg); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/repository/person_detail_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:doubanapp/bean/celebrity_entity.dart'; 2 | import 'package:doubanapp/bean/celebrity_work_entity.dart'; 3 | import 'package:doubanapp/http/http_request.dart'; 4 | import 'package:doubanapp/http/API.dart'; 5 | 6 | class PersonDetailRepository { 7 | CelebrityEntity celebrityEntity; 8 | CelebrityWorkEntity celebrityWorkEntity; 9 | HttpRequest _httpRequest = HttpRequest(API.BASE_URL); 10 | PersonDetailRepository({this.celebrityEntity, this.celebrityWorkEntity}); 11 | 12 | Future requestAPI(String id) async { 13 | var result = await _httpRequest.get('/v2/movie/celebrity/$id/works?apikey=0b2bdeda43b5688921839c8ecb20399b'); 14 | celebrityWorkEntity = CelebrityWorkEntity.fromJson(result); 15 | 16 | result = await _httpRequest.get('/v2/movie/celebrity/$id?apikey=0b2bdeda43b5688921839c8ecb20399b'); 17 | celebrityEntity = CelebrityEntity.fromJson(result); 18 | return PersonDetailRepository(celebrityEntity: celebrityEntity, celebrityWorkEntity: celebrityWorkEntity); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:doubanapp/pages/detail/detail_page.dart'; 4 | import 'package:doubanapp/pages/container_page.dart'; 5 | import 'package:doubanapp/pages/videos_play_page.dart'; 6 | import 'package:doubanapp/pages/search/search_page.dart'; 7 | import 'package:doubanapp/pages/photo_hero_page.dart'; 8 | import 'package:doubanapp/pages/person_detail_page.dart'; 9 | import 'package:doubanapp/pages/web_view_page.dart'; 10 | 11 | ///https://www.jianshu.com/p/b9d6ec92926f 12 | 13 | class MyRouter { 14 | static const homePage = 'app://'; 15 | static const detailPage = 'app://DetailPage'; 16 | static const playListPage = 'app://VideosPlayPage'; 17 | static const searchPage = 'app://SearchPage'; 18 | static const photoHero = 'app://PhotoHero'; 19 | static const personDetailPage = 'app://PersonDetailPage'; 20 | 21 | // Widget _router(String url, dynamic params) { 22 | // String pageId = _pageIdMap[url]; 23 | // return _getPage(pageId, params); 24 | // } 25 | // 26 | // Map _pageIdMap = { 27 | // 'app/': 'ContainerPageWidget', 28 | // detailPage: 'DetailPage', 29 | // }; 30 | 31 | Widget _getPage(String url, dynamic params) { 32 | if (url.startsWith('https://') || url.startsWith('http://')) { 33 | return WebViewPage(url, params: params); 34 | } else { 35 | switch (url) { 36 | case detailPage: 37 | return DetailPage(params); 38 | case homePage: 39 | return ContainerPage(); 40 | case playListPage: 41 | return VideoPlayPage(params); 42 | case searchPage: 43 | return SearchPage(searchHintContent: params); 44 | case photoHero: 45 | return PhotoHeroPage( 46 | photoUrl: params['photoUrl'], width: params['width']); 47 | case personDetailPage: 48 | return PersonDetailPage(params['personImgUrl'], params['id']); 49 | } 50 | } 51 | return null; 52 | } 53 | 54 | // 55 | // void push(BuildContext context, String url, dynamic params) { 56 | // Navigator.push(context, MaterialPageRoute(builder: (context) { 57 | // return _getPage(url, params); 58 | // })); 59 | // } 60 | 61 | MyRouter.pushNoParams(BuildContext context, String url) { 62 | Navigator.push(context, MaterialPageRoute(builder: (context) { 63 | return _getPage(url, null); 64 | })); 65 | } 66 | 67 | MyRouter.push(BuildContext context, String url, dynamic params) { 68 | Navigator.push(context, MaterialPageRoute(builder: (context) { 69 | return _getPage(url, params); 70 | })); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/util/pick_img_main_color.dart: -------------------------------------------------------------------------------- 1 | //import 'package:palette_generator/palette_generator.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:palette_generator/palette_generator.dart'; 5 | 6 | typedef ColorCallBack = void Function(Color color); 7 | 8 | ///提取图片的主色调 9 | class PickImgMainColor { 10 | static Future pick( 11 | ImageProvider imageProvider, ColorCallBack callBack) async { 12 | var paletteGenerator = 13 | await PaletteGenerator.fromImageProvider(imageProvider); 14 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 15 | callBack(paletteGenerator.colors.toList()[0]); 16 | } else { 17 | callBack(null); 18 | } 19 | } 20 | 21 | static Future pick2( 22 | ImageProvider imageProvider) async { 23 | var paletteGenerator = 24 | await PaletteGenerator.fromImageProvider(imageProvider); 25 | if (paletteGenerator != null && paletteGenerator.colors.isNotEmpty) { 26 | return (paletteGenerator.colors.toList()[0]); 27 | } else { 28 | return (null); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/util/screen_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:ui' as ui; 3 | //系统默认的appBar等高度 4 | //位于Dart Packages/flutter/src/material/constans.dart 5 | 6 | /** 7 | * @Author: thl 8 | * @GitHub: https://github.com/Sky24n 9 | * @Email: 863764940@qq.com 10 | * @Email: sky24no@gmail.com 11 | * @Description: Screen Util. 12 | * @Date: 2018/9/8 13 | */ 14 | 15 | ///默认设计稿尺寸(单位 dp or pt) 16 | double _designW = 360.0; 17 | double _designH = 640.0; 18 | double _designD = 3.0; 19 | 20 | /** 21 | * 配置设计稿尺寸(单位 dp or pt) 22 | * w 宽 23 | * h 高 24 | * density 像素密度 25 | */ 26 | 27 | /// 配置设计稿尺寸 屏幕 宽,高,密度。 28 | /// Configuration design draft size screen width, height, density. 29 | void setDesignWHD(double w, double h, {double density = 3.0}) { 30 | _designW = w; 31 | _designH = h; 32 | _designD = density; 33 | } 34 | 35 | /// Screen Util. 36 | class ScreenUtils { 37 | double _screenWidth = 0.0; 38 | double _screenHeight = 0.0; 39 | double _screenDensity = 0.0; 40 | double _statusBarHeight = 0.0; 41 | double _bottomBarHeight = 0.0; 42 | double _appBarHeight = 0.0; 43 | MediaQueryData _mediaQueryData; 44 | 45 | static final ScreenUtils _singleton = ScreenUtils(); 46 | 47 | static ScreenUtils getInstance() { 48 | _singleton._init(); 49 | return _singleton; 50 | } 51 | 52 | _init() { 53 | MediaQueryData mediaQuery = MediaQueryData.fromWindow(ui.window); 54 | if (_mediaQueryData != mediaQuery) { 55 | _mediaQueryData = mediaQuery; 56 | _screenWidth = mediaQuery.size.width; 57 | _screenHeight = mediaQuery.size.height; 58 | _screenDensity = mediaQuery.devicePixelRatio; 59 | _statusBarHeight = mediaQuery.padding.top; 60 | _bottomBarHeight = mediaQuery.padding.bottom; 61 | _appBarHeight = kToolbarHeight; 62 | } 63 | } 64 | 65 | /// screen width 66 | /// 屏幕 宽 67 | double get screenWidth => _screenWidth; 68 | 69 | /// screen height 70 | /// 屏幕 高 71 | double get screenHeight => _screenHeight; 72 | 73 | /// appBar height 74 | /// appBar 高 75 | double get appBarHeight => _appBarHeight; 76 | 77 | /// screen density 78 | /// 屏幕 像素密度 79 | double get screenDensity => _screenDensity; 80 | 81 | /// status bar Height 82 | /// 状态栏高度 83 | double get statusBarHeight => _statusBarHeight; 84 | 85 | /// bottom bar Height 86 | double get bottomBarHeight => _bottomBarHeight; 87 | 88 | /// media Query Data 89 | MediaQueryData get mediaQueryData => _mediaQueryData; 90 | 91 | /// screen width 92 | /// 当前屏幕 宽 93 | static double screenW(BuildContext context) { 94 | MediaQueryData mediaQuery = MediaQuery.of(context); 95 | return mediaQuery.size.width; 96 | } 97 | 98 | /// screen height 99 | /// 当前屏幕 高 100 | static double screenH(BuildContext context) { 101 | MediaQueryData mediaQuery = MediaQuery.of(context); 102 | return mediaQuery.size.height; 103 | } 104 | 105 | /// screen density 106 | /// 当前屏幕 像素密度 107 | static double getScreenDensity(BuildContext context) { 108 | MediaQueryData mediaQuery = MediaQuery.of(context); 109 | return mediaQuery.devicePixelRatio; 110 | } 111 | 112 | /// status bar Height 113 | /// 当前状态栏高度 114 | static double getStatusBarH(BuildContext context) { 115 | MediaQueryData mediaQuery = MediaQuery.of(context); 116 | return mediaQuery.padding.top; 117 | } 118 | 119 | /// status bar Height 120 | /// 当前BottomBar高度 121 | static double getBottomBarH(BuildContext context) { 122 | MediaQueryData mediaQuery = MediaQuery.of(context); 123 | return mediaQuery.padding.bottom; 124 | } 125 | 126 | /// 当前MediaQueryData 127 | static MediaQueryData getMediaQueryData(BuildContext context) { 128 | MediaQueryData mediaQuery = MediaQuery.of(context); 129 | return mediaQuery; 130 | } 131 | 132 | /// returns the size after adaptation according to the screen width.(unit dp or pt) 133 | /// 返回根据屏幕宽适配后尺寸(单位 dp or pt) 134 | /// size 单位 dp or pt 135 | static double getScaleW(BuildContext context, double size) { 136 | if (context == null || screenW(context) == 0.0) return size; 137 | return size * screenW(context) / _designW; 138 | } 139 | 140 | /// returns the size after adaptation according to the screen height.(unit dp or pt) 141 | /// 返回根据屏幕高适配后尺寸 (单位 dp or pt) 142 | /// size unit dp or pt 143 | static double getScaleH(BuildContext context, double size) { 144 | if (context == null || screenH(context) == 0.0) return size; 145 | return size * screenH(context) / _designH; 146 | } 147 | 148 | /// returns the font size after adaptation according to the screen density. 149 | /// 返回根据屏幕宽适配后字体尺寸 150 | /// fontSize 字体尺寸 151 | static double getScaleSp(BuildContext context, double fontSize) { 152 | if (context == null || getScreenDensity(context) == 0.0) return fontSize; 153 | return fontSize * screenW(context) / _designW; 154 | } 155 | 156 | /// Orientation 157 | /// 设备方向(portrait, landscape) 158 | static Orientation getOrientation(BuildContext context) { 159 | MediaQueryData mediaQuery = MediaQuery.of(context); 160 | return mediaQuery.orientation; 161 | } 162 | 163 | /// returns the size after adaptation according to the screen width.(unit dp or pt) 164 | /// 返回根据屏幕宽适配后尺寸(单位 dp or pt) 165 | /// size 单位 dp or pt 166 | double getWidth(double size) { 167 | return _screenWidth == 0.0 ? size : (size * _screenWidth / _designW); 168 | } 169 | 170 | /// returns the size after adaptation according to the screen height.(unit dp or pt) 171 | /// 返回根据屏幕高适配后尺寸(单位 dp or pt) 172 | /// size unit dp or pt 173 | double getHeight(double size) { 174 | return _screenHeight == 0.0 ? size : (size * _screenHeight / _designH); 175 | } 176 | 177 | /// returns the size after adaptation according to the screen width.(unit dp or pt) 178 | /// 返回根据屏幕宽适配后尺寸(单位 dp or pt) 179 | /// sizePx unit px 180 | double getWidthPx(double sizePx) { 181 | return _screenWidth == 0.0 182 | ? (sizePx / _designD) 183 | : (sizePx * _screenWidth / (_designW * _designD)); 184 | } 185 | 186 | /// returns the size after adaptation according to the screen height.(unit dp or pt) 187 | /// 返回根据屏幕高适配后尺寸(单位 dp or pt) 188 | /// sizePx unit px 189 | double getHeightPx(double sizePx) { 190 | return _screenHeight == 0.0 191 | ? (sizePx / _designD) 192 | : (sizePx * _screenHeight / (_designH * _designD)); 193 | } 194 | 195 | /// returns the font size after adaptation according to the screen density. 196 | /// 返回根据屏幕宽适配后字体尺寸 197 | /// fontSize 字体尺寸 198 | double getSp(double fontSize) { 199 | if (_screenDensity == 0.0) return fontSize; 200 | return fontSize * _screenWidth / _designW; 201 | } 202 | } -------------------------------------------------------------------------------- /lib/widgets/animal_photo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | 4 | ///点击图片放大显示 5 | class AnimalPhoto { 6 | AnimalPhoto.show(BuildContext context, String url, {double width}) { 7 | if (width == null) { 8 | width = MediaQuery.of(context).size.width; 9 | } 10 | Navigator.of(context) 11 | .push(MaterialPageRoute(builder: (BuildContext context) { 12 | return Container( 13 | // The blue background emphasizes that it's a new route. 14 | color: Colors.transparent, 15 | padding: const EdgeInsets.all(10.0), 16 | alignment: Alignment.center, 17 | child: _PhotoHero( 18 | photo: url, 19 | width: width, 20 | onTap: () { 21 | Navigator.of(context).pop(); 22 | }, 23 | ), 24 | ); 25 | })); 26 | } 27 | } 28 | 29 | class _PhotoHero extends StatelessWidget { 30 | const _PhotoHero({Key key, this.photo, this.onTap, this.width}) 31 | : super(key: key); 32 | 33 | final String photo; 34 | final VoidCallback onTap; 35 | final double width; 36 | 37 | Widget build(BuildContext context) { 38 | return SizedBox( 39 | width: width, 40 | child: Hero( 41 | tag: photo, 42 | child: Material( 43 | color: Colors.transparent, 44 | child: InkWell( 45 | onTap: onTap, 46 | child: Image.network( 47 | photo, 48 | fit: BoxFit.contain, 49 | ), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/widgets/image/LaminatedImage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///层叠的图片,三张图片层叠显示 4 | class LaminatedImage extends StatelessWidget { 5 | final urls; 6 | final w; 7 | 8 | LaminatedImage({Key key, @required this.urls, this.w}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | double h = w * 1.5; 13 | double dif = w * 0.14; 14 | double secondLeftPadding = w * 0.42; 15 | double thirdLeftPadding = w * 0.78; 16 | return Container( 17 | height: h, 18 | width: w + thirdLeftPadding, 19 | child: Stack( 20 | alignment: Alignment.bottomLeft, 21 | children: [ 22 | Positioned( 23 | left: w * 0.78, 24 | child: ClipRRect( 25 | borderRadius: BorderRadius.circular(6.0), 26 | child: Image.network( 27 | urls[2], 28 | width: w, 29 | height: h - dif - dif / 2, 30 | fit: BoxFit.cover, 31 | color: Color.fromARGB(100, 246, 246, 246), 32 | colorBlendMode: BlendMode.screen, 33 | ), 34 | ), 35 | ), 36 | Positioned( 37 | left: secondLeftPadding, 38 | child: ClipRRect( 39 | borderRadius: BorderRadius.circular(6.0), 40 | child: Image.network( 41 | urls[1], 42 | width: w, 43 | height: h - dif, 44 | fit: BoxFit.cover, 45 | color: Color.fromARGB(100, 246, 246, 246), 46 | colorBlendMode: BlendMode.screen, 47 | ), 48 | ), 49 | ), 50 | Positioned( 51 | left: 0, 52 | child: ClipRRect( 53 | borderRadius: BorderRadius.circular(6.0), 54 | child: Image.network( 55 | urls[0], 56 | width: w, 57 | height: h, 58 | fit: BoxFit.cover, 59 | ), 60 | ), 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | 67 | //圆角图片 68 | getImage(var imgUrl, var w, var h) { 69 | // this.color, 70 | // this.elevation = 1.0, 71 | // this.shape, 72 | // this.margin = const EdgeInsets.all(4.0), 73 | // this.clipBehavior = Clip.none, 74 | // this.child, 75 | // this.semanticContainer = true, 76 | return Card( 77 | child: Image.network( 78 | imgUrl, 79 | width: w, 80 | height: h, 81 | fit: BoxFit.cover, 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/widgets/image/cache_img_radius.dart: -------------------------------------------------------------------------------- 1 | //import 'package:doubanapp/widgets/image/cached_network_image.dart'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | typedef OnTab = void Function(); 6 | 7 | class CacheImgRadius extends StatelessWidget { 8 | final String imgUrl; 9 | final double radius; 10 | final OnTab onTab; 11 | CacheImgRadius({Key key, @required this.imgUrl, this.radius, this.onTab}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GestureDetector( 17 | child: ClipRRect( 18 | borderRadius: BorderRadius.all(Radius.circular(radius)), 19 | child: CachedNetworkImage(imageUrl: imgUrl), 20 | ), 21 | onTap: () { 22 | onTab(); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/widgets/image/heart_img_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HeartImgWidget extends StatefulWidget { 4 | final Image img; 5 | 6 | HeartImgWidget(this.img, {Key key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _HeartImgWidgetState(); 10 | } 11 | 12 | class _HeartImgWidgetState extends State 13 | with SingleTickerProviderStateMixin { 14 | AnimationController controller; 15 | Animation animation; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | controller = new AnimationController( 21 | duration: const Duration(milliseconds: 1500), vsync: this); 22 | animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn) 23 | ..addStatusListener((status) { 24 | if (status == AnimationStatus.completed) { 25 | controller.reverse(); 26 | } else if (status == AnimationStatus.dismissed) { 27 | controller.forward(); 28 | } 29 | }); 30 | 31 | controller.forward(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return _AnimatedImg(widget.img, animation: animation); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | controller.dispose(); 42 | super.dispose(); 43 | } 44 | } 45 | 46 | class _AnimatedImg extends AnimatedWidget { 47 | static final _opacityTween = new Tween(begin: 0.5, end: 1.0); 48 | static final _sizeTween = new Tween(begin: 290.0, end: 300.0); 49 | final Image img; 50 | _AnimatedImg(this.img, {Key key, Animation animation}) 51 | : super(key: key, listenable: animation); 52 | 53 | Widget build(BuildContext context) { 54 | final Animation animation = listenable; 55 | return new Center( 56 | child: new Opacity( 57 | opacity: _opacityTween.evaluate(animation), 58 | child: new Container( 59 | margin: new EdgeInsets.symmetric(vertical: 10.0), 60 | height: _sizeTween.evaluate(animation), 61 | width: _sizeTween.evaluate(animation), 62 | child: img, 63 | ), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/widgets/image/network_img_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NetworkImgWidget extends StatefulWidget { 4 | final imgUrl; 5 | final placeHolderAsset; 6 | 7 | NetworkImgWidget({Key key, this.placeHolderAsset, this.imgUrl}) 8 | : super(key: key); 9 | 10 | @override 11 | State createState() { 12 | return _NetworkImgWidgetState(placeHolderAsset, imgUrl); 13 | } 14 | } 15 | 16 | class _NetworkImgWidgetState extends State { 17 | final imgUrl; 18 | final placeHolderAsset; 19 | Image img, netImg; 20 | _NetworkImgWidgetState(this.placeHolderAsset, this.imgUrl); 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | img = Image.asset(placeHolderAsset); 26 | try{ 27 | netImg = Image.network(imgUrl); 28 | }on Exception catch(e){ 29 | print(e); 30 | } 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Stack(children: [ 36 | img, 37 | netImg 38 | ],); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/image/radius_img.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RadiusImg { 4 | static Widget get(String imgUrl, double imgW, {double imgH, Color shadowColor, double elevation, double radius = 6.0, RoundedRectangleBorder shape}) { 5 | if (shadowColor == null) { 6 | shadowColor = Colors.transparent; 7 | } 8 | return Card( 9 | //影音海报 10 | shape:shape?? RoundedRectangleBorder( 11 | borderRadius: BorderRadius.all(Radius.circular(radius)), 12 | ), 13 | color: shadowColor, 14 | clipBehavior: Clip.antiAlias, 15 | elevation: elevation == null ? 0.0 : 5.0, 16 | child: imgW == null ? Image.network( 17 | imgUrl, 18 | height: imgH, 19 | fit: BoxFit.cover, 20 | ):Image.network( 21 | imgUrl, 22 | width: imgW, 23 | height: imgH, 24 | fit: imgH == null ? BoxFit.contain : BoxFit.cover, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/widgets/item_count_title.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/constant/text_size_constant.dart'; 3 | import 'package:doubanapp/constant/color_constant.dart'; 4 | 5 | typedef OnClick = void Function(); 6 | 7 | ///左边是豆瓣热门,右边是全部 8 | class ItemCountTitle extends StatelessWidget { 9 | // final state = _ItemCountTitleState(); 10 | final count; 11 | final OnClick onClick; 12 | final String title; 13 | final double fontSize; 14 | 15 | ItemCountTitle(this.title, {Key key, this.onClick, this.count, this.fontSize}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return GestureDetector( 21 | child: Row( 22 | children: [ 23 | Expanded( 24 | child: Text( 25 | title, 26 | style: TextStyle( 27 | fontSize: fontSize == null 28 | ? TextSizeConstant.BookAudioPartTabBar 29 | : fontSize, 30 | fontWeight: FontWeight.bold, 31 | color: ColorConstant.colorDefaultTitle), 32 | )), 33 | Text( 34 | '全部 ${count == null ? 0 : count} > ', 35 | style: TextStyle( 36 | fontSize: 12, color: Colors.grey, ), 37 | ) 38 | ], 39 | ), 40 | onTap: () { 41 | if (onClick != null) { 42 | onClick(); 43 | } 44 | }, 45 | ); 46 | } 47 | } 48 | // 49 | //class _ItemCountTitleState extends State { 50 | // @override 51 | // Widget build(BuildContext context) { 52 | // return GestureDetector( 53 | // child: Row( 54 | // children: [ 55 | // Expanded( 56 | // child: Text( 57 | // widget.title, 58 | // style: TextStyle( 59 | // fontSize: TextSizeConstant.BookAudioPartTabBar, 60 | // fontWeight: FontWeight.bold, 61 | // color: ColorConstant.colorDefaultTitle), 62 | // )), 63 | // Text( 64 | // '全部 ${widget.count} > ', 65 | // style: TextStyle( 66 | // fontSize: 12, color: Colors.black, fontWeight: FontWeight.bold), 67 | // ) 68 | // ], 69 | // ), 70 | // onTap: () { 71 | // if (widget.onClick != null) { 72 | // widget.onClick(); 73 | // } 74 | // }, 75 | // ); 76 | // } 77 | // 78 | //} 79 | -------------------------------------------------------------------------------- /lib/widgets/loading_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class LoadingWidget { 5 | static Widget getLoading({Color backgroundColor, Color loadingBgColor}) { 6 | return Container( 7 | alignment: AlignmentDirectional.center, 8 | decoration: new BoxDecoration( 9 | color: backgroundColor == null ? Colors.transparent : backgroundColor, 10 | ), 11 | child: new Container( 12 | decoration: new BoxDecoration( 13 | color: loadingBgColor == null ? Colors.white : loadingBgColor, borderRadius: new BorderRadius.circular(10.0)), 14 | width: 70.0, 15 | height: 70.0, 16 | alignment: AlignmentDirectional.center, 17 | child: SizedBox( 18 | height: 25.0, 19 | width: 25.0, 20 | child: CupertinoActivityIndicator( 21 | radius: 15.0, 22 | ), 23 | ), 24 | ), 25 | ); 26 | } 27 | 28 | static Widget containerLoadingBody(Widget body, 29 | {bool loading = true, Color backgroundColor, Color loadingBgColor}) { 30 | return Stack( 31 | children: [ 32 | body, 33 | Offstage( 34 | child: getLoading( 35 | backgroundColor: backgroundColor, loadingBgColor: loadingBgColor), 36 | offstage: !loading, 37 | ) 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/widgets/my_tab_bar_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/pages/douya_top_250_list_widget.dart'; 3 | import 'package:doubanapp/pages/movie/movie_page.dart'; 4 | 5 | class FlutterTabBarView extends StatelessWidget { 6 | final TabController tabController; 7 | 8 | FlutterTabBarView({Key key, @required this.tabController}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | print('build FlutterTabBarView'); 13 | var viewList = [ 14 | MoviePage(key: PageStorageKey('MoviePage'),), 15 | Page2(), 16 | DouBanListView(key: PageStorageKey('DouBanListView'),), 17 | Page4(), 18 | Page5(), 19 | Page1(), 20 | ]; 21 | return TabBarView( 22 | children: viewList, 23 | controller: tabController, 24 | ); 25 | } 26 | } 27 | 28 | class Page1 extends StatelessWidget { 29 | @override 30 | Widget build(BuildContext context) { 31 | print('build Page1'); 32 | 33 | return Center( 34 | child: Text('Page1'), 35 | ); 36 | } 37 | } 38 | 39 | class Page2 extends StatelessWidget { 40 | @override 41 | Widget build(BuildContext context) { 42 | print('build Page2'); 43 | return Center( 44 | child: Text('Page2'), 45 | ); 46 | } 47 | } 48 | 49 | class Page3 extends StatelessWidget { 50 | @override 51 | Widget build(BuildContext context) { 52 | print('build Page3'); 53 | return Center( 54 | child: Text('Page3'), 55 | ); 56 | } 57 | } 58 | 59 | class Page4 extends StatelessWidget { 60 | @override 61 | Widget build(BuildContext context) { 62 | print('build Page4'); 63 | return Center( 64 | child: Text('Page4'), 65 | ); 66 | } 67 | } 68 | 69 | class Page5 extends StatelessWidget { 70 | @override 71 | Widget build(BuildContext context) { 72 | print('build Page5'); 73 | return Center( 74 | child: Text('Page5'), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/widgets/rating_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RatingBar extends StatelessWidget { 4 | var stars; 5 | final double size; 6 | final double fontSize; 7 | final color = Color.fromARGB(255, 255, 170, 71); 8 | 9 | RatingBar(this.stars, {Key key, this.size = 18.0, this.fontSize = 13.0}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | stars = stars * 1.0; 15 | List startList = []; 16 | //实心星星 17 | var startNumber = stars ~/ 2; 18 | //半实心星星 19 | var startHalf = 0; 20 | if (stars.toString().contains('.')) { 21 | int tmp = int.parse((stars.toString().split('.')[1])); 22 | if (tmp >= 5) { 23 | startHalf = 1; 24 | } 25 | } 26 | //空心星星 27 | var startEmpty = 5 - startNumber - startHalf; 28 | 29 | for (var i = 0; i < startNumber; i++) { 30 | startList.add(Icon( 31 | Icons.star, 32 | color: color, 33 | size: size, 34 | )); 35 | } 36 | if (startHalf > 0) { 37 | startList.add(Icon( 38 | Icons.star_half, 39 | color: color, 40 | size: size, 41 | )); 42 | } 43 | for (var i = 0; i < startEmpty; i++) { 44 | startList.add(Icon( 45 | Icons.star_border, 46 | color: Colors.grey, 47 | size: size, 48 | )); 49 | } 50 | startList.add(Text( 51 | '$stars', 52 | style: TextStyle(color: Colors.grey, fontSize: fontSize), 53 | )); 54 | return Container( 55 | alignment: Alignment.topLeft, 56 | child: Row( 57 | children: startList, 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/widgets/search_text_field_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | ///文本搜索框 3 | class SearchTextFieldWidget extends StatelessWidget { 4 | final ValueChanged onSubmitted; 5 | final VoidCallback onTab; 6 | final String hintText; 7 | final EdgeInsetsGeometry margin; 8 | 9 | SearchTextFieldWidget({Key key, this.hintText, this.onSubmitted, this.onTab, this.margin}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | margin: margin == null ? EdgeInsets.all(0.0) : margin, 16 | width: MediaQuery.of(context).size.width, 17 | alignment: AlignmentDirectional.center, 18 | height: 37.0, 19 | decoration: BoxDecoration( 20 | color: Color.fromARGB(255, 237, 236, 237), 21 | borderRadius: BorderRadius.circular(24.0)), 22 | child: TextField( 23 | onSubmitted: onSubmitted, 24 | onTap: onTab, 25 | cursorColor: Color.fromARGB(255, 0, 189, 96), 26 | decoration: InputDecoration( 27 | contentPadding: const EdgeInsets.only(top: 8.0), 28 | border: InputBorder.none, 29 | hintText: hintText, 30 | hintStyle: TextStyle( 31 | fontSize: 17, color: Color.fromARGB(255, 192, 191, 191)), 32 | prefixIcon: Icon( 33 | Icons.search, 34 | size: 25, 35 | color: Color.fromARGB(255, 128, 128, 128), 36 | )), 37 | style: TextStyle(fontSize: 17), 38 | ), 39 | ); 40 | } 41 | 42 | getContainer(BuildContext context, ValueChanged onSubmitted) { 43 | return Container( 44 | width: MediaQuery.of(context).size.width, 45 | alignment: AlignmentDirectional.center, 46 | height: 40.0, 47 | decoration: BoxDecoration( 48 | color: Color.fromARGB(255, 237, 236, 237), 49 | borderRadius: BorderRadius.circular(24.0)), 50 | child: TextField( 51 | onSubmitted: onSubmitted, 52 | cursorColor: Color.fromARGB(255, 0, 189, 96), 53 | decoration: InputDecoration( 54 | contentPadding: EdgeInsets.zero, 55 | border: InputBorder.none, 56 | hintText: hintText, 57 | hintStyle: TextStyle(fontSize: 20), 58 | prefixIcon: Icon( 59 | Icons.search, 60 | size: 29, 61 | color: Color.fromARGB(255, 128, 128, 128), 62 | )), 63 | style: TextStyle(fontSize: 20), 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/widgets/subject_mark_image_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | //import 'package:doubanapp/widgets/image/cached_network_image.dart'; 4 | //import 'package:connectivity/connectivity.dart'; 5 | 6 | typedef BoolCallback = void Function(bool markAdded); 7 | 8 | //test http://img1.doubanio.com/view/photo/s_ratio_poster/public/p457760035.webp 9 | ///点击图片变成订阅状态的缓存图片控件 10 | class SubjectMarkImageWidget extends StatefulWidget { 11 | final imgNetUrl; 12 | final BoolCallback markAdd; 13 | var height; 14 | final width; 15 | ///360 x 513 16 | 17 | SubjectMarkImageWidget(this.imgNetUrl, 18 | {Key key, this.markAdd, this.width = 150.0}) 19 | : super(key: key); 20 | 21 | @override 22 | State createState() { 23 | height = this.width / 150.0 * 210.0; 24 | return _SubjectMarkImageState(imgNetUrl, markAdd, width, height); 25 | } 26 | } 27 | 28 | class _SubjectMarkImageState extends State { 29 | var markAdded = false; 30 | String imgLocalPath, imgNetUrl; 31 | final BoolCallback markAdd; 32 | var markAddedIcon, defaultMarkIcon; 33 | var loadImg; 34 | var imgWH = 28.0; 35 | var height; 36 | var width; 37 | 38 | _SubjectMarkImageState(this.imgNetUrl, this.markAdd, this.width, this.height); 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | markAddedIcon = Image( 44 | image: AssetImage('assets/images/ic_subject_mark_added.png'), 45 | width: imgWH, 46 | height: imgWH, 47 | ); 48 | defaultMarkIcon = ClipRRect( 49 | borderRadius: BorderRadius.only(topLeft: Radius.circular(5.0), bottomRight: Radius.circular(5.0)), 50 | child: Image( 51 | image: AssetImage('assets/images/ic_subject_rating_mark_wish.png'), 52 | width: imgWH, 53 | height: imgWH, 54 | ), 55 | ); 56 | var defaultImg = Image.asset('assets/images/ic_default_img_subject_movie.9.png'); 57 | 58 | loadImg = ClipRRect( 59 | child: CachedNetworkImage( 60 | imageUrl: imgNetUrl, 61 | width: width, 62 | height: height, 63 | fit: BoxFit.fill, 64 | placeholder: (BuildContext context, String url){ 65 | return defaultImg; 66 | }, 67 | fadeInDuration: const Duration(milliseconds: 80), 68 | fadeOutDuration: const Duration(milliseconds: 80), 69 | ), 70 | borderRadius: BorderRadius.all(Radius.circular(5.0)), 71 | ); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Stack( 77 | children: [ 78 | loadImg, 79 | GestureDetector( 80 | child: markAdded ? markAddedIcon : defaultMarkIcon, 81 | onTap: () { 82 | if (markAdd != null) { 83 | markAdd(markAdded); 84 | } 85 | setState(() { 86 | markAdded = !markAdded; 87 | }); 88 | }, 89 | ), 90 | ], 91 | ); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /lib/widgets/title_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:doubanapp/constant/constant.dart'; 3 | 4 | typedef OnTabBack = void Function(); 5 | 6 | ///导航头,如果设置了body,则不用再次使用Scaffold 7 | class TitleBar extends StatelessWidget { 8 | final String title; 9 | final Color backgroundColor; 10 | final Color textColor; 11 | final Widget body; 12 | final OnTabBack onTabBack; 13 | final EdgeInsetsGeometry padding; 14 | 15 | TitleBar( 16 | {Key key, 17 | this.title, 18 | this.backgroundColor = Colors.white, 19 | this.textColor, 20 | this.onTabBack, 21 | this.padding, 22 | this.body}) 23 | : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | if (body == null) { 28 | return _title(context); 29 | } else {} 30 | return Scaffold( 31 | body: Container( 32 | padding: padding ?? EdgeInsets.all(10.0), 33 | alignment: Alignment.center, 34 | child: SafeArea( 35 | child: Column( 36 | mainAxisAlignment: MainAxisAlignment.start, 37 | children: [ 38 | _title(context), 39 | Expanded( 40 | child: Padding( 41 | padding: EdgeInsets.all(12.0), 42 | child: body, 43 | ), 44 | ) 45 | ], 46 | )), 47 | color: backgroundColor, 48 | ), 49 | ); 50 | } 51 | 52 | Widget _title(BuildContext context) { 53 | return Stack( 54 | children: [ 55 | Align( 56 | alignment: Alignment.centerLeft, 57 | child: GestureDetector( 58 | child: Padding( 59 | padding: padding ?? EdgeInsets.all(10.0), 60 | child: Image.asset( 61 | Constant.ASSETS_IMG + 'ic_arrow_back.png', 62 | width: 25.0, 63 | height: 25.0, 64 | ), 65 | ), 66 | onTap: () { 67 | if (onTabBack == null) { 68 | Navigator.of(context).pop(); 69 | } else { 70 | onTabBack(); 71 | } 72 | }, 73 | ), 74 | ), 75 | Align( 76 | alignment: Alignment.center, 77 | child: Text( 78 | title == null ? '' : title, 79 | style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), 80 | ), 81 | ) 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/logo.png -------------------------------------------------------------------------------- /mock/group.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups":[ 3 | { 4 | "imgUrl":"", 5 | "title":"", 6 | "comment":"", 7 | "count":"" 8 | }, 9 | { 10 | "imgUrl":"", 11 | "title":"", 12 | "comment":"", 13 | "count":"" 14 | }, 15 | { 16 | "imgUrl":"", 17 | "title":"", 18 | "comment":"", 19 | "count":"" 20 | }, 21 | { 22 | "imgUrl":"", 23 | "title":"", 24 | "comment":"", 25 | "count":"" 26 | }, 27 | { 28 | "imgUrl":"", 29 | "title":"", 30 | "comment":"", 31 | "count":"" 32 | }, 33 | { 34 | "imgUrl":"", 35 | "title":"", 36 | "comment":"", 37 | "count":"" 38 | }, 39 | { 40 | "imgUrl":"", 41 | "title":"", 42 | "comment":"", 43 | "count":"" 44 | }, 45 | { 46 | "imgUrl":"", 47 | "title":"", 48 | "comment":"", 49 | "count":"" 50 | }, 51 | { 52 | "imgUrl":"", 53 | "title":"", 54 | "comment":"", 55 | "count":"" 56 | }, 57 | { 58 | "imgUrl":"", 59 | "title":"", 60 | "comment":"", 61 | "count":"" 62 | } 63 | 64 | ] 65 | 66 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: doubanapp 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^1.0.4 26 | 27 | flutter_cache_manager: ^3.3.0 28 | 29 | palette_generator: ^0.3.2 30 | 31 | flutter_webview_plugin: 0.4.0 32 | 33 | webview_flutter: ^3.0.0 34 | 35 | video_player: ^2.2.17 36 | 37 | 38 | # 用来模拟网络返回的数据,这些数据是真实的,这里模拟 39 | dio: ^4.0.4 40 | 41 | fluttertoast: ^8.0.8 42 | 43 | cached_network_image: ^3.2.0 44 | 45 | shared_preferences: ^2.0.13 46 | 47 | dev_dependencies: 48 | flutter_test: 49 | sdk: flutter 50 | 51 | 52 | # For information on the generic Dart part of this file, see the 53 | # following page: https://www.dartlang.org/tools/pub/pubspec 54 | 55 | # The following section is specific to Flutter. 56 | flutter: 57 | 58 | # The following line ensures that the Material Icons font is 59 | # included with your application, so that you can use the icons in 60 | # the material Icons class. 61 | uses-material-design: true 62 | assets: 63 | - assets/images/ 64 | - mock/ 65 | 66 | # To add assets to your application, add an assets section, like this: 67 | # assets: 68 | # - images/a_dot_burr.jpeg 69 | # - images/a_dot_ham.jpeg 70 | 71 | # An image asset can refer to one or more resolution-specific "variants", see 72 | # https://flutter.io/assets-and-images/#resolution-aware. 73 | 74 | # For details regarding adding assets from package dependencies, see 75 | # https://flutter.io/assets-and-images/#from-packages 76 | 77 | # To add custom fonts to your application, add a fonts section here, 78 | # in this "flutter" section. Each entry in this list should have a 79 | # "family" key with the font family name, and a "fonts" key with a 80 | # list giving the asset and other descriptors for the font. For 81 | # example: 82 | # fonts: 83 | # - family: Schyler 84 | # fonts: 85 | # - asset: fonts/Schyler-Regular.ttf 86 | # - asset: fonts/Schyler-Italic.ttf 87 | # style: italic 88 | # - family: Trajan Pro 89 | # fonts: 90 | # - asset: fonts/TrajanPro.ttf 91 | # - asset: fonts/TrajanPro_Bold.ttf 92 | # weight: 700 93 | # 94 | # For details regarding fonts from package dependencies, 95 | # see https://flutter.io/custom-fonts/#from-packages 96 | -------------------------------------------------------------------------------- /res/values/strings_en.arb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaina404/FlutterDouBan/f8b5c021a2bca1afdfb3897f1f7330cb789b4da6/res/values/strings_en.arb --------------------------------------------------------------------------------