import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:multi_image_picker/multi_image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sport/bean/message.dart'; import 'package:sport/db/message_db.dart'; import 'package:sport/pages/social/chat_page.dart'; import 'package:sport/services/api/inject_api.dart'; import 'package:sport/utils/toast.dart'; import 'package:sport/widgets/button_primary.dart'; import 'package:sport/widgets/decoration.dart'; import 'package:sport/widgets/space.dart'; import '../application.dart'; import 'dialog/request_dialog.dart'; class MenuBar extends StatefulWidget { final String inputField; final Function closeMenuCallBack; final Widget messageList; final Function scrollToBottom; final MenuIdentity menuIdentity; final Function sendCallBack; final GlobalKey globalkey; MenuBar( this.messageList, { @required this.menuIdentity, this.inputField, this.closeMenuCallBack, this.scrollToBottom, this.sendCallBack, this.globalkey, }); @override State createState() { // TODO: implement createState return _MenuBarState(); } } class _MenuBarState extends State with WidgetsBindingObserver, InjectApi { GlobalKey _myKey = new GlobalKey(); // 用来定位Message位置 List imageList = []; // 选图片的列表 File _image; // 拍照后的图片路径 String _textFieldValue = ""; // TextField的文本 // TextField的 controller TextEditingController _controller; FocusNode _focusNode = new FocusNode(); // TextField 的 focus double keyBoardHeight = 270.0; // 初始化下面menu的高度 后续会动态调整后 优化 bool isFirst = true; // flag 优化menu高度的操作 String emojiJson; // 读取的JSON int isShowMenuBottomIndex = 0; // 用数字去优化if else 的判断 double height; Timer _timer; bool showInput = true; @override void didUpdateWidget(MenuBar oldWidget) { super.didUpdateWidget(oldWidget); widget.scrollToBottom(); } void initState() { super.initState(); initEmoji(); _controller = TextEditingController(); // _focusNode.addListener(() { // if (_focusNode.hasFocus) { //// setState(() { //// isShowExtMenu = false; //// }); // } // }); } // 直接进来就请求了 不要 搞这些骚的.... void initEmoji() async { String json = await DefaultAssetBundle.of(context) .loadString("lib/assets/json/emoji_list.json"); setState(() { emojiJson = json; }); } //页面销毁 @override void dispose() { super.dispose(); //释放 _focusNode.dispose(); _timer?.cancel(); } // 这里 插库 + 渲染更新... Future add(MessageInstance message) async { // 聊天的时候 是没有返回 curId的 本地存储的时候 就得自己构造一个? 或者是不存? await MessageDB().insert(new MessageItem( message: message, status: 0, userId: message.toUser.id, curId: 0)); var list = await MessageDB().findHasUserId(message.toUser.id); if (list.length == 0) { await MessageDB() .insertUser(new UserTableInfo(userId: message.toUser.id, isTop: 0)); } // view 上setstate 渲染的... widget.sendCallBack(message); // 添加完 在 callBack... } _postFeedBackpostFeedBack(String content, {int typeId = 0}) async { typeId == null ? typeId = 0 : typeId = typeId; await api.postFeedback(typeId, content, extra: await Application.getDeviceInfo()); } Widget extMenuItem( String text, String url, int index, ) { String error; void getPicture() async { try { // 这里是拿到想要发的图片 List resultList = await MultiImagePicker.pickImages( maxImages: 9, selectedAssets: imageList, materialOptions: MaterialOptions( actionBarTitle: "选择图片", allViewTitle: "选择图片", useDetailsView: true, startInAllView: true, selectionLimitReachedText: "不能选择更多了~", ), cupertinoOptions: CupertinoOptions( selectionFillColor: "#ff11ab", selectionTextColor: "#ffffff", selectionCharacter: "✓", )); List urls = []; if (resultList != null) { Directory tempDir = await getTemporaryDirectory(); Directory directory = new Directory('${tempDir.path}/upload'); if (!directory.existsSync()) { directory.createSync(); print('文档初始化成功,文件保存路径为 ${directory.path}'); } for (var i = 0; i < resultList.length; i++) { Asset asset = resultList[i]; ByteData byteData = await asset.getByteData(quality: 85); File file = File( '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg'); List bytes = byteData.buffer.asUint8List().toList(); print('临时文件 ${file.path} ${bytes.length}'); file.writeAsBytesSync(bytes); String url = (await api.postChatUpload(file)).data["url"]; urls.add(url); file.delete(); } } if (widget.menuIdentity.menuScene == "chat") { if (urls.length > 0) { for (int i = 0; i < urls.length; i++) { MessageInstance message = (await api.postChatSend( widget.menuIdentity.userId, "image", '{"url":"${urls[i]}"}')) .data; add(message); } } } } on Exception catch (e) { error = e.toString(); } } void getPhoto() async { try { // 拍完直接发... final pickedFile = await new ImagePicker() .getImage(source: ImageSource.camera, imageQuality: 70); // 如果是聊天 直接发 ... if (widget.menuIdentity.menuScene == "chat") { await request(context, () async { // 先上传 var data = (await api.postChatUpload(File(pickedFile.path))).data; print(data['url']); // print("${data}-----------------------------"); MessageInstance message = (await api.postChatSend( widget.menuIdentity.userId, "image", '{ "url":"${data['url']}" }')) .data; add(message); }); } } on Exception catch (e) { error = e.toString(); } } List menuOperation = [getPicture, getPhoto]; return InkWell( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( padding: EdgeInsets.fromLTRB(14.0, 16.0, 14.0, 14.0), child: Image.asset("lib/assets/img/$url.png"), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), color: Color(0xfff1f1f1), ), ), SizedBox( height: 5.0, ), Text("$text") ], ), onTap: () { menuOperation[index](); }, ); } Widget _emoJiList() { if (emojiJson != null && emojiJson.length != 0) { List data = json.decode(emojiJson); return Container( height: keyBoardHeight, padding: EdgeInsets.only(left: 5, top: 5, right: 5, bottom: 5), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))), child: GridView.custom( padding: EdgeInsets.all(3), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, mainAxisSpacing: 0.5, crossAxisSpacing: 6.0, ), childrenDelegate: SliverChildBuilderDelegate( (context, index) { return GestureDetector( onTap: () { String intPutString = _controller.text + String.fromCharCode(data[index]["unicode"]); var content = intPutString; _controller.value = TextEditingValue( // 设置内容 text: content, // 保持光标在最后 selection: TextSelection.fromPosition(TextPosition( affinity: TextAffinity.downstream, offset: content.length))); // 主要是 onchange 没有办法 加上 表情 ... setState(() {}); }, child: Center( child: Text( String.fromCharCode(data[index]["unicode"]), style: TextStyle(fontSize: 33), ), ), ); }, childCount: data.length, ), ), ); } return Container(); } Widget menuBottom() { // 这里的软键盘动画还是有点问题.... List list = [ Container(), SizedBox( height: MediaQuery.of(context).viewInsets.bottom, ), Container( height: keyBoardHeight, padding: EdgeInsets.only(left: 24.0, right: 24.0), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))), child: GridView( shrinkWrap: true, padding: EdgeInsets.only(top: 24.0), physics: NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, //横轴三个子widget ), children: [ extMenuItem("图片", "bbs_icon_picture", 0), extMenuItem("拍照", "bbs_icon_photo", 1), ])), _emoJiList(), ]; return list[isShowMenuBottomIndex]; } Widget build(BuildContext context) { // 优化 输入键盘高度 跟 菜单栏高度的操作 if (isFirst && MediaQuery.of(context).viewInsets.bottom != 0.0) { setState(() { keyBoardHeight = MediaQuery.of(context).viewInsets.bottom; isFirst = false; }); } return Column( children: [ Expanded( child: Column( children: [ Flexible( child: GestureDetector( onTap: () { setState(() { isShowMenuBottomIndex = 0; }); _focusNode.unfocus(); }, child: widget.messageList, )), ], ), ), if (showInput) // Column( // children: [ //// Container( //// color: Colors.white, //// padding: EdgeInsets.only(top: 10.0,left: 10.0,right: 10.0,bottom: 10.0), //// child: //// ), // , // ], // ), Container( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 12.0), decoration: shadowTop(), child: Row( children: [ GestureDetector( onTap: () async { if (_focusNode.hasFocus) { await SystemChannels.textInput .invokeMethod('TextInput.hide'); setState(() { isShowMenuBottomIndex = 2; }); } else { setState(() { isShowMenuBottomIndex = 2; }); } }, child: Padding( // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), child: Image.asset( "lib/assets/img/bbs_icon_addmore.png"), padding: EdgeInsets.only(right: 12.0), ), ), GestureDetector( onTap: () { setState(() { isShowMenuBottomIndex = 3; FocusScope.of(context).requestFocus(_focusNode); if (_focusNode.hasFocus) { SystemChannels.textInput .invokeMethod('TextInput.hide'); } }); }, child: Padding( // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), child: Image.asset( "lib/assets/img/bbs_icon_expression.png"), padding: EdgeInsets.only(right: 12.0), ), ), Expanded( child: TextField( controller: _controller, focusNode: _focusNode, keyboardType: TextInputType.multiline, style: TextStyle( fontSize: 16.0, ), strutStyle: StrutStyle(forceStrutHeight: true, height: 1.4), minLines: 1, maxLines: 3, // maxLength: 200, onChanged: (value) { setState(() { // _textFieldValue = value; }); }, onTap: () { setState(() { isShowMenuBottomIndex = 1; }); widget.scrollToBottom(); }, decoration: InputDecoration( filled: true, fillColor: Color(0xfff1f1f1), counterText: "", hintText: "${widget.inputField}", contentPadding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 0.0), border: OutlineInputBorder( borderSide: BorderSide(color: Color(0xfff1f1f1), width: 0.5), borderRadius: BorderRadius.all(Radius.circular(44.0)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Color(0xfff1f1f1), width: 1.0), borderRadius: BorderRadius.all(Radius.circular(10.0)), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: Color(0xfff1f1f1), width: 0.5), borderRadius: BorderRadius.all(Radius.circular(10.0)), ), ), ), ), Space( width: 5.0, ), PrimaryButton( width: 75, height: 35.0, content: "发送", callback: () async { if (_controller.text.length <= 0) { ToastUtil.show("请输入正确的内容"); return; } if (widget.menuIdentity.menuScene == "chat") { MessageInstance message = (await api.postChatSend( widget.menuIdentity.userId, "text", '{"text":"${_controller.text}"}')) .data; await add( message); // await 是等待的标志 我等待完 在做后面的init 的事? _controller.text = ""; } if (widget.menuIdentity.menuScene == "feedback") { await _postFeedBackpostFeedBack(_textFieldValue); } // 这里可能传不了 callback 所有需要的值都在menu bar里面 只能通过传 不同的 场景 进行 不同的操作 }, shadow: _controller.text.length <= 0 ? false : true, buttonColor: _controller.text == "" ? Color(0xffd2d2d2) : null, ), ], )), // 底部的骚操作 menuBottom(), ], ); } } // 只是用来封装 scrollToBottom class GetMenuController { ScrollController scrollMenuController = new ScrollController(); // 暂时不用 这个 scrollToBottom ... void scrollToBottom(BuildContext context, GlobalKey key, bool first) { scrollMenuController.addListener(() { print("[offSet:]${scrollMenuController.offset}----------------------"); }); // 页面一屏的高度 double pageHeight = MediaQuery.of(context).size.height; // 头部的高度... double appBarAndHeight = MediaQuery.of(context).padding.top + 56; // 尾部的高度... double bottomHeight = 160; // scrollView 的 高度 ... double scrollHeight = key.currentContext.size.height; print("[scrollHeight]:$scrollHeight----------------------------"); double position = scrollMenuController?.position?.maxScrollExtent; double currentOffset = scrollMenuController.offset; double getScrollTop(bool first) { if (first) { return scrollHeight; } else { // 不需要滚动的状态... 当前 所处的位置 需不需要 加上 bottomHeight... if (currentOffset < bottomHeight) { return position + appBarAndHeight + bottomHeight; } else { return position + appBarAndHeight; } } } if (scrollMenuController.hasClients) { Future.delayed(Duration(milliseconds: 30), () { scrollMenuController?.jumpTo(getScrollTop(first)); }); } } // 后续修改为用这个... void scroll() { // SchedulerBinding.instance.addPostFrameCallback((_) { ////here the sublist is already build // scrollMenuController // .jumpTo(scrollMenuController.position.maxScrollExtent); // print( // "${scrollMenuController.position.minScrollExtent} - ${scrollMenuController.position.maxScrollExtent}"); // }); } } // 自己内部传的 bean 用来鉴别 是什么 场景使用的 menuBar class MenuIdentity { String menuScene; // 当前的MenuBar 所属的场景在哪 比如 聊天 chat / 社区 social / 反馈 feedBack 等 int userId; // Chat的时候需要的userId MenuIdentity({this.menuScene, this.userId}); MenuIdentity.fromJson(Map json) { menuScene = json['menuScene']; userId = json['userId']; } Map toJson() { final Map data = new Map(); data['menuScene'] = this.menuScene; data['userId'] = this.userId; return data; } }