mind_map_view.dart 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart' show rootBundle;
  3. import 'dart:ui' as ui;
  4. import 'dart:io';
  5. import 'package:image/image.dart' as imageTool;
  6. import 'package:path_provider/path_provider.dart';
  7. import 'dart:convert';
  8. import '../../common/models/image_picker_type.dart';
  9. import '../../common/models/mindmap/mind_map.dart';
  10. import '../../common/models/mindmap/mind_node.dart';
  11. import '../../common/utils/o2_api_manager.dart';
  12. import '../../common/utils/x_file_assemble_control.dart';
  13. import '../../common/utils/x_mind_assemble_control.dart';
  14. import '../../common/widgets/dialogs.dart';
  15. import '../../common/widgets/final_widget.dart';
  16. import '../../common/widgets/flow_delegate.dart';
  17. import '../../common/widgets/loading.dart';
  18. import '../../common/widgets/mind_map_hyperlink.dart';
  19. import '../../common/widgets/snack_bars.dart';
  20. import '../../common/widgets/transparent_app_bar_widget.dart';
  21. import '../../o2.dart';
  22. import 'mind_painter.dart';
  23. import 'mind_map_input.dart';
  24. import 'mind_map_data.dart';
  25. import 'theme/mind_map_theme.dart';
  26. class MindMapView extends StatefulWidget {
  27. final String mapId;
  28. const MindMapView(this.mapId, {Key? key}) : super(key: key);
  29. @override
  30. MindMapViewState createState() {
  31. return MindMapViewState();
  32. }
  33. }
  34. class MindMapViewState extends State<MindMapView>
  35. with SingleTickerProviderStateMixin {
  36. final GlobalKey globalKey = GlobalKey();
  37. final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
  38. AnimationController? movePositionController;
  39. Animation<Offset>? tween;
  40. Color actionBarItemColor = Colors.black87; // 默认顶部操作栏上的按钮和标题颜色
  41. MindMap? _mindMap;
  42. MindMapData? map;
  43. Size? canvasSize;
  44. Size? mediaSize;
  45. LinePaintElement? linePaintElement;
  46. NodePaintElement? node;
  47. SelectNode? selectNode;
  48. bool inputViewIsHidden = true;
  49. bool autoFocus = false;
  50. double inputLeft = 0.0;
  51. double inputTop = 0.0;
  52. double inputWidth = 0.0;
  53. double inputHeight = 0.0;
  54. Color inputFillColor = Colors.white;
  55. TextSpan? textSpan;
  56. ui.Image? linkIconImage;
  57. var priorityImages = Map<int, ui.Image>();
  58. var progressImages = Map<int, ui.Image>();
  59. Map<String, ui.Image> mindMapImages = Map<String, ui.Image>();
  60. int numPointers = 0;
  61. var scale = 1.0; // 放大缩小 最小0.2 最大2
  62. var lastScale = 1.0; // 多次放大缩小的时候保存上一次的结果。
  63. var scaling = false;
  64. //移动位置
  65. Offset? canvasPosition;
  66. Offset? lastPosition;
  67. Offset? startMovePosition;
  68. double maxNodeX = 0.0; // 所有节点中x位置最大的 这个需要加上节点的宽度 不然右边会看不到
  69. double minNodeX = 0.0; //所有节点中x位置最小的
  70. double maxNodeY = 0.0; //所有节点中y位置最大的 这个需要加上节点的高度 不然下面会看不到
  71. double minNodeY = 0.0; //所有节点中y位置最小的
  72. PersistentBottomSheetController? bottomSheetController;
  73. // 加载数据
  74. Future loadData() async {
  75. // 先加载资源图片
  76. await _loadAssetsImage();
  77. if (widget.mapId != null) {
  78. // 获取解析json数据
  79. _mindMap = await MindMapService().mindMap(widget.mapId);
  80. if (_mindMap != null) {
  81. map = MindMapData.fromJson(json.decode(_mindMap!.content!));
  82. //todo 根据主题背景色来判断处理actionBarItemColor的颜色
  83. actionBarItemColor = Colors.white;
  84. _reloadDataFromJson();
  85. setState(() {});
  86. } else {
  87. _showErrorMessage('获取脑图数据异常!');
  88. }
  89. } else {
  90. _showErrorMessage('id不存在无法加载数据!');
  91. }
  92. }
  93. void _saveMindMapThumbnail() async {
  94. if (_mindMap == null) {
  95. _showErrorMessage("数据为空 无法保存!");
  96. return;
  97. }
  98. Loading.start(context);
  99. //画板保存成图片
  100. var image = await _rendered();
  101. var pngBytes = await image.toByteData(format: ui.ImageByteFormat.png);
  102. if (pngBytes == null) {
  103. _showErrorMessage("数据为空 无法保存!");
  104. return;
  105. }
  106. //裁剪边上的空白
  107. // var newSize = canvasSize / 1.5;
  108. // var oldImage = imageTool.Image.fromBytes(canvasSize.width.toInt(), canvasSize.height.toInt(), pngBytes.buffer.asUint8List());
  109. // var x = (canvasSize.width - newSize.width) / 2;
  110. // var y = (canvasSize.height - newSize.height) / 2;
  111. // debugPrint('x:$x ,y:$y');
  112. // var newImage = imageTool.copyCrop(oldImage, x.toInt(), y.toInt(), newSize.width.toInt(), newSize.height.toInt());
  113. //图片临时存储
  114. var tempDirPath = await getTemporaryDirectory();
  115. File file = File('${tempDirPath.path}/${_mindMap!.id ?? 'tempId'}.png')
  116. ..writeAsBytesSync(pngBytes.buffer.asInt8List());
  117. FileAssembleService().uploadImageForMindMap(_mindMap!.id!, file).then((id) {
  118. if (id != null && id.isNotEmpty) {
  119. _mindMap!.icon = id;
  120. } else {
  121. print('保存缩略图失败。。。。。没有返回id');
  122. }
  123. _saveMindMap();
  124. }).catchError((error) {
  125. print('保存缩略图失败,$error');
  126. Loading.complete(context);
  127. _showErrorMessage('保存失败!!');
  128. });
  129. }
  130. void _saveMindMap() {
  131. MindMapService().saveMindMap(_mindMap, map).then((id) {
  132. Loading.complete(context);
  133. _showErrorMessage('保存成功!');
  134. }).catchError((error) {
  135. print('保存失败,$error');
  136. Loading.complete(context);
  137. _showErrorMessage('保存失败!');
  138. });
  139. }
  140. @override
  141. void initState() {
  142. print('initState................');
  143. super.initState();
  144. movePositionController =
  145. AnimationController(duration: const Duration(milliseconds: 200), vsync: this);
  146. movePositionController!.addListener(() {
  147. setState(() {
  148. canvasPosition = tween?.value;
  149. });
  150. });
  151. loadData();
  152. }
  153. @override
  154. void dispose() {
  155. movePositionController?.dispose();
  156. super.dispose();
  157. }
  158. @override
  159. Widget build(BuildContext context) {
  160. var horizontalInitOffset = 0.0;
  161. var verticalInitOffset = 0.0;
  162. if (map != null && canvasSize != null) {
  163. mediaSize = MediaQuery.of(context).size;
  164. horizontalInitOffset = canvasSize!.width - mediaSize!.width > 0
  165. ? (canvasSize!.width - mediaSize!.width) / 2
  166. : 0.0;
  167. verticalInitOffset = canvasSize!.height - mediaSize!.height > 0
  168. ? (canvasSize!.height - mediaSize!.height) / 2
  169. : 0.0;
  170. canvasPosition ??= Offset(-horizontalInitOffset, -verticalInitOffset);
  171. }
  172. var title = '脑图';
  173. if (_mindMap != null) {
  174. title = _mindMap?.name ?? '脑图';
  175. }
  176. return Scaffold(
  177. key: _scaffoldKey,
  178. body: TransparentAppBarWidget(
  179. title: Text(
  180. title,
  181. style: TextStyle(color: actionBarItemColor, fontSize: 18),
  182. ),
  183. actions: _topBar(),
  184. backButtonColor: actionBarItemColor,
  185. body: map == null
  186. ? const Center(child: CircularProgressIndicator())
  187. : Stack(children: _contentViews())),
  188. );
  189. }
  190. ///
  191. /// 主屏内容
  192. ///
  193. List<Widget> _contentViews() {
  194. List<Widget> l = [];
  195. l.add(_canvasView());
  196. l.add(_bottomOperationBar());
  197. return l;
  198. }
  199. ///
  200. /// 脑图内容 包括一些事件
  201. ///
  202. Widget _canvasView() {
  203. return Listener(
  204. onPointerDown: (_) => numPointers++,
  205. onPointerUp: (_) => numPointers--,
  206. child: GestureDetector(
  207. child: Stack(
  208. // overflow: Overflow.clip,
  209. children: <Widget>[
  210. Container(
  211. color: map?.mapTheme.canvasBackgroundColor,
  212. ),
  213. Positioned(
  214. left: canvasPosition?.dx,
  215. top: canvasPosition?.dy,
  216. width: canvasSize?.width,
  217. height: canvasSize?.height,
  218. child: Transform.scale(
  219. scale: scale,
  220. child: Flow(
  221. delegate: CameraFlowDelegate(),
  222. children: <Widget>[
  223. CustomPaint(
  224. key: globalKey,
  225. size: canvasSize!,
  226. painter: MindMapPainter(
  227. root: node!,
  228. linePaintElement: linePaintElement,
  229. selectRect: selectNode?.selectRect,
  230. priorityImages: priorityImages,
  231. progressImages: progressImages,
  232. linkIconImage: linkIconImage,
  233. mindMapImages: mindMapImages,
  234. ),
  235. ),
  236. Offstage(
  237. child: Stack(
  238. children: <Widget>[
  239. MindMapTextEdit(
  240. inputLeft: inputLeft,
  241. inputTop: inputTop,
  242. inputWidth: inputWidth,
  243. inputHeight: inputHeight,
  244. autoFocus: autoFocus,
  245. inputFillColor: inputFillColor,
  246. textSpan: textSpan,
  247. textEditDone: (text) {
  248. var data = NodeData();
  249. data.text = text;
  250. _updateSelectData(data);
  251. },
  252. )
  253. ],
  254. ),
  255. offstage: inputViewIsHidden,
  256. )
  257. ],
  258. ),
  259. ),
  260. )
  261. ],
  262. ),
  263. onScaleStart: _scaleStart,
  264. onScaleEnd: _scaleEnd,
  265. onScaleUpdate: _scaleUpdate,
  266. onTapUp: (detail) => _tapUp(context, detail),
  267. ),
  268. );
  269. }
  270. ///
  271. /// 顶部操作栏
  272. ///
  273. List<Widget> _topBar() {
  274. if (map == null) {
  275. return <Widget>[];
  276. } else {
  277. return <Widget>[
  278. IconButton(
  279. icon: Icon(
  280. Icons.save,
  281. color: actionBarItemColor,
  282. ),
  283. onPressed: () {
  284. _saveMindMapThumbnail();
  285. })
  286. ];
  287. }
  288. }
  289. ///
  290. /// 底部节点操作栏
  291. ///
  292. Widget _bottomOperationBar() {
  293. return Offstage(
  294. offstage: !inputViewIsHidden,
  295. child: Align(
  296. alignment: Alignment.bottomCenter,
  297. child: Offstage(
  298. offstage: selectNode == null,
  299. child: Container(
  300. color: const Color.fromARGB(255, 191, 194, 199),
  301. height: kToolbarHeight,
  302. constraints: const BoxConstraints.expand(height: kToolbarHeight),
  303. child: SingleChildScrollView(
  304. scrollDirection: Axis.horizontal,
  305. child: Row(
  306. children: <Widget>[
  307. _bottomOperationButton(addChildNode, Icons.add, '下级'),
  308. Offstage(
  309. offstage: !(selectNode != null &&
  310. selectNode!.node.data.id != 'root'),
  311. child: _bottomOperationButton(
  312. addBrotherNode, Icons.library_add, '同级')),
  313. _bottomOperationButton(_editNodeText, Icons.edit, '编辑'),
  314. Offstage(
  315. offstage: !(selectNode != null &&
  316. selectNode!.node.data.id != 'root'),
  317. child: _bottomOperationButton(
  318. deleteNode, Icons.delete_forever, '删除'),
  319. ),
  320. _bottomOperationButton(
  321. _showIconTools, Icons.tag_faces, '图标'),
  322. _bottomOperationButton(
  323. _showImagePickMenu, Icons.image, '图片'),
  324. _bottomOperationButton(_showLinkDialog, Icons.link, '超链接')
  325. ],
  326. ),
  327. ),
  328. ),
  329. ),
  330. ));
  331. }
  332. ///
  333. /// 底部操作栏上的按钮
  334. ///
  335. Widget _bottomOperationButton(
  336. VoidCallback onPressed, IconData icon, String title) {
  337. return Padding(
  338. padding: const EdgeInsets.fromLTRB(5, 3, 5, 3),
  339. child: RawMaterialButton(
  340. onPressed: onPressed,
  341. child: Flex(
  342. mainAxisAlignment: MainAxisAlignment.center,
  343. direction: Axis.horizontal,
  344. children: <Widget>[
  345. Icon(
  346. icon,
  347. color: O2UI.iconColor,
  348. ),
  349. Text(
  350. title,
  351. style: TextStyle(color: O2UI.iconColor),
  352. )
  353. ]),
  354. fillColor: Colors.white,
  355. shape: RoundedRectangleBorder(
  356. side: BorderSide.none,
  357. borderRadius: BorderRadius.all(Radius.circular(5))),
  358. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  359. padding: const EdgeInsets.all(5.0),
  360. constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0)),
  361. );
  362. }
  363. Future _loadAssetsImage() async {
  364. priorityImages.clear();
  365. progressImages.clear();
  366. for (var i = 0; i < 10; i++) {
  367. var path = i == 0 ? 'images/priorityx.png' : 'images/priority$i.png';
  368. var progressPath =
  369. i == 0 ? 'images/progressx.png' : 'images/progress$i.png';
  370. var bytes = await rootBundle.load(path);
  371. var list = bytes.buffer.asUint8List();
  372. var image = await decodeImageFromList(list);
  373. priorityImages[i] = image;
  374. var progressBytes = await rootBundle.load(progressPath);
  375. var progressList = progressBytes.buffer.asUint8List();
  376. var progressImage = await decodeImageFromList(progressList);
  377. progressImages[i] = (progressImage);
  378. }
  379. var linkBytes = await rootBundle.load('images/link.png');
  380. linkIconImage = await decodeImageFromList(linkBytes.buffer.asUint8List());
  381. }
  382. ////////////////////////////event///////////////////////////////////
  383. //添加下级节点
  384. void addChildNode() {
  385. void _searchForAddChildNode(Node node) {
  386. if (node.data.id == selectNode?.node.data.id) {
  387. _addChild(node);
  388. } else {
  389. if (node.children.isNotEmpty) {
  390. for (var child in node.children) {
  391. _searchForAddChildNode(child);
  392. }
  393. }
  394. }
  395. }
  396. if (selectNode != null) {
  397. if (selectNode?.node.data.id == 'root' && map?.root != null) {
  398. _addChild(map!.root);
  399. } else {
  400. if (map?.root != null && map?.root.children != null && map?.root.children.isNotEmpty == true) {
  401. for (var child in map!.root.children) {
  402. _searchForAddChildNode(child);
  403. }
  404. }
  405. }
  406. } else {
  407. debugPrint('请选中节点。。。。');
  408. }
  409. }
  410. //添加同级节点
  411. void addBrotherNode() {
  412. bool _searchForAddBrotherNode(Node node) {
  413. if (node.children.isNotEmpty) {
  414. var findIt = false;
  415. for (var i = 0; i < node.children.length; i++) {
  416. if (node.children[i].data.id == selectNode?.node.data.id) {
  417. findIt = true;
  418. break;
  419. }
  420. }
  421. if (findIt) {
  422. _addChild(node);
  423. return true;
  424. } else {
  425. for (var i = 0; i < node.children.length; i++) {
  426. var flag = _searchForAddBrotherNode(node.children[i]);
  427. if (flag) {
  428. return true;
  429. }
  430. }
  431. }
  432. }
  433. return false;
  434. }
  435. if (map?.root != null) {
  436. _searchForAddBrotherNode(map!.root);
  437. }
  438. }
  439. void deleteNode() {
  440. bool _searchForDeleteNode(Node node) {
  441. if (node.children.isNotEmpty) {
  442. var findIt = -1;
  443. for (var i = 0; i < node.children.length; i++) {
  444. if (node.children[i].data.id == selectNode?.node.data.id) {
  445. findIt = i;
  446. break;
  447. }
  448. }
  449. if (findIt != -1) {
  450. node.children.removeAt(findIt);
  451. selectNode = null;
  452. _reloadDataFromJson();
  453. if (mounted) {
  454. setState(() {});
  455. }
  456. return true;
  457. } else {
  458. for (var i = 0; i < node.children.length; i++) {
  459. var flag = _searchForDeleteNode(node.children[i]);
  460. if (flag) {
  461. return true;
  462. }
  463. }
  464. }
  465. }
  466. return false;
  467. }
  468. O2Dialogs.showConfirmDialog(
  469. context: context, message: '确定要删除当前主题,会同时删除它的所有下级主题?')
  470. .then((action) {
  471. if (action == O2DialogAction.positive && map?.root != null) {
  472. _searchForDeleteNode(map!.root);
  473. }
  474. });
  475. }
  476. ///
  477. /// 编辑超链接 对话框
  478. ///
  479. void _showLinkDialog() async {
  480. final link = selectNode?.node.data.hyperlink ?? '';
  481. final linkTitle = selectNode?.node.data.hyperlinkTitle ?? '';
  482. final result = await Navigator.push(
  483. context,
  484. MaterialPageRoute(
  485. builder: (BuildContext context) => MindMapHyperlinkForm(
  486. hyperlink: link,
  487. hyperlinkTitle: linkTitle,
  488. ),
  489. fullscreenDialog: true,
  490. ));
  491. if (result != null) {
  492. _updateSelectData((result as NodeData));
  493. }
  494. }
  495. ///
  496. /// 弹出popWindow 显示工具: 优先级、进度
  497. void _showIconTools() {
  498. bottomSheetController = _scaffoldKey.currentState?.showBottomSheet(
  499. // bottomSheetController = showBottomSheet(
  500. // context: context,
  501. // builder:
  502. (build) {
  503. final height = mediaSize!.height / 2;
  504. var priorityList = <Widget>[];
  505. for (var i = 0; i < 10; i++) {
  506. final path = i == 0 ? 'images/priorityx.png' : 'images/priority$i.png';
  507. priorityList.add(GestureDetector(
  508. child: Padding(
  509. padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
  510. child: Image.asset(path),
  511. ),
  512. onTap: () {
  513. _tapProgressOrPriority('priority', i);
  514. },
  515. ));
  516. }
  517. var progressList = <Widget>[];
  518. for (var i = 0; i < 10; i++) {
  519. final path = i == 0 ? 'images/progressx.png' : 'images/progress$i.png';
  520. progressList.add(GestureDetector(
  521. child: Padding(
  522. padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
  523. child: Image.asset(path),
  524. ),
  525. onTap: () {
  526. _tapProgressOrPriority('progress', i);
  527. },
  528. ));
  529. }
  530. return Container(
  531. height: height,
  532. child: SingleChildScrollView(
  533. scrollDirection: Axis.vertical,
  534. child: Column(
  535. mainAxisSize: MainAxisSize.max,
  536. children: <Widget>[
  537. Container(
  538. height: 1,
  539. color: Colors.black12,
  540. ),
  541. Padding(
  542. padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
  543. child: Container(
  544. height: 8,
  545. width: 88,
  546. decoration: BoxDecoration(
  547. color: Colors.grey[300],
  548. borderRadius: const BorderRadius.all(Radius.circular(8))),
  549. ),
  550. ),
  551. const Padding(
  552. padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
  553. child: Text(
  554. '图标',
  555. style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  556. ),
  557. ),
  558. const Align(
  559. alignment: Alignment.centerLeft,
  560. child: Padding(
  561. padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
  562. child: Text(
  563. '优先级:',
  564. style: TextStyle(fontSize: 16.0, color: Colors.black45),
  565. ),
  566. ),
  567. ),
  568. Padding(
  569. padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
  570. child: Wrap(
  571. spacing: 5,
  572. runSpacing: 10,
  573. children: priorityList,
  574. ),
  575. ),
  576. const Align(
  577. alignment: Alignment.centerLeft,
  578. child: Padding(
  579. padding: EdgeInsets.fromLTRB(20, 0, 20, 10),
  580. child: Text(
  581. '进度:',
  582. style: TextStyle(fontSize: 16.0, color: Colors.black45),
  583. ),
  584. )),
  585. Padding(
  586. padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
  587. child: Wrap(
  588. spacing: 5,
  589. runSpacing: 10,
  590. children: progressList,
  591. ),
  592. )
  593. ],
  594. ),
  595. ));
  596. });
  597. bottomSheetController?.closed.whenComplete(() {
  598. bottomSheetController = null;
  599. });
  600. }
  601. void _showImagePickMenu() {
  602. final imageUrl = selectNode?.node.data.image;
  603. final imageId = selectNode?.node.data.imageId;
  604. var hasImage = false;
  605. if (imageUrl != null && imageUrl.isNotEmpty) {
  606. hasImage = true;
  607. }
  608. if (imageId != null && imageId.isNotEmpty) {
  609. hasImage = true;
  610. }
  611. showModalBottomSheet(
  612. context: context,
  613. builder: (context) {
  614. return Wrap(
  615. children: <Widget>[
  616. hasImage
  617. ? ListTile(
  618. onTap: () {
  619. Navigator.of(context).pop();
  620. //
  621. _clearNodeImage();
  622. },
  623. leading: Icon(Icons.delete),
  624. title: Text('删除图片', style: O2UI.primaryTextStyle),
  625. )
  626. : Container(
  627. height: 2,
  628. ),
  629. ListTile(
  630. onTap: () {
  631. Navigator.of(context).pop();
  632. _chooseImageAndUpload(ImagePickerType.gallery);
  633. },
  634. leading: Icon(Icons.photo_library),
  635. title: Text('相册', style: O2UI.primaryTextStyle),
  636. ),
  637. ListTile(
  638. onTap: () {
  639. Navigator.of(context).pop();
  640. _chooseImageAndUpload(ImagePickerType.camera);
  641. },
  642. leading: Icon(Icons.camera),
  643. title: Text('拍照', style: O2UI.primaryTextStyle),
  644. ),
  645. Container(
  646. color: O2UI.backgroundColor,
  647. height: 4,
  648. ),
  649. ListTile(
  650. onTap: () {
  651. Navigator.of(context).pop();
  652. },
  653. title: const Align(
  654. alignment: Alignment.center,
  655. child: Text('取消', style: O2UI.primaryTextStyle),
  656. ),
  657. )
  658. ],
  659. );
  660. });
  661. }
  662. Widget _loadingOverlay() {
  663. return Stack(
  664. children: [
  665. const Opacity(
  666. opacity: 0.5,
  667. child: ModalBarrier(dismissible: false, color: Colors.black),
  668. ),
  669. const Center(
  670. child: CircularProgressIndicator(),
  671. ),
  672. ],
  673. );
  674. }
  675. void _scaleStart(ScaleStartDetails detail) {
  676. if (numPointers == 1 && !scaling) {
  677. lastPosition = canvasPosition; //标记下位置
  678. startMovePosition = detail.focalPoint;
  679. }
  680. if (numPointers == 2) {
  681. if (!scaling) {
  682. scaling = true;
  683. }
  684. }
  685. }
  686. void _scaleUpdate(ScaleUpdateDetails detail) {
  687. if (numPointers == 1 && !scaling) {
  688. final distance = detail.focalPoint - startMovePosition!;
  689. final newPosition = lastPosition! + distance; //移动
  690. var x = newPosition.dx;
  691. var minX = 0 - _scaleX(maxNodeX);
  692. var maxX = mediaSize == null
  693. ? 0 - _scaleX(minNodeX)
  694. : 0 - _scaleX(minNodeX) + mediaSize!.width;
  695. if (x > maxX) {
  696. x = maxX;
  697. }
  698. if (x < minX) {
  699. x = minX;
  700. }
  701. var y = newPosition.dy;
  702. var minY = 0 - _scaleY(maxNodeY);
  703. var maxY = mediaSize == null
  704. ? 0 - _scaleY(minNodeY)
  705. : 0 - _scaleY(minNodeY) + mediaSize!.height;
  706. if (y > maxY) {
  707. y = maxY;
  708. }
  709. if (y < minY) {
  710. y = minY;
  711. }
  712. canvasPosition = Offset(x, y); //移动
  713. setState(() {});
  714. }
  715. if (numPointers == 2) {
  716. if (scaling) {
  717. var newScale = lastScale * detail.scale;
  718. if (newScale < 0.2) {
  719. newScale = 0.2;
  720. } else if (newScale > 1.0) {
  721. newScale = 1.0;
  722. }
  723. scale = newScale;
  724. // 计算大小
  725. setState(() {});
  726. }
  727. }
  728. }
  729. void _scaleEnd(ScaleEndDetails detail) {
  730. if (scaling) {
  731. lastScale = scale;
  732. scaling = false;
  733. }
  734. }
  735. void _tapUp(BuildContext context, TapUpDetails detail) {
  736. debugPrint('_tapup.............');
  737. _closeMoreTool();
  738. if (autoFocus) {
  739. _closeEdit(); //点击取消
  740. } else {
  741. RenderBox? canvasBox = globalKey.currentContext?.findRenderObject() as RenderBox?;
  742. Offset? canvasLocal = canvasBox?.globalToLocal(detail.globalPosition);
  743. debugPrint('canvasLocal.:$canvasLocal');
  744. if (canvasLocal != null && node != null) {
  745. selectNode = _checkTapNode(node!, canvasLocal);
  746. }
  747. _moveSelectPositionToCenter();
  748. }
  749. setState(() {});
  750. }
  751. void _moveSelectPositionToCenter() {
  752. if (selectNode != null) {
  753. // 居中显示
  754. var x = 0 - _scaleX(selectNode!.node.offset!.dx);
  755. var y = 0 - _scaleY(selectNode!.node.offset!.dy);
  756. x = mediaSize == null
  757. ? x
  758. : x +
  759. mediaSize!.width / 2 -
  760. (selectNode!.node.nodeSize.width * scale) / 2;
  761. y = mediaSize == null
  762. ? y
  763. : y +
  764. mediaSize!.height / 2 -
  765. (selectNode!.node.nodeSize.height * scale) / 2;
  766. // canvasPosition = Offset(x, y); //移动
  767. final old = canvasPosition;
  768. final end = Offset(x, y);
  769. print("old:$old");
  770. print('end:$end');
  771. if (movePositionController!=null) {
  772. tween = MaterialPointArcTween(begin: old, end: end)
  773. .animate(movePositionController!);
  774. movePositionController!.forward(from: 0.0);
  775. }
  776. }
  777. }
  778. //////////////////////////////////////////////////////////////////////////
  779. // 考虑放大缩小的问题
  780. double _scaleX(double x) {
  781. if (scale < 1) {
  782. return x * scale + (canvasSize!.width - canvasSize!.width * scale) / 2;
  783. } else {
  784. return x;
  785. }
  786. }
  787. double _scaleY(double y) {
  788. if (scale < 1) {
  789. return y * scale + (canvasSize!.height - canvasSize!.height * scale) / 2;
  790. } else {
  791. return y;
  792. }
  793. }
  794. void _closeMoreTool() {
  795. if (bottomSheetController != null) {
  796. bottomSheetController!.close();
  797. }
  798. }
  799. ///
  800. /// 设置 进度 或 优先级
  801. /// tag : progress priority
  802. ///
  803. void _tapProgressOrPriority(String tag, int result) {
  804. _closeMoreTool();
  805. var data = NodeData();
  806. switch (tag) {
  807. case 'progress':
  808. data.progress = result;
  809. break;
  810. case 'priority':
  811. data.priority = result;
  812. break;
  813. default:
  814. break;
  815. }
  816. _updateSelectData(data);
  817. }
  818. ///
  819. /// 文字编辑
  820. ///
  821. void _editNodeText() {
  822. debugPrint('编辑文字。。。。。');
  823. if (selectNode != null) {
  824. debugPrint('select node is here!!!!!!');
  825. final selectFill = selectNode!.node.paintElements[NodeElement.background];
  826. final selectText = selectNode!.node.paintElements[NodeElement.text];
  827. if (selectText != null && selectText is TextPaintElement) {
  828. debugPrint('text paint is here !.........');
  829. inputWidth = selectText.painter.width;
  830. inputHeight = selectText.painter.height;
  831. if (selectText.painter.text != null) {
  832. textSpan = selectText.painter.text as TextSpan;
  833. }
  834. inputLeft = selectText.offset?.dx ?? 0;
  835. inputTop = selectText.offset?.dy ?? 0;
  836. inputViewIsHidden = false;
  837. autoFocus = true;
  838. }
  839. if (selectFill != null) {
  840. debugPrint('select node background is here!!.....');
  841. inputFillColor = (selectFill as RRectPaintElement).style.color;
  842. } else {
  843. inputFillColor = map!.mapTheme.canvasBackgroundColor;
  844. }
  845. setState(() {});
  846. }
  847. }
  848. ///
  849. /// 关闭文字编辑
  850. ///
  851. void _closeEdit() {
  852. debugPrint('_closeEdit...........');
  853. inputWidth = 0.0;
  854. inputHeight = 0.0;
  855. textSpan = null;
  856. inputLeft = 0.0;
  857. inputTop = 0.0;
  858. inputViewIsHidden = true;
  859. autoFocus = false;
  860. }
  861. ///
  862. /// 修改选中的节点的数据
  863. ///
  864. void _updateSelectData(NodeData data) {
  865. debugPrint('修改数据并刷新');
  866. _closeEdit(); // 关闭编辑
  867. if (selectNode?.node.data.id == 'root' && map?.root.data != null) {
  868. _setData(map!.root.data, data);
  869. } else {
  870. if (map?.root.children != null && map?.root.children.isNotEmpty == true && selectNode?.node.data != null) {
  871. for (var child in map!.root.children) {
  872. _searchForUpdateSelectData(child, data, selectNode!.node.data.id ?? '');
  873. }
  874. }
  875. }
  876. // reload
  877. _reloadDataFromJson();
  878. selectNode = null;
  879. setState(() {});
  880. }
  881. void _searchForUpdateSelectData(Node node, NodeData data, String selectId) {
  882. if (node.data.id == selectId) {
  883. _setData(node.data, data);
  884. } else {
  885. if (node.children != null && node.children.length > 0) {
  886. for (var child in node.children) {
  887. _searchForUpdateSelectData(child, data, selectId);
  888. }
  889. }
  890. }
  891. }
  892. void _setData(NodeData oldData, NodeData newData) {
  893. if (newData.text != null) {
  894. oldData.text = newData.text;
  895. }
  896. if (newData.progress != null) {
  897. oldData.progress = newData.progress;
  898. }
  899. if (newData.priority != null) {
  900. oldData.priority = newData.priority;
  901. }
  902. if (newData.image != null || newData.imageId != null) {
  903. oldData.image = newData.image;
  904. oldData.imageId = newData.imageId;
  905. oldData.imageTitle = newData.imageTitle;
  906. oldData.imageSize = newData.imageSize;
  907. }
  908. if (newData.hyperlink != null) {
  909. oldData.hyperlink = newData.hyperlink;
  910. oldData.hyperlinkTitle = newData.hyperlinkTitle;
  911. }
  912. }
  913. //给Node添加子节点
  914. void _addChild(Node n) {
  915. int childNum = n.children == null ? 0 : n.children.length;
  916. childNum++;
  917. final time = DateTime.now().millisecond;
  918. final id = 'mind_$time';
  919. NodeData data = NodeData(id: id, created: time, text: '子主题$childNum');
  920. Node child = Node(data: data, children: []);
  921. if (n.children.isNotEmpty) {
  922. var children = n.children;
  923. children.add(child);
  924. n.children = children;
  925. } else {
  926. n.children = <Node>[child];
  927. }
  928. // reload
  929. _reloadDataFromJson();
  930. // selected
  931. selectNode = _autoSelect(node!, id);
  932. _moveSelectPositionToCenter();
  933. // 马上编辑
  934. _editNodeText();
  935. }
  936. //获取Data计算所有的内容
  937. void _reloadDataFromJson() {
  938. node = map!.mapTheme.calElementSize(map!.root);
  939. _cacheImage(node!);
  940. canvasSize = map!.mapTemplate.canvasSize(node!);
  941. mediaSize ??= MediaQuery.of(context).size;
  942. if (mediaSize != null && mediaSize!.width > 0) {
  943. double newWidth = canvasSize!.width;
  944. double newHeight = canvasSize!.height;
  945. if (canvasSize!.width < mediaSize!.width) {
  946. newWidth = mediaSize!.width;
  947. }
  948. if (canvasSize!.height < mediaSize!.height) {
  949. newHeight = mediaSize!.height;
  950. }
  951. canvasSize = Size(newWidth, newHeight);
  952. }
  953. linePaintElement = map!.mapTemplate.paintElementPosition(node!, canvasSize!);
  954. // position计算完成后 获取边界的节点
  955. void searchEdgeOffsetChild(NodePaintElement child) {
  956. if (minNodeX > child.offset!.dx) {
  957. minNodeX = child.offset!.dx;
  958. }
  959. if (maxNodeX < child.offset!.dx) {
  960. maxNodeX = child.offset!.dx;
  961. }
  962. if (minNodeY > child.offset!.dy) {
  963. minNodeY = child.offset!.dy;
  964. }
  965. if (maxNodeY < child.offset!.dy) {
  966. maxNodeY = child.offset!.dy;
  967. }
  968. if (child.children != null && child.children!.length > 0) {
  969. for (var i = 0; i < child.children!.length; i++) {
  970. searchEdgeOffsetChild(child.children![i]);
  971. }
  972. }
  973. }
  974. void searchEdgeOffset() {
  975. //初始化最大最小位置
  976. var positionX = node?.offset!.dx;
  977. var positionY = node?.offset!.dy;
  978. minNodeX = positionX!;
  979. maxNodeX = positionX;
  980. minNodeY = positionY!;
  981. maxNodeY = positionY;
  982. if (node?.children != null && node?.children!.isNotEmpty == true) {
  983. for (var i = 0; i < node!.children!.length; i++) {
  984. searchEdgeOffsetChild(node!.children![i]);
  985. }
  986. }
  987. }
  988. searchEdgeOffset();
  989. debugPrint(
  990. '查询边界结果, minx:$minNodeX , maxx:$maxNodeX ,miny:$minNodeY , maxy:$maxNodeY');
  991. }
  992. ///
  993. /// 选中节点
  994. ///
  995. SelectNode? _checkTapNode(NodePaintElement? node, Offset localPosition) {
  996. if (node != null) {
  997. final left = node.offset!.dx;
  998. final top = node.offset!.dy;
  999. final width = node.nodeSize.width;
  1000. final height = node.nodeSize.height;
  1001. if (left <= localPosition.dx &&
  1002. localPosition.dx <= left + width &&
  1003. top <= localPosition.dy &&
  1004. localPosition.dy <= top + height) {
  1005. Rect rect = Rect.fromLTWH(left, top, width, height);
  1006. RRectPaintElement selectrect = RRectPaintElement(
  1007. RRect.fromRectAndRadius(rect, const Radius.circular(5.0)),
  1008. PaintStyle(
  1009. color: Colors.redAccent,
  1010. style: PaintingStyle.stroke,
  1011. strokeWidth: 2.0));
  1012. return SelectNode(node, selectrect);
  1013. } else {
  1014. if (node.children != null && node.children!.isNotEmpty) {
  1015. for (var child in node.children!) {
  1016. var select = _checkTapNode(child, localPosition);
  1017. if (select != null) {
  1018. return select;
  1019. }
  1020. }
  1021. }
  1022. }
  1023. }
  1024. return null;
  1025. }
  1026. ///
  1027. /// 选中节点
  1028. ///
  1029. SelectNode? _autoSelect(NodePaintElement? node, String id) {
  1030. if (node != null) {
  1031. final left = node.offset!.dx;
  1032. final top = node.offset!.dy;
  1033. final width = node.nodeSize.width;
  1034. final height = node.nodeSize.height;
  1035. if (node.data.id == id) {
  1036. Rect rect = Rect.fromLTWH(left, top, width, height);
  1037. RRectPaintElement selectrect = RRectPaintElement(
  1038. RRect.fromRectAndRadius(rect, Radius.circular(5.0)),
  1039. PaintStyle(
  1040. color: Colors.redAccent,
  1041. style: PaintingStyle.stroke,
  1042. strokeWidth: 2.0));
  1043. return SelectNode(node, selectrect);
  1044. } else {
  1045. if (node.children != null && node.children!.isNotEmpty) {
  1046. for (var child in node.children!) {
  1047. var select = _autoSelect(child, id);
  1048. if (select != null) {
  1049. return select;
  1050. }
  1051. }
  1052. }
  1053. }
  1054. }
  1055. return null;
  1056. }
  1057. void _cacheImage(NodePaintElement node) {
  1058. if (node.data.image != null || node.data.imageId != null) {
  1059. var url = node.data.image;
  1060. if (node.data.imageId != null && node.data.imageId != 'null' && node.data.imageId?.isNotEmpty == true) {
  1061. url = O2ApiManager.instance.getFileURL(node.data.imageId);
  1062. }
  1063. if (url !=null && url != 'null' && url.isNotEmpty) {
  1064. _loadNetworkImage(url);
  1065. }
  1066. }
  1067. if (node.children != null && node.children!.isNotEmpty) {
  1068. for (var child in node.children!) {
  1069. _cacheImage(child);
  1070. }
  1071. }
  1072. }
  1073. ///
  1074. /// 画布中的网络图片异步获取刷新
  1075. ///
  1076. void _loadNetworkImage(String url) {
  1077. ///
  1078. /// 下载网络图片 ,然后更新画布
  1079. ///
  1080. if (!mindMapImages.containsKey(url)) {
  1081. Image.network(url).image.resolve(createLocalImageConfiguration(context))
  1082. .addListener(ImageStreamListener(
  1083. (ImageInfo imageInfo, bool synchronousCall) async {
  1084. var img = imageInfo.image;
  1085. if (mounted) {
  1086. mindMapImages[url] = img;
  1087. setState(() {});
  1088. }
  1089. }));
  1090. }
  1091. }
  1092. // final ImagePicker _picker = ImagePicker();
  1093. void _chooseImageAndUpload(ImagePickerType type) async {
  1094. //第一步 选择照片或拍照
  1095. var backFile = await O2MethodChannelManager.instance.methodChannel.invokeMethod(method_name_o2_pick_image, {
  1096. "source": type.name//gallery,camera
  1097. });
  1098. // var xfile = await _picker.pickImage(source: source);
  1099. // var file = await ImagePicker.pickImage(
  1100. // source: source, maxWidth: 400, maxHeight: 400);
  1101. if (backFile != null && backFile is Map) {
  1102. if (backFile.containsKey(param_name_o2_picker_image_file)) {
  1103. var pFile = backFile[param_name_o2_picker_image_file] as String?;
  1104. debugPrintStack(label: "选择图片 $pFile");
  1105. if (pFile != null && pFile.isNotEmpty) {
  1106. //第二步 上传图片 返回id
  1107. Loading.start(context);
  1108. var id = await FileAssembleService()
  1109. .uploadImageForMindMap(_mindMap!.id!, File(pFile), scale: 400);
  1110. if (id != null && id.isNotEmpty) {
  1111. print('上传成功id:$id');
  1112. var url = O2ApiManager.instance.getFileURL(id);
  1113. //更新缓存
  1114. _loadNetworkImage(url);
  1115. //根据node的Image信息
  1116. await _readImageSizeAndUpdateNode(File(pFile), url, id);
  1117. Loading.complete(context);
  1118. } else {
  1119. Loading.complete(context);
  1120. _showErrorMessage('图片上传失败');
  1121. }
  1122. } else {
  1123. debugPrintStack(label: '没有选择图片。pFile为空。。。');
  1124. }
  1125. } else {
  1126. debugPrintStack(label: '没有选择图片。。file为空。。');
  1127. }
  1128. } else {
  1129. debugPrintStack(label: '没有选择图片。。。。');
  1130. }
  1131. }
  1132. Future<Null> _readImageSizeAndUpdateNode(
  1133. File file, String url, String id) async {
  1134. //第三步 获取图片大小
  1135. var imageInfo = imageTool.decodeImage(file.readAsBytesSync());
  1136. if (imageInfo != null) {
  1137. //第四步 更新node数据 imageId image imageSize(最大200)
  1138. var h = imageInfo.height;
  1139. var w = imageInfo.width;
  1140. print('图片宽度:$w, 高度:$h');
  1141. var size = _scaleImageSize(w, h);
  1142. NodeData data = NodeData();
  1143. data.imageId = id;
  1144. data.image = url;
  1145. data.imageSize = size;
  1146. _updateSelectData(data);
  1147. }
  1148. return null;
  1149. }
  1150. void _clearNodeImage() {
  1151. NodeData data = NodeData();
  1152. data.imageId = '';
  1153. data.image = '';
  1154. data.imageSize = null;
  1155. _updateSelectData(data);
  1156. }
  1157. ImageSize _scaleImageSize(int width, int height) {
  1158. int newH = height <= 0 ? 200 : height;
  1159. int newW = width <= 0 ? 200 : width;
  1160. if (height > width) {
  1161. if (height > 200) {
  1162. var scale = height / 200;
  1163. newH = 200;
  1164. newW = width ~/ scale;
  1165. }
  1166. } else {
  1167. if (width > 200) {
  1168. var scale = width / 200;
  1169. newH = height ~/ scale;
  1170. newW = 200;
  1171. }
  1172. }
  1173. return ImageSize(width: newW, height: newH);
  1174. }
  1175. void _showErrorMessage(String message) {
  1176. O2SnackBars.showSnackBar(_scaffoldKey, message);
  1177. }
  1178. ///
  1179. /// 画板生成图片
  1180. ///
  1181. Future<ui.Image> _rendered() async {
  1182. ui.PictureRecorder recorder = ui.PictureRecorder();
  1183. Canvas canvas = Canvas(recorder);
  1184. MindMapPainter painter = MindMapPainter(
  1185. root: node!,
  1186. linePaintElement: linePaintElement,
  1187. selectRect: null,
  1188. priorityImages: priorityImages,
  1189. progressImages: progressImages,
  1190. linkIconImage: linkIconImage,
  1191. mindMapImages: mindMapImages,
  1192. );
  1193. painter.paint(canvas, canvasSize!);
  1194. var image = await recorder
  1195. .endRecording()
  1196. .toImage(canvasSize!.width.floor(), canvasSize!.height.floor());
  1197. return image;
  1198. }
  1199. }