menu_bar.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'dart:math';
  5. import 'package:flutter/cupertino.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/scheduler.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:image_picker/image_picker.dart';
  10. import 'package:multi_image_picker/multi_image_picker.dart';
  11. import 'package:path_provider/path_provider.dart';
  12. import 'package:sport/bean/message.dart';
  13. import 'package:sport/db/message_db.dart';
  14. import 'package:sport/pages/social/chat_page.dart';
  15. import 'package:sport/services/api/inject_api.dart';
  16. import 'package:sport/utils/toast.dart';
  17. import 'package:sport/widgets/button_primary.dart';
  18. import 'package:sport/widgets/decoration.dart';
  19. import 'package:sport/widgets/space.dart';
  20. import '../application.dart';
  21. import 'dialog/request_dialog.dart';
  22. class MenuBar extends StatefulWidget {
  23. final String inputField;
  24. final Function closeMenuCallBack;
  25. final Widget messageList;
  26. final Function scrollToBottom;
  27. final MenuIdentity menuIdentity;
  28. final Function sendCallBack;
  29. final GlobalKey globalkey;
  30. MenuBar(
  31. this.messageList, {
  32. @required this.menuIdentity,
  33. this.inputField,
  34. this.closeMenuCallBack,
  35. this.scrollToBottom,
  36. this.sendCallBack,
  37. this.globalkey,
  38. });
  39. @override
  40. State<StatefulWidget> createState() {
  41. // TODO: implement createState
  42. return _MenuBarState();
  43. }
  44. }
  45. class _MenuBarState extends State<MenuBar>
  46. with WidgetsBindingObserver, InjectApi {
  47. GlobalKey _myKey = new GlobalKey(); // 用来定位Message位置
  48. List<Asset> imageList = []; // 选图片的列表
  49. File _image; // 拍照后的图片路径
  50. String _textFieldValue = ""; // TextField的文本
  51. // TextField的 controller
  52. TextEditingController _controller;
  53. FocusNode _focusNode = new FocusNode(); // TextField 的 focus
  54. double keyBoardHeight = 270.0; // 初始化下面menu的高度 后续会动态调整后 优化
  55. bool isFirst = true; // flag 优化menu高度的操作
  56. String emojiJson; // 读取的JSON
  57. int isShowMenuBottomIndex = 0; // 用数字去优化if else 的判断
  58. double height;
  59. Timer _timer;
  60. bool showInput = true;
  61. @override
  62. void didUpdateWidget(MenuBar oldWidget) {
  63. super.didUpdateWidget(oldWidget);
  64. widget.scrollToBottom();
  65. }
  66. void initState() {
  67. super.initState();
  68. initEmoji();
  69. _controller = TextEditingController();
  70. // _focusNode.addListener(() {
  71. // if (_focusNode.hasFocus) {
  72. //// setState(() {
  73. //// isShowExtMenu = false;
  74. //// });
  75. // }
  76. // });
  77. }
  78. // 直接进来就请求了 不要 搞这些骚的....
  79. void initEmoji() async {
  80. String json = await DefaultAssetBundle.of(context)
  81. .loadString("lib/assets/json/emoji_list.json");
  82. setState(() {
  83. emojiJson = json;
  84. });
  85. }
  86. //页面销毁
  87. @override
  88. void dispose() {
  89. super.dispose();
  90. //释放
  91. _focusNode.dispose();
  92. _timer?.cancel();
  93. }
  94. // 这里 插库 + 渲染更新...
  95. Future add(MessageInstance message) async {
  96. // 聊天的时候 是没有返回 curId的 本地存储的时候 就得自己构造一个? 或者是不存?
  97. await MessageDB().insert(new MessageItem(
  98. message: message, status: 0, userId: message.toUser.id, curId: 0));
  99. var list = await MessageDB().findHasUserId(message.toUser.id);
  100. if (list.length == 0) {
  101. await MessageDB()
  102. .insertUser(new UserTableInfo(userId: message.toUser.id, isTop: 0));
  103. }
  104. // view 上setstate 渲染的...
  105. widget.sendCallBack(message); // 添加完 在 callBack...
  106. }
  107. _postFeedBackpostFeedBack(String content, {int typeId = 0}) async {
  108. typeId == null ? typeId = 0 : typeId = typeId;
  109. await api.postFeedback(typeId, content,
  110. extra: await Application.getDeviceInfo());
  111. }
  112. Widget extMenuItem(
  113. String text,
  114. String url,
  115. int index,
  116. ) {
  117. String error;
  118. void getPicture() async {
  119. try {
  120. // 这里是拿到想要发的图片
  121. List<Asset> resultList = await MultiImagePicker.pickImages(
  122. maxImages: 9,
  123. selectedAssets: imageList,
  124. materialOptions: MaterialOptions(
  125. actionBarTitle: "选择图片",
  126. allViewTitle: "选择图片",
  127. useDetailsView: true,
  128. startInAllView: true,
  129. selectionLimitReachedText: "不能选择更多了~",
  130. ),
  131. cupertinoOptions: CupertinoOptions(
  132. selectionFillColor: "#ff11ab",
  133. selectionTextColor: "#ffffff",
  134. selectionCharacter: "✓",
  135. ));
  136. List<String> urls = [];
  137. if (resultList != null) {
  138. Directory tempDir = await getTemporaryDirectory();
  139. Directory directory = new Directory('${tempDir.path}/upload');
  140. if (!directory.existsSync()) {
  141. directory.createSync();
  142. print('文档初始化成功,文件保存路径为 ${directory.path}');
  143. }
  144. for (var i = 0; i < resultList.length; i++) {
  145. Asset asset = resultList[i];
  146. ByteData byteData = await asset.getByteData(quality: 85);
  147. File file = File(
  148. '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg');
  149. List<int> bytes = byteData.buffer.asUint8List().toList();
  150. print('临时文件 ${file.path} ${bytes.length}');
  151. file.writeAsBytesSync(bytes);
  152. String url = (await api.postChatUpload(file)).data["url"];
  153. urls.add(url);
  154. file.delete();
  155. }
  156. }
  157. if (widget.menuIdentity.menuScene == "chat") {
  158. if (urls.length > 0) {
  159. for (int i = 0; i < urls.length; i++) {
  160. MessageInstance message = (await api.postChatSend(
  161. widget.menuIdentity.userId,
  162. "image",
  163. '{"url":"${urls[i]}"}'))
  164. .data;
  165. add(message);
  166. }
  167. }
  168. }
  169. } on Exception catch (e) {
  170. error = e.toString();
  171. }
  172. }
  173. void getPhoto() async {
  174. try {
  175. // 拍完直接发...
  176. final pickedFile = await new ImagePicker()
  177. .getImage(source: ImageSource.camera, imageQuality: 70);
  178. // 如果是聊天 直接发 ...
  179. if (widget.menuIdentity.menuScene == "chat") {
  180. await request(context, () async {
  181. // 先上传
  182. var data = (await api.postChatUpload(File(pickedFile.path))).data;
  183. print(data['url']);
  184. // print("${data}-----------------------------");
  185. MessageInstance message = (await api.postChatSend(
  186. widget.menuIdentity.userId,
  187. "image",
  188. '{ "url":"${data['url']}" }'))
  189. .data;
  190. add(message);
  191. });
  192. }
  193. } on Exception catch (e) {
  194. error = e.toString();
  195. }
  196. }
  197. List<Function> menuOperation = [getPicture, getPhoto];
  198. return InkWell(
  199. child: Column(
  200. crossAxisAlignment: CrossAxisAlignment.center,
  201. children: <Widget>[
  202. Container(
  203. padding: EdgeInsets.fromLTRB(14.0, 16.0, 14.0, 14.0),
  204. child: Image.asset("lib/assets/img/$url.png"),
  205. decoration: BoxDecoration(
  206. borderRadius: BorderRadius.circular(10.0),
  207. color: Color(0xfff1f1f1),
  208. ),
  209. ),
  210. SizedBox(
  211. height: 5.0,
  212. ),
  213. Text("$text")
  214. ],
  215. ),
  216. onTap: () {
  217. menuOperation[index]();
  218. },
  219. );
  220. }
  221. Widget _emoJiList() {
  222. if (emojiJson != null && emojiJson.length != 0) {
  223. List<dynamic> data = json.decode(emojiJson);
  224. return Container(
  225. height: keyBoardHeight,
  226. padding: EdgeInsets.only(left: 5, top: 5, right: 5, bottom: 5),
  227. decoration: BoxDecoration(
  228. color: Colors.white,
  229. border:
  230. Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
  231. child: GridView.custom(
  232. padding: EdgeInsets.all(3),
  233. shrinkWrap: true,
  234. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  235. crossAxisCount: 6,
  236. mainAxisSpacing: 0.5,
  237. crossAxisSpacing: 6.0,
  238. ),
  239. childrenDelegate: SliverChildBuilderDelegate(
  240. (context, index) {
  241. return GestureDetector(
  242. onTap: () {
  243. String intPutString = _controller.text +
  244. String.fromCharCode(data[index]["unicode"]);
  245. var content = intPutString;
  246. _controller.value = TextEditingValue(
  247. // 设置内容
  248. text: content,
  249. // 保持光标在最后
  250. selection: TextSelection.fromPosition(TextPosition(
  251. affinity: TextAffinity.downstream,
  252. offset: content.length)));
  253. // 主要是 onchange 没有办法 加上 表情 ...
  254. setState(() {});
  255. },
  256. child: Center(
  257. child: Text(
  258. String.fromCharCode(data[index]["unicode"]),
  259. style: TextStyle(fontSize: 33),
  260. ),
  261. ),
  262. );
  263. },
  264. childCount: data.length,
  265. ),
  266. ),
  267. );
  268. }
  269. return Container();
  270. }
  271. Widget menuBottom() {
  272. // 这里的软键盘动画还是有点问题....
  273. List<Widget> list = [
  274. Container(),
  275. SizedBox(
  276. height: MediaQuery.of(context).viewInsets.bottom,
  277. ),
  278. Container(
  279. height: keyBoardHeight,
  280. padding: EdgeInsets.only(left: 24.0, right: 24.0),
  281. decoration: BoxDecoration(
  282. color: Colors.white,
  283. border: Border(
  284. top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
  285. child: GridView(
  286. shrinkWrap: true,
  287. padding: EdgeInsets.only(top: 24.0),
  288. physics: NeverScrollableScrollPhysics(),
  289. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  290. crossAxisCount: 4, //横轴三个子widget
  291. ),
  292. children: <Widget>[
  293. extMenuItem("图片", "bbs_icon_picture", 0),
  294. extMenuItem("拍照", "bbs_icon_photo", 1),
  295. ])),
  296. _emoJiList(),
  297. ];
  298. return list[isShowMenuBottomIndex];
  299. }
  300. Widget build(BuildContext context) {
  301. // 优化 输入键盘高度 跟 菜单栏高度的操作
  302. if (isFirst && MediaQuery.of(context).viewInsets.bottom != 0.0) {
  303. setState(() {
  304. keyBoardHeight = MediaQuery.of(context).viewInsets.bottom;
  305. isFirst = false;
  306. });
  307. }
  308. return Column(
  309. children: <Widget>[
  310. Expanded(
  311. child: Column(
  312. children: <Widget>[
  313. Flexible(
  314. child: GestureDetector(
  315. onTap: () {
  316. setState(() {
  317. isShowMenuBottomIndex = 0;
  318. });
  319. _focusNode.unfocus();
  320. },
  321. child: widget.messageList,
  322. )),
  323. ],
  324. ),
  325. ),
  326. if (showInput)
  327. // Column(
  328. // children: <Widget>[
  329. //// Container(
  330. //// color: Colors.white,
  331. //// padding: EdgeInsets.only(top: 10.0,left: 10.0,right: 10.0,bottom: 10.0),
  332. //// child:
  333. //// ),
  334. // ,
  335. // ],
  336. // ),
  337. Container(
  338. padding: const EdgeInsets.symmetric(
  339. vertical: 8.0, horizontal: 12.0),
  340. decoration: shadowTop(),
  341. child: Row(
  342. children: <Widget>[
  343. GestureDetector(
  344. onTap: () async {
  345. if (_focusNode.hasFocus) {
  346. await SystemChannels.textInput
  347. .invokeMethod('TextInput.hide');
  348. setState(() {
  349. isShowMenuBottomIndex = 2;
  350. });
  351. } else {
  352. setState(() {
  353. isShowMenuBottomIndex = 2;
  354. });
  355. }
  356. },
  357. child: Padding(
  358. // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  359. child: Image.asset(
  360. "lib/assets/img/bbs_icon_addmore.png"),
  361. padding: EdgeInsets.only(right: 12.0),
  362. ),
  363. ),
  364. GestureDetector(
  365. onTap: () {
  366. setState(() {
  367. isShowMenuBottomIndex = 3;
  368. FocusScope.of(context).requestFocus(_focusNode);
  369. if (_focusNode.hasFocus) {
  370. SystemChannels.textInput
  371. .invokeMethod('TextInput.hide');
  372. }
  373. });
  374. },
  375. child: Padding(
  376. // padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
  377. child: Image.asset(
  378. "lib/assets/img/bbs_icon_expression.png"),
  379. padding: EdgeInsets.only(right: 12.0),
  380. ),
  381. ),
  382. Expanded(
  383. child: CupertinoTextField(
  384. controller: _controller,
  385. focusNode: _focusNode,
  386. keyboardType: TextInputType.multiline,
  387. style: TextStyle(
  388. fontSize: 16.0,
  389. ),
  390. strutStyle: StrutStyle(forceStrutHeight: true, height: 1.4),
  391. minLines: 1,
  392. maxLines: 3,
  393. // maxLength: 200,
  394. onChanged: (value) {
  395. setState(() {
  396. // _textFieldValue = value;
  397. });
  398. },
  399. onTap: () {
  400. setState(() {
  401. isShowMenuBottomIndex = 1;
  402. });
  403. widget.scrollToBottom();
  404. },
  405. decoration: BoxDecoration(
  406. shape: BoxShape.rectangle,
  407. borderRadius: BorderRadius.all(Radius.circular(10)),
  408. color: Color(0xfff1f1f1)
  409. ),
  410. ),
  411. ),
  412. Space(
  413. width: 5.0,
  414. ),
  415. PrimaryButton(
  416. width: 75,
  417. height: 35.0,
  418. content: "发送",
  419. callback: () async {
  420. if (_controller.text.length <= 0) {
  421. ToastUtil.show("请输入正确的内容");
  422. return;
  423. }
  424. if (widget.menuIdentity.menuScene == "chat") {
  425. MessageInstance message = (await api.postChatSend(
  426. widget.menuIdentity.userId,
  427. "text",
  428. '{"text":"${_controller.text}"}'))
  429. .data;
  430. await add(
  431. message); // await 是等待的标志 我等待完 在做后面的init 的事?
  432. _controller.text = "";
  433. }
  434. if (widget.menuIdentity.menuScene == "feedback") {
  435. await _postFeedBackpostFeedBack(_textFieldValue);
  436. }
  437. // 这里可能传不了 callback 所有需要的值都在menu bar里面 只能通过传 不同的 场景 进行 不同的操作
  438. },
  439. shadow: _controller.text.length <= 0 ? false : true,
  440. buttonColor:
  441. _controller.text == "" ? Color(0xffd2d2d2) : null,
  442. ),
  443. ],
  444. )),
  445. // 底部的骚操作
  446. menuBottom(),
  447. ],
  448. );
  449. }
  450. }
  451. // 只是用来封装 scrollToBottom
  452. class GetMenuController {
  453. ScrollController scrollMenuController = new ScrollController();
  454. // 暂时不用 这个 scrollToBottom ...
  455. void scrollToBottom(BuildContext context, GlobalKey key, bool first) {
  456. scrollMenuController.addListener(() {
  457. print("[offSet:]${scrollMenuController.offset}----------------------");
  458. });
  459. // 页面一屏的高度
  460. double pageHeight = MediaQuery.of(context).size.height;
  461. // 头部的高度...
  462. double appBarAndHeight = MediaQuery.of(context).padding.top + 56;
  463. // 尾部的高度...
  464. double bottomHeight = 160;
  465. // scrollView 的 高度 ...
  466. double scrollHeight = key.currentContext.size.height;
  467. print("[scrollHeight]:$scrollHeight----------------------------");
  468. double position = scrollMenuController?.position?.maxScrollExtent;
  469. double currentOffset = scrollMenuController.offset;
  470. double getScrollTop(bool first) {
  471. if (first) {
  472. return scrollHeight;
  473. } else {
  474. // 不需要滚动的状态... 当前 所处的位置 需不需要 加上 bottomHeight...
  475. if (currentOffset < bottomHeight) {
  476. return position + appBarAndHeight + bottomHeight;
  477. } else {
  478. return position + appBarAndHeight;
  479. }
  480. }
  481. }
  482. if (scrollMenuController.hasClients) {
  483. Future.delayed(Duration(milliseconds: 30), () {
  484. scrollMenuController?.jumpTo(getScrollTop(first));
  485. });
  486. }
  487. }
  488. // 后续修改为用这个...
  489. void scroll() {
  490. // SchedulerBinding.instance.addPostFrameCallback((_) {
  491. ////here the sublist is already build
  492. // scrollMenuController
  493. // .jumpTo(scrollMenuController.position.maxScrollExtent);
  494. // print(
  495. // "${scrollMenuController.position.minScrollExtent} - ${scrollMenuController.position.maxScrollExtent}");
  496. // });
  497. }
  498. }
  499. // 自己内部传的 bean 用来鉴别 是什么 场景使用的 menuBar
  500. class MenuIdentity {
  501. String menuScene; // 当前的MenuBar 所属的场景在哪 比如 聊天 chat / 社区 social / 反馈 feedBack 等
  502. int userId; // Chat的时候需要的userId
  503. MenuIdentity({this.menuScene, this.userId});
  504. MenuIdentity.fromJson(Map<String, dynamic> json) {
  505. menuScene = json['menuScene'];
  506. userId = json['userId'];
  507. }
  508. Map<String, dynamic> toJson() {
  509. final Map<String, dynamic> data = new Map<String, dynamic>();
  510. data['menuScene'] = this.menuScene;
  511. data['userId'] = this.userId;
  512. return data;
  513. }
  514. }