├── .gitignore ├── .metadata ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── android_generated.zip ├── flutter_hybrid.iml ├── flutter_hybrid_android.iml ├── lib ├── Model │ ├── CLCommentsModel.dart │ └── CLMomentsModel.dart ├── Moment │ ├── CLMomentsDetailPage.dart │ ├── CLMomentsPage.dart │ ├── CLOptionsPage.dart │ ├── CLPhotoViewBrowser.dart │ └── CLPublishMomentPage.dart ├── Utils │ ├── CLDioUtil.dart │ ├── CLPushUtil.dart │ └── CLUtil.dart ├── custom │ ├── CLAppbar.dart │ ├── CLFlow.dart │ ├── CLListViewRefresh.dart │ ├── CLPhotoView.dart │ ├── CLText.dart │ └── HUD.dart └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── sources.gif └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | .pub/ 7 | build/ 8 | # If you're building an application, you may want to check-in your pubspec.lock 9 | pubspec.lock 10 | 11 | # Directory created by dartdoc 12 | # If you don't generate documentation locally you can remove this line. 13 | doc/api/ 14 | 15 | .DS_Store 16 | .dart_tool/ 17 | 18 | .packages 19 | .pub/ 20 | 21 | .idea/ 22 | .vagrant/ 23 | .sconsign.dblite 24 | .svn/ 25 | 26 | *.swp 27 | profile 28 | 29 | DerivedData/ 30 | 31 | .generated/ 32 | 33 | *.pbxuser 34 | *.mode1v3 35 | *.mode2v3 36 | *.perspectivev3 37 | 38 | !default.pbxuser 39 | !default.mode1v3 40 | !default.mode2v3 41 | !default.perspectivev3 42 | 43 | xcuserdata 44 | 45 | *.moved-aside 46 | 47 | *.pyc 48 | *sync/ 49 | Icon? 50 | .tags* 51 | 52 | build/ 53 | .android/ 54 | .ios/ 55 | .flutter-plugins 56 | -------------------------------------------------------------------------------- /.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: 8661d8aecd626f7f57ccbcb735553edc05a2e713 8 | channel: stable 9 | 10 | project_type: module 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 cleven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 先上效果图 2 | ![](./sources.gif) 3 | 4 | # 此项目主要是Android及时通讯项目中的朋友圈模块 5 | 6 | 1. 为什么选择用fultter实现朋友圈模块呢? 7 | 8 | 2. 使用flutter是什么感受? 9 | 10 | 3. 你认为的flutter未来发展趋势是什么样的? 11 | 12 | 13 | ### 先讲第一个问题 14 | 为什么选择flutter,主要是谷歌2018年推出flutter,后来看了flutter发布会视频,当时的感觉是作为一个iOS开发人员真累,老是有很多公司砸我的饭碗,心塞啊~😭,但是没办法,人总要面对现实往前走,所以在过完年这段时间仔细研究了一下flutter,学完总要做点东西,要不然就是纸上谈兵说不定过几天就忘了,写点东西总能加深影响,哪怕后续忘了,回头看看自己的代码也能回忆回忆 15 | 16 | ### 第二个问题 17 | 使用flutter的感受,说实话挺爽的,跟原生比画界面flutter要快很多,而且flutter性能更高,我已经深深的爱上这么技术了,真希望能早点在公司项目中使用,flutter总体使用感受还是特别好的,比Android好十倍,Android开发是真的麻烦,可能是我习惯了iOS开发,但是Android开发给我的感觉不是很好,如果这三种排优先级的话,flutter,iOS,Android, 18 | 19 | ### 第三个问题 20 | 21 | flutter的未来发展趋势,我认为应该是很不错的,就以目前跨平台的技术来看,RN的性能并不能像flutter一样达到120fps,RN虽然是跨平台技术,但是底层还是映射成原生控件,而且RN的适配很麻烦,包体积也比Flutter要大, 22 | flutter是基于skia,skia是一套轻量级而且很成熟的引擎,flutter抛弃了以往的局限性,直接自己渲染,所以性能要更高,最后希望flutter发展越来越好吧~~ 23 | -------------------------------------------------------------------------------- /android_generated.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleven1/Flutter_Moments/fe4133e5efb5392ed6fbf90bb63818d38efacb46/android_generated.zip -------------------------------------------------------------------------------- /flutter_hybrid.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /flutter_hybrid_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/Model/CLCommentsModel.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CLCommentModel { 4 | String momentType; 5 | String momentId; 6 | List comments; 7 | String content; 8 | CLUserinfo userInfo; 9 | List momentPics; 10 | String timeStamp; 11 | String avatarUrl = ""; 12 | String aliasName = ""; 13 | 14 | bool isDidFullButton = false; 15 | bool isShowFullButton = false; 16 | 17 | CLCommentModel({this.momentType, this.avatarUrl, this.momentId, this.comments, this.content, this.userInfo, this.momentPics, this.timeStamp}); 18 | 19 | CLCommentModel.fromJson(Map json) { 20 | momentType = json['moment_type']; 21 | momentId = json['moment_id']; 22 | if (json['comments'] != null) { 23 | comments = new List(); 24 | json['comments'].forEach((v) { comments.add(new CLCommentsDetailModel.fromJson(v)); }); 25 | } 26 | content = json['content']; 27 | isShowFullButton = content.length > 100; 28 | userInfo = json['user_info'] != null ? new CLUserinfo.fromJson(json['user_info']) : null; 29 | if (json['moment_pics'] != null) { 30 | momentPics = new List(); 31 | json['moment_pics'].forEach((v) { momentPics.add(v); }); 32 | } 33 | timeStamp = json['time_stamp']; 34 | avatarUrl = json['avatar_url']; 35 | aliasName = json['alias_name']; 36 | } 37 | 38 | Map toJson() { 39 | final Map data = new Map(); 40 | data['moment_type'] = this.momentType; 41 | data['moment_id'] = this.momentId; 42 | if (this.comments != null) { 43 | data['comments'] = this.comments.map((v) => v.toJson()).toList(); 44 | } 45 | data['content'] = this.content; 46 | if (this.userInfo != null) { 47 | data['user_info'] = this.userInfo.toJson(); 48 | } 49 | if (this.momentPics != null) { 50 | // data['moment_pics'] = this.momentPics.map((v) => v.toJson()).toList(); 51 | } 52 | data['time_stamp'] = this.timeStamp; 53 | data["avatar_url"] = this.avatarUrl; 54 | data["alias_name"] = this.aliasName; 55 | return data; 56 | } 57 | } 58 | 59 | class CLCommentsDetailModel { 60 | CLUserinfo replyUserInfo; 61 | String momentId; 62 | String content; 63 | CLUserinfo userInfo; 64 | String replyUserName; 65 | String timeStamp; 66 | String aliasName; 67 | String avatarUrl; 68 | /// 判断是否有评论数据 69 | bool isEmptyContent = false; 70 | 71 | CLCommentsDetailModel({this.replyUserInfo, this.momentId, this.content, this.userInfo, this.replyUserName, this.timeStamp, this.aliasName, this.avatarUrl, this.isEmptyContent}); 72 | 73 | CLCommentsDetailModel.fromJson(Map json) { 74 | replyUserInfo = json['reply_user_info'] != null ? new CLUserinfo.fromJson(json['reply_user_info']) : null; 75 | momentId = json['moment_id']; 76 | content = json['content']; 77 | userInfo = json['user_info'] != null ? new CLUserinfo.fromJson(json['user_info']) : null; 78 | replyUserName = json['reply_user_name']; 79 | timeStamp = json['time_stamp']; 80 | aliasName = json['alias_name']; 81 | avatarUrl = json['avatar_url']; 82 | } 83 | 84 | Map toJson() { 85 | final Map data = new Map(); 86 | if (this.replyUserInfo != null) { 87 | data['reply_user_info'] = this.replyUserInfo.toJson(); 88 | } 89 | data['moment_id'] = this.momentId; 90 | data['content'] = this.content; 91 | if (this.userInfo != null) { 92 | data['user_info'] = this.userInfo.toJson(); 93 | } 94 | data['reply_user_name'] = this.replyUserName; 95 | data['time_stamp'] = this.timeStamp; 96 | data['alias_name'] = this.aliasName; 97 | data['avatar_url'] = this.avatarUrl; 98 | return data; 99 | } 100 | } 101 | 102 | class CLUserinfo { 103 | String city; 104 | String birthday; 105 | String aliasName; 106 | String name; 107 | String avatarUrl; 108 | int gender; 109 | String userId; 110 | String description; 111 | 112 | CLUserinfo( 113 | {this.city, 114 | this.birthday, 115 | this.aliasName, 116 | this.name, 117 | this.avatarUrl, 118 | this.gender, 119 | this.userId, 120 | this.description}); 121 | 122 | CLUserinfo.fromJson(Map json) { 123 | city = json['city']; 124 | birthday = json['birthday']; 125 | aliasName = json['aliasName']; 126 | name = json['name']; 127 | avatarUrl = json['avatarUrl']; 128 | gender = json['gender']; 129 | userId = json['userId']; 130 | description = json['description']; 131 | } 132 | 133 | Map toJson() { 134 | final Map data = new Map(); 135 | data['city'] = this.city; 136 | data['birthday'] = this.birthday; 137 | data['aliasName'] = this.aliasName; 138 | data['name'] = this.name; 139 | data['avatarUrl'] = this.avatarUrl; 140 | data['gender'] = this.gender; 141 | data['userId'] = this.userId; 142 | data['description'] = this.description; 143 | return data; 144 | } 145 | } -------------------------------------------------------------------------------- /lib/Model/CLMomentsModel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CLMomentsModel { 4 | String content; 5 | CLUserInfo userInfo; 6 | int likeCount; 7 | List momentPics; 8 | String timeStamp; 9 | int momentType; 10 | String aliasName; 11 | String avatarUrl; 12 | String momentId; 13 | int commentCout; 14 | 15 | /// 是否显示全文按钮 16 | bool isShowFullButton; 17 | bool isDidFullButton = false; 18 | 19 | CLMomentsModel( 20 | {this.content, 21 | this.userInfo, 22 | this.likeCount, 23 | this.momentPics, 24 | this.timeStamp, 25 | this.momentType, 26 | this.aliasName, 27 | this.avatarUrl, 28 | this.momentId, 29 | this.commentCout}); 30 | 31 | CLMomentsModel.fromJson(Map json) { 32 | content = json['content']; 33 | isShowFullButton = content.length > 100; 34 | userInfo = json['user_info'] != null 35 | ? new CLUserInfo.fromJson(json['user_info']) 36 | : null; 37 | likeCount = json['like_count']; 38 | momentPics = json['moment_pics']; 39 | timeStamp = json['time_stamp']; 40 | momentType = json['moment_type']; 41 | aliasName = json['alias_name']; 42 | avatarUrl = json['avatar_url']; 43 | momentId = json['moment_id']; 44 | commentCout = json['comment_cout']; 45 | } 46 | 47 | Map toJson() { 48 | final Map data = new Map(); 49 | data['content'] = this.content; 50 | if (this.userInfo != null) { 51 | data['user_info'] = this.userInfo.toJson(); 52 | } 53 | data['like_count'] = this.likeCount; 54 | data['moment_pics'] = this.momentPics; 55 | data['time_stamp'] = this.timeStamp; 56 | data['moment_type'] = this.momentType; 57 | data['alias_name'] = this.aliasName; 58 | data['avatar_url'] = this.avatarUrl; 59 | data['moment_id'] = this.momentId; 60 | data['comment_cout'] = this.commentCout; 61 | return data; 62 | } 63 | } 64 | 65 | class CLUserInfo { 66 | String city; 67 | String birthday; 68 | String aliasName; 69 | String name; 70 | String avatarUrl; 71 | int gender; 72 | String userId; 73 | String description; 74 | 75 | CLUserInfo( 76 | {this.city, 77 | this.birthday, 78 | this.aliasName, 79 | this.name, 80 | this.avatarUrl, 81 | this.gender, 82 | this.userId, 83 | this.description}); 84 | 85 | CLUserInfo.fromJson(Map json) { 86 | city = json['city']; 87 | birthday = json['birthday']; 88 | aliasName = json['aliasName']; 89 | name = json['name']; 90 | avatarUrl = json['avatarUrl']; 91 | gender = json['gender']; 92 | userId = json['userId']; 93 | description = json['description']; 94 | } 95 | 96 | Map toJson() { 97 | final Map data = new Map(); 98 | data['city'] = this.city; 99 | data['birthday'] = this.birthday; 100 | data['aliasName'] = this.aliasName; 101 | data['name'] = this.name; 102 | data['avatarUrl'] = this.avatarUrl; 103 | data['gender'] = this.gender; 104 | data['userId'] = this.userId; 105 | data['description'] = this.description; 106 | return data; 107 | } 108 | } -------------------------------------------------------------------------------- /lib/Moment/CLMomentsDetailPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | import '../custom/CLText.dart'; 4 | import '../custom/CLListViewRefresh.dart'; 5 | import '../Utils/CLUtil.dart'; 6 | import 'package:common_utils/common_utils.dart'; 7 | import '../Utils/CLDioUtil.dart'; 8 | import '../Model/CLCommentsModel.dart'; 9 | import '../custom/HUD.dart'; 10 | import '../custom/CLFlow.dart'; 11 | import 'package:flutter/services.dart'; 12 | 13 | 14 | class CLMomentsDetailPage extends StatefulWidget { 15 | final Widget child; 16 | 17 | CLMomentsDetailPage({Key key, this.child}): super(key: key); 18 | 19 | _CLMomentsDetailPageState createState() => _CLMomentsDetailPageState(); 20 | } 21 | 22 | class _CLMomentsDetailPageState extends State { 23 | 24 | bool isReply = false; 25 | String replyId = ""; 26 | String _momentId = ""; 27 | String _userId = "123456"; 28 | CLCommentModel _commentModel; 29 | 30 | /// 接受客户端发送过来的数据 31 | EventChannel eventChannel = EventChannel("detailChannel"); 32 | MethodChannel methodChannel = MethodChannel("detailMethodChannel"); 33 | 34 | /// 发布评论 35 | _publishMomentComment() async{ 36 | if (textEditingController.text.isEmpty) { 37 | HUD().showMessageHud(context,content: "文本不能为空"); 38 | return; 39 | } 40 | Map params = { 41 | "user_id": _userId, 42 | "content": textEditingController.text, 43 | "moment_id": _momentId, 44 | "reply_user_id": replyId, 45 | }; 46 | print("object == $params"); 47 | 48 | HUD().showHud(context); 49 | CLResultModel result = await CLDioUtil().requestPost("http://api.cleven1.com/api/moments/addComments",params: params); 50 | if(result.success){ 51 | HUD().hideHud(); 52 | HUD().showMessageHud(context,content: "发送成功"); 53 | _getMomentComentsData(); 54 | setState(() { 55 | isReply = false; 56 | }); 57 | replyId = ""; 58 | textEditingController.clear(); 59 | FocusScope.of(context).requestFocus(FocusNode()); 60 | }else{ 61 | HUD().hideHud(); 62 | HUD().showMessageHud(context,content: "发送失败"); 63 | } 64 | } 65 | 66 | /// 获取评论数据 67 | _getMomentComentsData({bool isLoadMore = false,String lastCommentId = ""}) async{ 68 | String url = "http://api.cleven1.com/api/moments/commentsInfo?isLoadMore=${isLoadMore ? "1" : "0"}&offset_id=$lastCommentId&moment_id=$_momentId"; 69 | print(url); 70 | CLResultModel result = await CLDioUtil().requestGet(url); 71 | if(result.success){ 72 | Map data = result.data["data"]; 73 | CLCommentModel commentModel = CLCommentModel.fromJson(data); 74 | print("data === $data"); 75 | print("model === ${commentModel.toJson()}"); 76 | if (isLoadMore) { 77 | /// 把数据更新放到setState中会刷新页面 78 | List tempArray = _commentModel.comments; 79 | if (commentModel.comments.isEmpty == false || commentModel.comments.length <= 0) { 80 | tempArray.addAll(commentModel.comments); 81 | } 82 | commentModel.comments = tempArray; 83 | setState(() { 84 | _commentModel = commentModel; 85 | }); 86 | }else{ 87 | setState(() { 88 | _commentModel = commentModel; 89 | }); 90 | } 91 | }else{ 92 | HUD().showMessageHud(context,content: "获取失败"); 93 | } 94 | } 95 | 96 | @override 97 | void initState() { 98 | super.initState(); 99 | _reviceNativeParams(); 100 | } 101 | 102 | /// 获取客户端传递过来的参数 103 | _reviceNativeParams() async { 104 | try { 105 | eventChannel.receiveBroadcastStream().listen((result){ 106 | print("object == $result"); 107 | _momentId = result["moment_id"]; 108 | _userId = result["user_id"]; 109 | _getMomentComentsData(); 110 | }); 111 | } on PlatformException catch (e) { 112 | print("event get data err: '${e.message}'."); 113 | } 114 | } 115 | 116 | 117 | @override 118 | void dispose() { 119 | FocusScope.of(context).requestFocus(FocusNode()); 120 | super.dispose(); 121 | } 122 | 123 | @override 124 | Widget build(BuildContext context) { 125 | return Scaffold( 126 | backgroundColor: Colors.white, 127 | body: _getListViewContainer(), 128 | ); 129 | } 130 | 131 | _getHeaderContainer(CLCommentModel momentModel){ 132 | return _getBaseContainer( 133 | momentModel, 134 | Column( 135 | crossAxisAlignment: CrossAxisAlignment.start, 136 | children: [ 137 | CLText(text: momentModel.content, maxLines: momentModel.isDidFullButton ? 10000 : 6, style: TextStyle(color: Colors.pink),), 138 | SizedBox(height: 10,), 139 | momentModel.isShowFullButton ? GestureDetector( 140 | onTap: (){ 141 | setState(() { 142 | momentModel.isDidFullButton = !momentModel.isDidFullButton; 143 | }); 144 | }, 145 | child: CLText(text: momentModel.isDidFullButton == true ? "收起" : "全文",style: TextStyle(color: Colors.blue),),) : Container(), 146 | SizedBox(height: 10,), 147 | momentModel.momentType == "1" ? CLFlow( 148 | count: momentModel.momentPics.length, 149 | children: _getImageContaniner(momentModel), 150 | ) : Container() 151 | ], 152 | ), 153 | subChild: Column( 154 | crossAxisAlignment: CrossAxisAlignment.start, 155 | children: [ 156 | SizedBox(height: 10,), 157 | CLText(text: "留言板",style: setTextStyle(fontWeight: true,fontSize: 15),), 158 | SizedBox(height: 10,), 159 | ], 160 | ) 161 | ); 162 | } 163 | 164 | 165 | _getImageContaniner(model) { 166 | List images = []; 167 | List pics = []; 168 | for (var i = 0; i < model.momentPics.length; i++) { 169 | String imageUrl = model.momentPics[i]; 170 | pics.add(imageUrl); 171 | images.add(GestureDetector( 172 | onTap: (){ 173 | // print("imageUrl == $imageUrl index == $i"); 174 | methodChannel.invokeMethod("photoBrowser",{ 175 | "index": i, 176 | "pics": pics 177 | }); 178 | // CLPushUtil().pushNavigatiton(context, CLPhotoViewBrowser(pics: pics, currentIndex: i,)); 179 | }, 180 | child: ExtendedImage.network(imageUrl,cache: true,fit: BoxFit.cover,), 181 | )); 182 | } 183 | return images; 184 | } 185 | 186 | _getListViewContainer(){ 187 | List listData = []; 188 | int count = 0; 189 | if (_commentModel == null){ 190 | listData = []; 191 | }else if(_commentModel.comments.isEmpty || _commentModel.comments.length <= 0){ 192 | count = 1; 193 | listData = [1]; 194 | }else{ 195 | listData = _commentModel.comments; 196 | count = _commentModel.comments.length + 1; 197 | } 198 | return Column( 199 | crossAxisAlignment: CrossAxisAlignment.start, 200 | children: [ 201 | Expanded( 202 | child: NotificationListener( 203 | onNotification: (ScrollNotification note) { 204 | print(note.metrics.pixels.toInt()); // 滚动位置。 205 | FocusScope.of(context).requestFocus(FocusNode()); 206 | }, 207 | child: CLListViewRefresh( 208 | listData: listData, 209 | child: ListView.builder( 210 | itemCount: count, 211 | itemBuilder: (context,index) { 212 | if (index == 0) { 213 | return _getHeaderContainer(_commentModel); 214 | } 215 | CLCommentsDetailModel commentsModel = _commentModel.comments[index - 1]; 216 | return _getListViewItemContainer(commentsModel); 217 | }, 218 | ), 219 | onRefresh: (){ 220 | _getMomentComentsData(); 221 | }, 222 | loadMore: (){ 223 | if (_commentModel.comments.isEmpty == false || _commentModel.comments.length > 0){ 224 | CLCommentsDetailModel commentsModel = _commentModel.comments.last; 225 | _getMomentComentsData(isLoadMore: true,lastCommentId: commentsModel.momentId); 226 | } 227 | }, 228 | ), 229 | ), 230 | ), 231 | _getTextFieldContainer(), 232 | SizedBox(height: 10,) 233 | ], 234 | ); 235 | } 236 | 237 | TextEditingController textEditingController = TextEditingController(); 238 | FocusNode _focusNode = new FocusNode(); 239 | _getTextFieldContainer(){ 240 | return Row( 241 | children: [ 242 | Expanded( 243 | child: TextField( 244 | controller: textEditingController, 245 | focusNode: _focusNode, 246 | maxLines: 1, 247 | maxLength: 100, 248 | textInputAction: TextInputAction.done, 249 | decoration: InputDecoration( 250 | hintText: isReply ? "回复: " : "留言:", 251 | hintStyle: TextStyle(color: Colors.grey), 252 | border: InputBorder.none 253 | ), 254 | onSubmitted: (text){ 255 | /// 隐藏键盘 256 | FocusScope.of(context).requestFocus(FocusNode()); 257 | }, 258 | ), 259 | ), 260 | FlatButton( 261 | child: CLText(text: "发送",style: TextStyle(color: Colors.redAccent),), 262 | onPressed: (){ 263 | _publishMomentComment(); 264 | }, 265 | ) 266 | ], 267 | ); 268 | } 269 | 270 | _getListViewItemContainer(CLCommentsDetailModel commentsModel){ 271 | return GestureDetector( 272 | onTap: (){ 273 | FocusScope.of(context).requestFocus(_focusNode); 274 | setState(() { 275 | replyId = commentsModel.userInfo.userId; 276 | isReply = true; 277 | }); 278 | }, 279 | child: _getBaseContainer( 280 | commentsModel, 281 | RichText( 282 | text: TextSpan( 283 | text: commentsModel.replyUserInfo == null ? commentsModel.content : "回复", 284 | style: TextStyle(color: Colors.black), 285 | children: [ 286 | TextSpan( 287 | text: commentsModel.replyUserInfo == null ? "" : "@${commentsModel.replyUserName}:", 288 | style: TextStyle(color: Colors.red) 289 | ), 290 | TextSpan( 291 | text: commentsModel.replyUserInfo == null ? "" : " ${commentsModel.content}", 292 | style: TextStyle(color: Colors.black) 293 | ) 294 | ] 295 | ), 296 | ), 297 | nameColor: Colors.blueAccent, 298 | subChild: Column( 299 | children: [ 300 | SizedBox(height: 5,), 301 | Divider(height: 1.0,), 302 | ], 303 | ), 304 | name: commentsModel.replyUserName == null ? "" : commentsModel.aliasName 305 | ), 306 | ); 307 | 308 | } 309 | 310 | _getBaseContainer(var model, Widget child ,{Widget subChild, Color nameColor, String name = ""}) { 311 | int timeStamp = model.timeStamp == null ? CLUtil.currentTimeMillis() : int.parse(model.timeStamp); 312 | String formatTime = TimelineUtil.format(timeStamp,dayFormat: DayFormat.Simple); 313 | String avatarUrl = model.avatarUrl == null ? model.userInfo.avatarUrl : model.avatarUrl; 314 | return Container( 315 | color: Colors.white, 316 | padding: EdgeInsets.only(left: 10,right: 10,top: 5), 317 | child: Column( 318 | crossAxisAlignment: CrossAxisAlignment.start, 319 | children: [ 320 | Row( 321 | crossAxisAlignment: CrossAxisAlignment.start, 322 | children: [ 323 | ExtendedImage.network( 324 | avatarUrl, 325 | width: 40, 326 | height: 40, 327 | shape: BoxShape.circle, 328 | borderRadius: BorderRadius.circular(20), 329 | cache: true, 330 | ), 331 | SizedBox(width: 10,), 332 | Expanded( 333 | child: Column( 334 | crossAxisAlignment: CrossAxisAlignment.start, 335 | children: [ 336 | CLText(text: name.isEmpty ? model.aliasName : name,style: setTextStyle(textColor: nameColor == null ? Colors.black87 : nameColor),), 337 | CLText(text: formatTime,style: setTextStyle(textColor: Colors.grey,fontSize: 12),), 338 | SizedBox(height: 5,), 339 | child, 340 | ], 341 | ), 342 | ), 343 | ], 344 | ), 345 | subChild == null ? Container() : subChild 346 | ], 347 | ), 348 | ); 349 | } 350 | 351 | } 352 | 353 | -------------------------------------------------------------------------------- /lib/Moment/CLMomentsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../custom/CLText.dart'; 3 | import 'package:extended_image/extended_image.dart'; 4 | import '../custom/CLFlow.dart'; 5 | import '../custom/CLListViewRefresh.dart'; 6 | import '../Utils/CLDioUtil.dart'; 7 | import '../Model/CLMomentsModel.dart'; 8 | import 'package:common_utils/common_utils.dart'; 9 | import '../Utils/CLUtil.dart'; 10 | import '../Utils/CLPushUtil.dart'; 11 | import 'CLPublishMomentPage.dart'; 12 | import './CLOptionsPage.dart'; 13 | import './CLPhotoViewBrowser.dart'; 14 | import 'package:flutter/services.dart'; 15 | import '../custom/HUD.dart'; 16 | 17 | class CLMomentsPage extends StatefulWidget { 18 | final Widget child; 19 | 20 | CLMomentsPage({Key key, this.child}) : super(key: key); 21 | 22 | _CLMomentsPageState createState() => _CLMomentsPageState(); 23 | } 24 | 25 | class _CLMomentsPageState extends State with AutomaticKeepAliveClientMixin{ 26 | 27 | @override 28 | bool get wantKeepAlive => true; 29 | 30 | List mList = []; 31 | 32 | /// 传值channel 33 | MethodChannel channel = MethodChannel("moment"); 34 | 35 | void initState() { 36 | super.initState(); 37 | /// 监听发布完成,更新数据 38 | channel.setMethodCallHandler((MethodCall call) async { 39 | if (call.method == "updateMomentsData"){ 40 | getMomentsData(); 41 | } 42 | }); 43 | getMomentsData(); 44 | } 45 | 46 | getMomentsData({bool isLoadMore = false,String lastId}) async { 47 | 48 | CLResultModel result = await CLDioUtil().requestGet("http://api.cleven1.com/api/moments/momentsList?isLoadMore=${isLoadMore ? 1 : 0}&offset_id=$lastId"); 49 | List jsons = result.data['data']; 50 | List tempModel = []; 51 | jsons.forEach((model){ 52 | tempModel.add(CLMomentsModel.fromJson(model)); 53 | }); 54 | if (isLoadMore) { 55 | /// 把数据更新放到setState中会刷新页面 56 | setState(() { 57 | mList.addAll(tempModel); 58 | }); 59 | }else{ 60 | setState(() { 61 | mList = tempModel; 62 | }); 63 | } 64 | 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return MaterialApp( 70 | color: Colors.white, 71 | home: Scaffold( 72 | backgroundColor: Colors.white, 73 | body: Stack( 74 | children: [ 75 | getListViewContainer(), 76 | Positioned( 77 | right: 15, 78 | bottom: 25, 79 | child: FloatingActionButton( 80 | child: Icon(Icons.add), 81 | onPressed: () async{ 82 | final String greeting = await channel.invokeMethod("gotoMomentPublish"); 83 | print(greeting); 84 | // CLPushUtil().pushNavigatiton(context, 85 | // CLPublishMomentPage(pubilshMomentsSuccess: (){ 86 | // getMomentsData(); 87 | // },) 88 | // ); 89 | }, 90 | ), 91 | ) 92 | ], 93 | ), 94 | ), 95 | ); 96 | } 97 | 98 | getListViewContainer() { 99 | 100 | return CLListViewRefresh( 101 | listData: mList, 102 | child: ListView.builder( 103 | itemCount: mList.length, 104 | itemBuilder: (BuildContext context, int index) { 105 | CLMomentsModel model = mList[index]; 106 | return model.momentType == 0 ? getItemTextContainer(model, index) : getItemImageContainer(model, index); 107 | }, 108 | ), 109 | onRefresh: (){ 110 | getMomentsData(); 111 | }, 112 | loadMore: (){ 113 | getMomentsData(isLoadMore: true,lastId: mList.last.momentId); 114 | }, 115 | ); 116 | } 117 | 118 | /// 文本布局 119 | getItemTextContainer(CLMomentsModel model ,int index){ 120 | 121 | return getItemBaseContainer( 122 | model: model, 123 | index: index, 124 | subChild: Column( 125 | crossAxisAlignment: CrossAxisAlignment.start, 126 | children: [ 127 | getTextContainer(model), 128 | model.isShowFullButton ? getFullContainer(model) : Container(), 129 | ], 130 | ) 131 | ); 132 | } 133 | 134 | /// 图片布局 135 | getItemImageContainer(CLMomentsModel model, int index){ 136 | return getItemBaseContainer( 137 | model: model, 138 | index: index, 139 | subChild: Column( 140 | crossAxisAlignment: CrossAxisAlignment.start, 141 | children: [ 142 | getTextContainer(model), 143 | model.isShowFullButton ? getFullContainer(model) : Container(), 144 | SizedBox(height: 10,), 145 | CLFlow( 146 | count: model.momentPics.length, 147 | children: getImageContaniner(model), 148 | ), 149 | ], 150 | ), 151 | ); 152 | } 153 | 154 | getTextContainer(CLMomentsModel model) { 155 | GlobalKey _myKey = new GlobalKey(); 156 | 157 | CLText text = CLText( 158 | key: _myKey, 159 | text: model.content, 160 | maxLines: model.isDidFullButton ? 100000 : 6, 161 | style: setTextStyle(textColor: Colors.pinkAccent), 162 | ); 163 | // RenderObject renderObject = _myKey.currentContext.findRenderObject(); 164 | // print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey.currentContext.size}"); 165 | return text; 166 | } 167 | 168 | getImageContaniner(CLMomentsModel model) { 169 | List images = []; 170 | List pics = []; 171 | for (var i = 0; i < model.momentPics.length; i++) { 172 | String imageUrl = model.momentPics[i]; 173 | pics.add(imageUrl); 174 | images.add(GestureDetector( 175 | onTap: (){ 176 | // print("imageUrl == $imageUrl index == $i"); 177 | print("pics == ${pics.length}"); 178 | channel.invokeMethod("photoBrowser",{ 179 | "index": i, 180 | "pics": pics 181 | }); 182 | // CLPushUtil().pushNavigatiton(context, CLPhotoViewBrowser(pics: pics, currentIndex: i,)); 183 | }, 184 | child: ExtendedImage.network(imageUrl,cache: true,fit: BoxFit.cover,), 185 | )); 186 | } 187 | return images; 188 | } 189 | 190 | getFullContainer(CLMomentsModel model){ 191 | return Container( 192 | padding: EdgeInsets.only(top: 10), 193 | child: GestureDetector( 194 | onTap: (){ 195 | setState(() { 196 | model.isDidFullButton = !model.isDidFullButton; 197 | }); 198 | }, 199 | child: CLText(text: model.isDidFullButton ? '收起' : '全文',style: setTextStyle(textColor: Colors.blue),), 200 | ) 201 | ); 202 | } 203 | 204 | getItemBaseContainer({CLMomentsModel model, Widget subChild, int index}){ 205 | int timeStamp = model.timeStamp == null ? CLUtil.currentTimeMillis() : int.parse(model.timeStamp); 206 | String formatTime = TimelineUtil.format(timeStamp,dayFormat: DayFormat.Simple); 207 | String avatarUrl = model.avatarUrl == null ? model.userInfo.avatarUrl : model.avatarUrl; 208 | return GestureDetector( 209 | onTap: () async{ 210 | print("object == $index content == ${model.content}"); 211 | final String greeting = await channel.invokeMethod("gotoDetailPage",model.momentId); 212 | print(greeting); 213 | // CLPushUtil().pushNavigatiton(context, CLMomentsDetailPage(momentModel: model,)); 214 | }, 215 | child: Container( 216 | padding: EdgeInsets.only(left: 15,right: 15,top: 15), 217 | child: Row( 218 | crossAxisAlignment: CrossAxisAlignment.start, 219 | children: [ 220 | ExtendedImage.network( 221 | avatarUrl, 222 | width: 40, 223 | height: 40, 224 | shape: BoxShape.circle, 225 | borderRadius: BorderRadius.circular(20), 226 | cache: true, 227 | ), 228 | SizedBox(width: 10,), 229 | Expanded( 230 | child: Column( 231 | crossAxisAlignment: CrossAxisAlignment.start, 232 | children: [ 233 | CLText( 234 | text: "${model.aliasName}", 235 | textAlign: TextAlign.start, 236 | style: setTextStyle(textColor: Colors.black87), 237 | ), 238 | CLText( 239 | text: formatTime,//TimelineUtil.format(int.parse(model.timeStamp),dayFormat: DayFormat.Simple),//.formatByDateTime(formatTime,locale: 'zh').toString(), 240 | style: setTextStyle(textColor: Colors.grey,fontSize: 12), 241 | ), 242 | SizedBox(height: 5,), 243 | subChild, 244 | ], 245 | ), 246 | ), 247 | GestureDetector( 248 | onTap: (){ 249 | CLOptionPage.showView( 250 | context, 251 | clickItemCallback: (CLOptionType type){ 252 | if (type == CLOptionType.delete){ 253 | print("删除"); 254 | CLOptionPage.hiddenView(); 255 | HUD().showMessageHud(context,content: "等待实现"); 256 | } 257 | }); 258 | }, 259 | child: Icon(Icons.more_horiz,), 260 | ) 261 | ], 262 | ), 263 | ), 264 | ); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /lib/Moment/CLOptionsPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../custom/HUD.dart'; 3 | 4 | enum CLOptionType { 5 | delete, 6 | report 7 | } 8 | /// 定义回调类型 9 | typedef Future OnClickItemCallback(CLOptionType optionType); 10 | 11 | class CLOptionPage extends PopupRoute { 12 | 13 | static CLOptionPage _currentOption; 14 | Duration delayed = Duration(milliseconds: 2000); 15 | OnClickItemCallback clickItemCallback; 16 | 17 | static showView(BuildContext context,{OnClickItemCallback clickItemCallback}) async{ 18 | 19 | try { 20 | if (_currentOption != null) { 21 | _currentOption.navigator.pop(); 22 | } 23 | CLOptionPage optionPage = CLOptionPage(); 24 | optionPage.clickItemCallback = clickItemCallback; 25 | _currentOption = optionPage; 26 | Navigator.push(context, optionPage); 27 | } catch (e) { 28 | _currentOption = null; 29 | } 30 | 31 | } 32 | 33 | static hiddenView() async{ 34 | _currentOption.navigator.pop(); 35 | _currentOption = null; 36 | } 37 | 38 | @override 39 | // TODO: implement barrierColor 40 | Color get barrierColor => null; 41 | 42 | @override 43 | // TODO: implement barrierLabel 44 | String get barrierLabel => null; 45 | 46 | @override 47 | // TODO: implement transitionDuration 48 | Duration get transitionDuration => kThemeAnimationDuration; 49 | 50 | @override 51 | // TODO: implement barrierDismissible 52 | bool get barrierDismissible => true; 53 | 54 | @override 55 | Widget buildPage(BuildContext context, Animation animation, 56 | Animation secondaryAnimation) { 57 | 58 | return GestureDetector( 59 | onTap: (){ 60 | hiddenView(); 61 | }, 62 | child: Container( 63 | color: Color.fromRGBO(0, 0, 0, 0.4), 64 | child: Container( 65 | color: Colors.black38, 66 | child: Column( 67 | crossAxisAlignment: CrossAxisAlignment.start, 68 | children: [ 69 | Expanded( 70 | child: Container(color: Colors.black38,), 71 | ), 72 | Container( 73 | color: Colors.white, 74 | height: 120, 75 | child: Row( 76 | crossAxisAlignment: CrossAxisAlignment.center, 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | children: [ 79 | Expanded( 80 | child: Center( 81 | child: FlatButton( 82 | onPressed: (){ 83 | clickItemCallback(CLOptionType.delete); 84 | }, 85 | child: Text("删除",style: TextStyle(color: Colors.black,fontSize: 18, decoration: TextDecoration.none),), 86 | )) 87 | ), 88 | SizedBox(width: 10,), 89 | Expanded( 90 | child: Center(child: FlatButton( 91 | onPressed: (){ 92 | clickItemCallback(CLOptionType.report); 93 | hiddenView(); 94 | HUD().showMessageHud(context,content: "举报成功"); 95 | }, 96 | child: Text("举报",style: TextStyle(color: Colors.black,fontSize: 18, decoration: TextDecoration.none),), 97 | )), 98 | ), 99 | ], 100 | ), 101 | ) 102 | ], 103 | ), 104 | ), 105 | ), 106 | ); 107 | } 108 | 109 | @override 110 | Widget buildTransitions(BuildContext context, Animation animation, 111 | Animation secondaryAnimation, Widget child) { 112 | // TODO: implement buildTransitions 113 | return super 114 | .buildTransitions(context, animation, secondaryAnimation, child); 115 | } 116 | } -------------------------------------------------------------------------------- /lib/Moment/CLPhotoViewBrowser.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | 4 | /// 图片查看 5 | class CLPhotoViewBrowser extends StatelessWidget { 6 | final Widget child; 7 | final List pics; 8 | 9 | int currentIndex = 0; 10 | 11 | CLPhotoViewBrowser({Key key, this.child, @required this.pics, this.currentIndex}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | backgroundColor: Colors.black, 17 | body: GestureDetector( 18 | onTap: (){ 19 | Navigator.pop(context); 20 | }, 21 | child: ExtendedImageGesturePageView.builder( 22 | itemBuilder: (BuildContext context, int index) { 23 | var url = pics[index]; 24 | Widget image = ExtendedImage.network( 25 | url, 26 | fit: BoxFit.contain, 27 | mode: ExtendedImageMode.Gesture, 28 | gestureConfig: GestureConfig( 29 | inPageView: true, initialScale: 1.0, 30 | //you can cache gesture state even though page view page change. 31 | //remember call clearGestureDetailsCache() method at the right time.(for example,this page dispose) 32 | cacheGesture: false 33 | ), 34 | ); 35 | image = Container( 36 | child: image, 37 | padding: EdgeInsets.all(5.0), 38 | ); 39 | if (index == currentIndex) { 40 | return Hero( 41 | tag: url + index.toString(), 42 | child: image, 43 | ); 44 | } else { 45 | return image; 46 | } 47 | }, 48 | itemCount: pics.length, 49 | onPageChanged: (int index) { 50 | currentIndex = index; 51 | }, 52 | controller: PageController( 53 | initialPage: currentIndex, 54 | ), 55 | scrollDirection: Axis.horizontal, 56 | ), 57 | ), 58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /lib/Moment/CLPublishMomentPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:ui'; 3 | import '../custom/CLText.dart'; 4 | // import 'package:photo/photo.dart'; 5 | // import 'package:photo_manager/photo_manager.dart'; 6 | import '../Utils/CLDioUtil.dart'; 7 | import '../custom/HUD.dart'; 8 | import 'package:flutter/services.dart'; 9 | 10 | /// 定义回调类型 11 | typedef Future PubilshMomentsSuccess(); 12 | 13 | class CLPublishMomentPage extends StatefulWidget { 14 | final Widget child; 15 | final String title = "发布"; 16 | final PubilshMomentsSuccess pubilshMomentsSuccess; 17 | 18 | CLPublishMomentPage({Key key, this.child, this.pubilshMomentsSuccess}): super(key: key); 19 | 20 | _CLPublishMomentPageState createState() => _CLPublishMomentPageState(); 21 | } 22 | 23 | class _CLPublishMomentPageState extends State { 24 | 25 | // List imageList = []; 26 | 27 | String _content = ''; 28 | String _userId = '123456'; 29 | 30 | EventChannel eventChannel = EventChannel("publishChannel"); 31 | MethodChannel methodChannel = MethodChannel("publishChannel"); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Scaffold( 36 | resizeToAvoidBottomPadding: false, 37 | backgroundColor: Colors.white, 38 | body: _getPublishContainer(), 39 | ); 40 | } 41 | 42 | @override 43 | void initState() { 44 | super.initState(); 45 | _reviceNativeParams(); 46 | 47 | /// 监听客户端点击发布按钮 48 | methodChannel.setMethodCallHandler((MethodCall call) async { 49 | if (call.method == "publish") { 50 | _publishMoment(); 51 | } 52 | }); 53 | } 54 | 55 | /// 获取客户端传递过来的参数 56 | _reviceNativeParams() async { 57 | try { 58 | eventChannel.receiveBroadcastStream().listen((result){ 59 | print("object == $result"); 60 | _userId = result["user_id"]; 61 | }); 62 | } on PlatformException catch (e) { 63 | print("event get data err: '${e.message}'."); 64 | } 65 | } 66 | 67 | @override 68 | void dispose() { 69 | FocusScope.of(context).requestFocus(FocusNode()); 70 | super.dispose(); 71 | } 72 | 73 | /// 发布 74 | _publishMoment() async{ 75 | print("content == $_content"); 76 | 77 | if (_content.isEmpty) { 78 | HUD().showMessageHud(context,content: "内容不能为空"); 79 | return; 80 | } 81 | Map params = { 82 | "content": _content, 83 | "user_id": _userId, 84 | "moment_pics": [], 85 | "moment_type": "0" 86 | }; 87 | HUD().showHud(context); 88 | CLResultModel result = await CLDioUtil().requestPost("http://api.cleven1.com/api/moments/publishMoments",params: params); 89 | if(result.success){ 90 | print(result.data); 91 | // widget.pubilshMomentsSuccess(); 92 | // Navigator.pop(context); 93 | /// 发布成功 94 | methodChannel.invokeListMethod("publish_finish"); 95 | HUD().hideHud(); 96 | }else{ 97 | print("发布失败== ${result.data}"); 98 | HUD().hideHud(); 99 | HUD().showMessageHud(context,content: "发布失败"); 100 | } 101 | } 102 | 103 | _getRightActions() { 104 | return [ 105 | Container( 106 | width: 65, 107 | child: FlatButton(child: CLText(text: "发布",style: TextStyle( 108 | color: Colors.orangeAccent 109 | ),), 110 | onPressed: (){ 111 | print("发布"); 112 | _publishMoment(); 113 | },), 114 | ) 115 | ]; 116 | } 117 | 118 | _getAddPhotoContainer() { 119 | return Container( 120 | height: 80, 121 | width: 80, 122 | child: RaisedButton(child: Icon(Icons.add_to_photos),onPressed: () { 123 | 124 | print('选择照片'); 125 | // _photoListParams(); 126 | },), 127 | ); 128 | } 129 | 130 | _getPublishContainer() { 131 | return Container( 132 | color: Colors.white, 133 | padding: EdgeInsets.only(left: 10,right: 10), 134 | child: Column( 135 | crossAxisAlignment: CrossAxisAlignment.start, 136 | children: [ 137 | Expanded( 138 | flex: 1, 139 | child: _getTextFieldContainer(), 140 | ), 141 | SizedBox(height: 50,), 142 | // imageList.length > 0 ? CLPhotoView(list: imageList, 143 | // onDeleteItem: (list) { 144 | // setState(() { 145 | // imageList = list; 146 | // }); 147 | // },) : _getAddPhotoContainer(), 148 | SizedBox(height: 11,) 149 | 150 | ], 151 | ), 152 | 153 | 154 | ); 155 | } 156 | 157 | _getTextFieldContainer(){ 158 | return TextField( 159 | maxLines: 100, 160 | maxLength: 500, 161 | textInputAction: TextInputAction.done, 162 | decoration: InputDecoration( 163 | hintText: "请输入文本:", 164 | hintStyle: TextStyle(color: Colors.grey), 165 | border: InputBorder.none 166 | ), 167 | onChanged: (text){ 168 | _content = text; 169 | }, 170 | onSubmitted: (text){ 171 | /// 隐藏键盘 172 | FocusScope.of(context).requestFocus(FocusNode()); 173 | }, 174 | ); 175 | } 176 | 177 | // void _photoListParams() async { 178 | // var assetPathList = await PhotoManager.getImageAsset(); 179 | // _pickAsset(PickType.onlyImage, pathList: assetPathList); 180 | // } 181 | 182 | // void _pickAsset(PickType type, {List pathList}) async { 183 | 184 | // List imgList = await PhotoPicker.pickAsset( 185 | // context: context, 186 | // // BuildContext requied 187 | 188 | // /// The following are optional parameters. 189 | // themeColor: Colors.blue, 190 | // // the title color and bottom color 191 | // padding: 3.0, 192 | // // item padding 193 | // dividerColor: Colors.white, 194 | // // divider color 195 | // disableColor: Colors.grey.shade300, 196 | // // the check box disable color 197 | // itemRadio: 0.88, 198 | // // the content item radio 199 | // maxSelected: 9, 200 | // // max picker image count 201 | // provider: I18nProvider.chinese, 202 | // // i18n provider ,default is chinese. , you can custom I18nProvider or use ENProvider() 203 | // rowCount: 5, 204 | // // item row count 205 | // textColor: Colors.white, 206 | // // text color 207 | // thumbSize: 150, 208 | // // preview thumb size , default is 64 209 | // sortDelegate: SortDelegate.common, 210 | // // default is common ,or you make custom delegate to sort your gallery 211 | // checkBoxBuilderDelegate: DefaultCheckBoxBuilderDelegate( 212 | // activeColor: Colors.white, 213 | // unselectedColor: Colors.white, 214 | // ), // default is DefaultCheckBoxBuilderDelegate ,or you make custom delegate to create checkbox 215 | 216 | // // loadingDelegate: this, // if you want to build custom loading widget,extends LoadingDelegate [see example/lib/main.dart] 217 | 218 | // badgeDelegate: const DefaultBadgeDelegate(), /// or custom class extends [BadgeDelegate] 219 | 220 | // pickType: type, // all/image/video 221 | 222 | // photoPathList: pathList, 223 | // ); 224 | // String currentSelected = ''; 225 | // if (imgList == null) { 226 | // currentSelected = "not select item"; 227 | // } else { 228 | // /// 图片路径 229 | // List r = []; 230 | // for (var e in imgList) { 231 | // var file = await e.file; 232 | // r.add(file.absolute.path); 233 | // } 234 | // currentSelected = r.join("\n\n"); 235 | // print(currentSelected); 236 | 237 | // print(imgList[0].file.toString()); 238 | // print(imgList[0].originFile.toString()); 239 | // print(imgList[0].fullData); 240 | 241 | 242 | // setState(() { 243 | // imageList = imgList; 244 | // }); 245 | // } 246 | 247 | // } 248 | } -------------------------------------------------------------------------------- /lib/Utils/CLDioUtil.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:dio/dio.dart'; 3 | 4 | class CLDioUtil { 5 | static String baseUrl = ''; 6 | static Map baseHeaders = { 7 | // "platform":"iOS", 8 | // "application/json":"text/json" 9 | }; 10 | 11 | /// 单例实现 12 | factory CLDioUtil() => _getInstance(); 13 | static CLDioUtil get instance => _getInstance(); 14 | static CLDioUtil _instance; 15 | CLDioUtil._internal(){ 16 | /// 初始化 17 | } 18 | static CLDioUtil _getInstance() { 19 | if (_instance == null) { 20 | _instance = new CLDioUtil._internal(); 21 | } 22 | return _instance; 23 | } 24 | 25 | int timeOut = 30 * 1000; 26 | setTimeOut(int timeOut){ 27 | this.timeOut = timeOut * 1000; 28 | } 29 | 30 | requestPost(String url,{Map params}) async { 31 | return await _requestBase(url, params, baseHeaders, Options(method: 'post')); 32 | } 33 | 34 | requestGet(String url,{Map params}) async { 35 | return await _requestBase(url, params, baseHeaders, Options(method: 'get')); 36 | } 37 | 38 | _requestBase(String url, Map params, Mapheader, Options option, {noTip = false}) async { 39 | 40 | /// 处理请求头 41 | Map headers = Map(); 42 | if (header !=null){ 43 | headers.addAll(header); 44 | } 45 | /// options 处理 46 | if (option !=null) { 47 | option.headers = headers; 48 | }else{ 49 | option = new Options(method: 'get'); 50 | option.headers = headers; 51 | } 52 | /// 设置连接超时 53 | option.connectTimeout = this.timeOut; 54 | var dio = new Dio(); 55 | Response response; 56 | Response errorResponse; 57 | try { 58 | if (!url.startsWith('http')) { 59 | url = baseUrl + url; 60 | } 61 | response = await dio.request(url,data: params,options: option); 62 | } on DioError catch (error) { 63 | if (error.response != null){ 64 | errorResponse = error.response; 65 | }else{ 66 | errorResponse = new Response(statusCode: 500); 67 | } 68 | if (error.type ==DioErrorType.CONNECT_TIMEOUT) { 69 | errorResponse.statusCode = -2; 70 | } 71 | return new CLResultModel(errorResponse.data, false, errorResponse.statusCode); 72 | } 73 | try { 74 | if (response.statusCode == 200 || response.statusCode == 201) { 75 | return new CLResultModel(response.data, true, response.statusCode); 76 | }else{ 77 | return new CLResultModel(errorResponse.data, false, errorResponse.statusCode); 78 | } 79 | } catch (error) { 80 | return new CLResultModel(errorResponse.data, false, errorResponse.statusCode); 81 | } 82 | 83 | } 84 | 85 | } 86 | class CLResultModel { 87 | Map data; 88 | bool success; 89 | int code; 90 | var headers; 91 | 92 | CLResultModel(this.data, this.success, this.code, {this.headers}); 93 | } 94 | 95 | -------------------------------------------------------------------------------- /lib/Utils/CLPushUtil.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CLPushUtil { 4 | 5 | pushNavigatiton(BuildContext context, Widget target){ 6 | Navigator.push( 7 | context, 8 | new PageRouteBuilder(pageBuilder: (BuildContext context, 9 | Animation animation, Animation secondaryAnimation) { 10 | // 跳转的路由对象 11 | return target; 12 | }, transitionsBuilder: ( 13 | BuildContext context, 14 | Animation animation, 15 | Animation secondaryAnimation, 16 | Widget child, 17 | ) { 18 | return _createTransition(animation, child); 19 | })); 20 | } 21 | 22 | 23 | SlideTransition _createTransition(Animation animation, Widget child) { 24 | return new SlideTransition( 25 | position: new Tween( 26 | begin: const Offset(1.0, 0.0), 27 | end: const Offset(0.0, 0.0), 28 | ).animate(animation), 29 | child: child, 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/Utils/CLUtil.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CLUtil { 4 | 5 | /** 获取屏幕宽度 */ 6 | static double getScreenWidth(BuildContext context) { 7 | return MediaQuery.of(context).size.width; 8 | } 9 | 10 | /** 获取屏幕高度 */ 11 | static double getScreenHeight(BuildContext context) { 12 | return MediaQuery.of(context).size.height; 13 | } 14 | 15 | /** 获取系统状态栏高度 */ 16 | static double getSysStatsHeight(BuildContext context) { 17 | return MediaQuery.of(context).padding.top; 18 | } 19 | 20 | /** 返回当前时间戳 */ 21 | static int currentTimeMillis() { 22 | return new DateTime.now().millisecondsSinceEpoch; 23 | } 24 | 25 | static const RollupSize_Units = ["GB", "MB", "KB", "B"]; 26 | /** 返回文件大小字符串 */ 27 | static String getRollupSize(int size) { 28 | int idx = 3; 29 | int r1 = 0; 30 | String result = ""; 31 | while (idx >= 0) { 32 | int s1 = size % 1024; 33 | size = size >> 10; 34 | if (size == 0 || idx == 0) { 35 | r1 = (r1 * 100) ~/ 1024; 36 | if (r1 > 0) { 37 | if (r1 >= 10) 38 | result = "$s1.$r1${RollupSize_Units[idx]}"; 39 | else 40 | result = "$s1.0$r1${RollupSize_Units[idx]}"; 41 | } else 42 | result = s1.toString() + RollupSize_Units[idx]; 43 | break; 44 | } 45 | r1 = s1; 46 | idx--; 47 | } 48 | return result; 49 | } 50 | /** 返回两个日期相差的天数 */ 51 | static int daysBetween(DateTime a, DateTime b, [bool ignoreTime = false]) { 52 | if (ignoreTime) { 53 | int v = a.millisecondsSinceEpoch ~/ 86400000 - 54 | b.millisecondsSinceEpoch ~/ 86400000; 55 | if (v < 0) return -v; 56 | return v; 57 | } else { 58 | int v = a.millisecondsSinceEpoch - b.millisecondsSinceEpoch; 59 | if (v < 0) v = -v; 60 | return v ~/ 86400000; 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /lib/custom/CLAppbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CLAppBar extends PreferredSize { 4 | 5 | final Widget child; 6 | final String title; 7 | final Widget leading; 8 | final List actions; 9 | static final double navHeight = 44; 10 | 11 | CLAppBar({Key key, @required this.child, this.title, this.leading, this.actions}) : super(key: key, child:child, preferredSize: Size.fromHeight(navHeight)); 12 | 13 | @override 14 | PreferredSize build(BuildContext context) { 15 | return PreferredSize( 16 | preferredSize: Size.fromHeight(navHeight), 17 | child: AppBar( 18 | iconTheme: IconThemeData(color: Colors.black54,size: 15.0), 19 | textTheme: TextTheme(title: TextStyle(color: Colors.black,fontSize: 16,fontWeight: FontWeight.normal),), 20 | backgroundColor: Colors.white, 21 | title: Text('$title',), 22 | centerTitle: true, /// 标题居中 23 | /// 设置状态栏颜色 24 | brightness: Brightness.light, 25 | /// 设置导航栏阴影效果 26 | elevation: 0.0, 27 | /// 左侧按钮 28 | leading: leading, 29 | /// 右侧按钮 30 | actions: actions 31 | ), 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /lib/custom/CLFlow.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 仿朋友圈九宫格布局 4 | class CLFlow extends StatelessWidget { 5 | final List children; 6 | /// 子view总数 7 | final int count; 8 | /// 间距 9 | final double gap; 10 | 11 | CLFlow({ 12 | Key key, 13 | this.children, 14 | @required this.count, 15 | this.gap}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Flow( 20 | delegate: CLFlowDelegate( 21 | count: count, 22 | ), 23 | children: children, 24 | ); 25 | } 26 | } 27 | 28 | class CLFlowDelegate extends FlowDelegate { 29 | final int count; 30 | final double gap; 31 | CLFlowDelegate({ 32 | @required this.count, 33 | this.gap = 10.0, 34 | }); 35 | 36 | var columns = 3; 37 | var rows = 3; 38 | double itemW = 0; 39 | double itemH = 0; 40 | double totalW = 0; 41 | 42 | @override 43 | void paintChildren(FlowPaintingContext context) { 44 | var x = gap; 45 | var y = 0.0; 46 | 47 | /// 需要重新计算,解决刷新值为0的问题 48 | getItemSize(); 49 | getColumnsNumber(count); 50 | totalW = (itemW * rows) + (gap * (rows + 1)); 51 | 52 | //计算每一个子widget的位置 53 | for (int i = 0; i < count; i++) { 54 | var w = context.getChildSize(i).width + x; 55 | if (w < totalW) { 56 | context.paintChild(i, 57 | transform: new Matrix4.translationValues( 58 | x, y, 0.0)); 59 | x += context.getChildSize(i).width + gap; 60 | } else { 61 | x = gap; 62 | y += context.getChildSize(i).height + gap; 63 | context.paintChild(i, 64 | transform: new Matrix4.translationValues( 65 | x, y, 0.0)); 66 | x += context.getChildSize(i).width + gap; 67 | } 68 | } 69 | } 70 | 71 | getColumnsNumber(int length) { 72 | if (length <= 3) { 73 | rows = length; 74 | columns = 1; 75 | } else if (length <= 6) { 76 | rows = 3; 77 | columns = 2; 78 | if (length == 4) { 79 | rows = 2; 80 | } 81 | } else { 82 | rows = 3; 83 | columns = 3; 84 | } 85 | } 86 | 87 | getItemSize() { 88 | if (count == 1) { 89 | itemW = 120; 90 | itemH = 120; 91 | }else if (count <= 3){ 92 | itemW = 80; 93 | itemH = 80; 94 | }else if (count <= 6) { 95 | itemW = 70; 96 | itemH = 70; 97 | if (count == 4) { 98 | itemW = 80; 99 | itemH = 80; 100 | } 101 | } else { 102 | itemW = 60; 103 | itemH = 60; 104 | } 105 | } 106 | 107 | /// 设置每个子view的size 108 | getConstraintsForChild(int i, BoxConstraints constraints) { 109 | getItemSize(); 110 | return BoxConstraints( 111 | minWidth: itemW, 112 | minHeight: itemH, 113 | maxWidth: itemW, 114 | maxHeight: itemH 115 | ); 116 | 117 | } 118 | 119 | /// 设置flow的size 120 | getSize(BoxConstraints constraints){ 121 | getColumnsNumber(count); 122 | getItemSize(); 123 | double h = (columns * itemH) + ((columns - 1) * gap); 124 | totalW = (itemW * rows) + (gap * (rows + 1)); 125 | return Size(totalW, h); 126 | } 127 | 128 | @override 129 | bool shouldRepaint(FlowDelegate oldDelegate) { 130 | return oldDelegate != this; 131 | } 132 | } -------------------------------------------------------------------------------- /lib/custom/CLListViewRefresh.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter_easyrefresh/ball_pulse_header.dart'; 5 | import 'package:flutter_easyrefresh/ball_pulse_footer.dart'; 6 | 7 | 8 | /// 定义回调类型 9 | typedef Future OnRefresh(); 10 | typedef Future LoadMore(); 11 | 12 | class CLListViewRefresh extends StatefulWidget { 13 | final List listData; 14 | final Widget child; 15 | final OnRefresh onRefresh; 16 | final LoadMore loadMore; 17 | 18 | CLListViewRefresh({ 19 | Key key , 20 | @required this.child, 21 | @required this.listData, 22 | this.onRefresh, 23 | this.loadMore}) : super(key: key); 24 | 25 | _CLListViewRefreshState createState() => _CLListViewRefreshState(); 26 | } 27 | 28 | class _CLListViewRefreshState extends State { 29 | @override 30 | Widget build(BuildContext context) { 31 | return getListViewContainer(); 32 | } 33 | 34 | GlobalKey easyRefreshKey; 35 | GlobalKey headerKey; 36 | GlobalKey footerKey; 37 | void initState() { 38 | super.initState(); 39 | easyRefreshKey = new GlobalKey(); 40 | headerKey = new GlobalKey(); 41 | footerKey = new GlobalKey(); 42 | } 43 | 44 | getListViewContainer() { 45 | 46 | if (widget.listData.isEmpty){ /// 没有数据时显示loading状态 47 | return Center(child: CupertinoActivityIndicator(),); 48 | } 49 | 50 | return EasyRefresh( 51 | key: easyRefreshKey, 52 | refreshHeader: getBallHeader(), 53 | refreshFooter: getBallFotter(), 54 | /// 下拉刷新 55 | onRefresh: widget.onRefresh, 56 | loadMore: widget.loadMore,/// 上拉加载 57 | behavior: ScrollOverBehavior(), 58 | autoLoad: true, 59 | child: widget.child, 60 | ); 61 | } 62 | 63 | getBallHeader(){ 64 | return BallPulseHeader( 65 | key: headerKey, 66 | ); 67 | } 68 | 69 | getBallFotter() { 70 | return BallPulseFooter( 71 | key: footerKey, 72 | ); 73 | } 74 | 75 | getCustomHeader(){ 76 | var now = DateTime.now(); 77 | var formatter = '${now.hour.toString()}:${now.minute.toString()}:${now.second.toString()}'; 78 | return ClassicsHeader( 79 | key: headerKey, 80 | refreshText: '下拉刷新', 81 | refreshReadyText: '松手刷新', 82 | refreshingText: '正在刷新', 83 | refreshedText: '刷新结束', 84 | moreInfo: '更新时间: ${formatter}', 85 | bgColor: Colors.transparent, 86 | textColor: Colors.black87, 87 | moreInfoColor: Colors.black54, 88 | showMore: true, 89 | ); 90 | } 91 | 92 | getCustomFooter(){ 93 | var now = DateTime.now(); 94 | var formatter = '${now.hour.toString()}:${now.minute.toString()}:${now.second.toString()}'; 95 | return ClassicsFooter( 96 | key: footerKey, 97 | loadText: '上拉刷新', 98 | loadReadyText: '松手刷新', 99 | loadingText: '正在加载...', 100 | loadedText: '加载完成', 101 | noMoreText: '加载完成', 102 | moreInfo: '更新时间: ${formatter}', 103 | bgColor: Colors.transparent, 104 | textColor: Colors.black87, 105 | moreInfoColor: Colors.black54, 106 | showMore: true, 107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /lib/custom/CLPhotoView.dart: -------------------------------------------------------------------------------- 1 | // import 'package:flutter/material.dart'; 2 | // import 'dart:typed_data'; 3 | // import 'package:photo_manager/photo_manager.dart'; 4 | 5 | 6 | // /// 定义回调类型 7 | // typedef Future OnDeleteItem(List list); 8 | 9 | // class AssetImageWidget extends StatelessWidget { 10 | // final AssetEntity assetEntity; 11 | // final double width; 12 | // final double height; 13 | // final BoxFit boxFit; 14 | 15 | // const AssetImageWidget({ 16 | // Key key, 17 | // // @required this.assetEntity, 18 | // this.width, 19 | // this.height, 20 | // this.boxFit, 21 | // }) : super(key: key); 22 | 23 | // @override 24 | // Widget build(BuildContext context) { 25 | // if (assetEntity == null) { 26 | // return _buildContainer(); 27 | // } 28 | // return FutureBuilder( 29 | // builder: (c, s) { 30 | // if (!s.hasData) { 31 | // return Container(); 32 | // } 33 | // var size = s.data; 34 | // return FutureBuilder( 35 | // builder: (BuildContext context, snapshot) { 36 | // if (snapshot.hasData) { 37 | // return _buildContainer( 38 | // child: Image.memory( 39 | // snapshot.data, 40 | // width: width, 41 | // height: height, 42 | // fit: boxFit, 43 | // ), 44 | // ); 45 | // } else { 46 | // return _buildContainer(); 47 | // } 48 | // }, 49 | // future: assetEntity.thumbDataWithSize( 50 | // size.width.toInt(), 51 | // size.height.toInt(), 52 | // ), 53 | // ); 54 | // }, 55 | // future: assetEntity.size, 56 | // ); 57 | // } 58 | 59 | // Widget _buildContainer({Widget child}) { 60 | // child ??= Container(width: width,height: height,); 61 | // return Container( 62 | // width: width, 63 | // height: height, 64 | // child: child, 65 | // ); 66 | // } 67 | // } 68 | 69 | // class CLPhotoView extends StatefulWidget { 70 | // final List list; 71 | // final OnDeleteItem onDeleteItem; 72 | 73 | // CLPhotoView({ 74 | // Key key, 75 | // @required this.list, 76 | // this.onDeleteItem, 77 | // }) : super(key: key); 78 | 79 | // _CLPhotoViewState createState() => _CLPhotoViewState(); 80 | // } 81 | 82 | // class _CLPhotoViewState extends State { 83 | // double spacing = 10; 84 | // @override 85 | // Widget build(BuildContext context) { 86 | // return Wrap( 87 | // spacing: spacing, 88 | // runSpacing: spacing, 89 | // alignment: WrapAlignment.start, 90 | // children: widget.list 91 | // .map((item) => Stack( 92 | // children: [ 93 | // AssetImageWidget( 94 | // assetEntity: item, 95 | // width: (MediaQuery.of(context).size.width - (4 * spacing) - (2 * spacing)) / 5, 96 | // height: 60, 97 | // boxFit: BoxFit.cover, 98 | // ), 99 | // Positioned( 100 | // right: -8, 101 | // top: -3, 102 | // child: IconButton( 103 | // icon: Icon(Icons.delete_forever,color: Colors.red,size: 20,), 104 | // iconSize: 25, 105 | // alignment: Alignment.topRight, 106 | // onPressed: (){ 107 | // setState(() { 108 | // widget.list.remove(item); 109 | // widget.onDeleteItem(widget.list); 110 | // }); 111 | // },), 112 | // ) 113 | // ], 114 | // )) 115 | // .toList(), 116 | // ); 117 | // } 118 | // } -------------------------------------------------------------------------------- /lib/custom/CLText.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CLText extends StatelessWidget { 4 | 5 | final String text; 6 | int maxLines; 7 | final TextStyle style; 8 | final TextAlign textAlign; 9 | final TextDirection textDirection; 10 | final TextOverflow overflow; 11 | 12 | CLText({ 13 | Key key, 14 | @required this.text, 15 | this.maxLines = 1, 16 | this.textAlign = TextAlign.left, 17 | this.textDirection, 18 | this.overflow = TextOverflow.ellipsis, 19 | this.style}):super(key:key); 20 | 21 | @override 22 | Text build(BuildContext context) { 23 | return Text( 24 | text, 25 | maxLines: maxLines, 26 | textAlign: textAlign, 27 | textDirection: textDirection, 28 | style: style, 29 | overflow: TextOverflow.ellipsis, 30 | ); 31 | } 32 | } 33 | TextStyle setTextStyle({Color textColor, double fontSize, bool fontWeight}) { 34 | return TextStyle( 35 | color: textColor, 36 | fontSize: fontSize, 37 | fontWeight: fontWeight == true ? FontWeight.bold :FontWeight.normal, 38 | ); 39 | } -------------------------------------------------------------------------------- /lib/custom/HUD.dart: -------------------------------------------------------------------------------- 1 | import 'package:xs_progress_hud/xs_progress_hud.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class HUD { 5 | 6 | showHud(BuildContext context) async{ 7 | XsProgressHud.show(context); 8 | } 9 | 10 | hideHud(){ 11 | XsProgressHud.hide(); 12 | // Future.delayed(Duration(milliseconds: 1500)).then((val) { 13 | 14 | // }); 15 | } 16 | 17 | Future showMessageHud(BuildContext context,{String content}) async { 18 | XsProgressHud.showMessage(context, content); 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'Moment/CLMomentsPage.dart'; 3 | import './Moment/CLMomentsDetailPage.dart'; 4 | import './Moment/CLPublishMomentPage.dart'; 5 | 6 | void main() => runApp(MyApp()); 7 | 8 | class MyApp extends StatelessWidget { 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | 13 | return MaterialApp( 14 | // initialRoute: "moments", 15 | routes: { 16 | "moments": (BuildContext ctx) => CLMomentsPage(), 17 | "detailPage": (BuildContext ctx) => CLMomentsDetailPage(), 18 | "publishPage": (BuildContext ctx) => CLPublishMomentPage(), 19 | }, 20 | color: Colors.white, 21 | home: CLMomentsPage(), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | after_layout: 5 | dependency: transitive 6 | description: 7 | name: after_layout 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.0.7" 11 | async: 12 | dependency: transitive 13 | description: 14 | name: async 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.0.8" 18 | boolean_selector: 19 | dependency: transitive 20 | description: 21 | name: boolean_selector 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.0.4" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.2" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.14.11" 39 | common_utils: 40 | dependency: "direct main" 41 | description: 42 | name: common_utils 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.1" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.1" 53 | cookie_jar: 54 | dependency: transitive 55 | description: 56 | name: cookie_jar 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.0.0" 60 | crypto: 61 | dependency: transitive 62 | description: 63 | name: crypto 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "2.0.6" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.2" 74 | decimal: 75 | dependency: transitive 76 | description: 77 | name: decimal 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "0.3.3" 81 | dio: 82 | dependency: "direct main" 83 | description: 84 | name: dio 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.1.0" 88 | extended_image: 89 | dependency: "direct main" 90 | description: 91 | name: extended_image 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "0.2.2" 95 | flutter: 96 | dependency: "direct main" 97 | description: flutter 98 | source: sdk 99 | version: "0.0.0" 100 | flutter_easyrefresh: 101 | dependency: "direct main" 102 | description: 103 | name: flutter_easyrefresh 104 | url: "https://pub.dartlang.org" 105 | source: hosted 106 | version: "1.2.7" 107 | flutter_test: 108 | dependency: "direct dev" 109 | description: flutter 110 | source: sdk 111 | version: "0.0.0" 112 | http: 113 | dependency: transitive 114 | description: 115 | name: http 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "0.12.0+1" 119 | http_client_helper: 120 | dependency: transitive 121 | description: 122 | name: http_client_helper 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "0.1.9" 126 | http_parser: 127 | dependency: transitive 128 | description: 129 | name: http_parser 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "3.1.3" 133 | matcher: 134 | dependency: transitive 135 | description: 136 | name: matcher 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "0.12.3+1" 140 | meta: 141 | dependency: transitive 142 | description: 143 | name: meta 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "1.1.6" 147 | path: 148 | dependency: transitive 149 | description: 150 | name: path 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "1.6.2" 154 | path_provider: 155 | dependency: transitive 156 | description: 157 | name: path_provider 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "0.5.0+1" 161 | pedantic: 162 | dependency: transitive 163 | description: 164 | name: pedantic 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "1.4.0" 168 | photo_view: 169 | dependency: "direct main" 170 | description: 171 | name: photo_view 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "0.2.2" 175 | quiver: 176 | dependency: transitive 177 | description: 178 | name: quiver 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "2.0.1" 182 | rational: 183 | dependency: transitive 184 | description: 185 | name: rational 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "0.3.2" 189 | sky_engine: 190 | dependency: transitive 191 | description: flutter 192 | source: sdk 193 | version: "0.0.99" 194 | source_span: 195 | dependency: transitive 196 | description: 197 | name: source_span 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "1.5.4" 201 | stack_trace: 202 | dependency: transitive 203 | description: 204 | name: stack_trace 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "1.9.3" 208 | stream_channel: 209 | dependency: transitive 210 | description: 211 | name: stream_channel 212 | url: "https://pub.dartlang.org" 213 | source: hosted 214 | version: "1.6.8" 215 | string_scanner: 216 | dependency: transitive 217 | description: 218 | name: string_scanner 219 | url: "https://pub.dartlang.org" 220 | source: hosted 221 | version: "1.0.4" 222 | term_glyph: 223 | dependency: transitive 224 | description: 225 | name: term_glyph 226 | url: "https://pub.dartlang.org" 227 | source: hosted 228 | version: "1.1.0" 229 | test_api: 230 | dependency: transitive 231 | description: 232 | name: test_api 233 | url: "https://pub.dartlang.org" 234 | source: hosted 235 | version: "0.2.2" 236 | transparent_image: 237 | dependency: transitive 238 | description: 239 | name: transparent_image 240 | url: "https://pub.dartlang.org" 241 | source: hosted 242 | version: "0.1.0" 243 | typed_data: 244 | dependency: transitive 245 | description: 246 | name: typed_data 247 | url: "https://pub.dartlang.org" 248 | source: hosted 249 | version: "1.1.6" 250 | vector_math: 251 | dependency: transitive 252 | description: 253 | name: vector_math 254 | url: "https://pub.dartlang.org" 255 | source: hosted 256 | version: "2.0.8" 257 | xs_progress_hud: 258 | dependency: "direct main" 259 | description: 260 | name: xs_progress_hud 261 | url: "https://pub.dartlang.org" 262 | source: hosted 263 | version: "1.0.2" 264 | zoomable_image: 265 | dependency: "direct main" 266 | description: 267 | name: zoomable_image 268 | url: "https://pub.dartlang.org" 269 | source: hosted 270 | version: "1.3.1" 271 | sdks: 272 | dart: ">=2.1.0 <3.0.0" 273 | flutter: ">=0.1.4 <2.0.0" 274 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_hybrid 2 | description: A new flutter module 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 | # 15 | # This version is used _only_ for the Runner app, which is used if you just do 16 | # a `flutter run` or a `flutter make-host-app-editable`. It has no impact 17 | # on any other native host app that you embed your Flutter project into. 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.1.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | dio: ^2.1.0 28 | extended_image: ^0.2.2 29 | flutter_easyrefresh: ^1.2.7 30 | # fluttertoast: ^3.0.3 31 | common_utils: ^1.1.1 32 | zoomable_image: ^1.3.1 33 | photo_view: ^0.2.2 34 | # photo: ^0.3.3 # 相册选择 35 | xs_progress_hud: ^1.0.2 36 | 37 | # The following adds the Cupertino Icons font to your application. 38 | # Use with the CupertinoIcons class for iOS style icons. 39 | cupertino_icons: ^0.1.2 40 | 41 | dev_dependencies: 42 | flutter_test: 43 | sdk: flutter 44 | 45 | # For information on the generic Dart part of this file, see the 46 | # following page: https://www.dartlang.org/tools/pub/pubspec 47 | 48 | flutter: 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add Flutter specific assets to your application, add an assets section, 55 | # like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.io/assets-and-images/#resolution-aware. 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.io/assets-and-images/#from-packages 65 | 66 | # To add Flutter specific custom fonts to your application, add a fonts 67 | # section here, in this "flutter" section. Each entry in this list should 68 | # have a "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.io/custom-fonts/#from-packages 85 | 86 | 87 | # This section identifies your Flutter project as a module meant for 88 | # embedding in a native host app. These identifiers should _not_ ordinarily 89 | # be changed after generation - they are used to ensure that the tooling can 90 | # maintain consistency when adding or modifying assets and plugins. 91 | # They also do not have any bearing on your native host application's 92 | # identifiers, which may be completely independent or the same as these. 93 | module: 94 | androidPackage: com.example.flutter_hybrid 95 | iosBundleIdentifier: com.example.flutterHybrid 96 | -------------------------------------------------------------------------------- /sources.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleven1/Flutter_Moments/fe4133e5efb5392ed6fbf90bb63818d38efacb46/sources.gif -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_hybrid/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------