children = node.getChildren();
55 | float spacing = horizontal ? verticalSpacing : horizontalSpacing;
56 | float levelSpacing = horizontal ? horizontalSpacing : verticalSpacing;
57 |
58 | float totalSpacing = spacing * (children.size() - 1);
59 | float startOffset = -totalSpacing / 2;
60 |
61 | for (int i = 0; i < children.size(); i++) {
62 | MindNode child = children.get(i);
63 | float offset = startOffset + (i * spacing);
64 |
65 | if (horizontal) {
66 | float childX = parentX + (isFirstHalf ? -levelSpacing : levelSpacing);
67 | float childY = parentY + offset;
68 | child.setX(childX);
69 | child.setY(childY);
70 | } else {
71 | float childX = parentX + offset;
72 | float childY = parentY + (isFirstHalf ? -levelSpacing : levelSpacing);
73 | child.setX(childX);
74 | child.setY(childY);
75 | }
76 |
77 | layoutSubtree(child, child.getX(), child.getY(), isFirstHalf, horizontal);
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
29 |
30 |
36 |
37 |
43 |
44 |
50 |
51 |
52 |
57 |
58 |
64 |
65 |
71 |
72 |
78 |
79 |
85 |
86 |
87 |
92 |
93 |
99 |
100 |
104 |
105 |
106 |
107 |
115 |
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌳 Android Mind Map / Android 思维导图
2 |
3 |
4 |
5 | [](https://developer.android.com)
6 | [](https://kotlinlang.org)
7 | [](LICENSE)
8 |
9 | [English](#english) | [中文](#中文)
10 |
11 |
12 |
13 | ---
14 |
15 | ## 🌟 English
16 |
17 | ### 📱 Introduction
18 | An Android mind map application that supports multiple layout styles and interactive operations. Built with design patterns for better maintainability and extensibility.
19 |
20 | ### 🏗️ Architecture & Design Patterns
21 | - **Strategy Pattern**: Different layout algorithms implementation
22 | - **Factory Pattern**: Layout strategy creation
23 | - **Builder Pattern**: Mind map node construction
24 | - **Observer Pattern**: Node event handling
25 | - **MVC Pattern**: Basic project architecture
26 |
27 | ### 📂 Project Structure
28 | ```
29 | app/src/main/java/org/fireking/xmind/
30 | ├── layout/ # Layout strategies
31 | │ ├── LayoutStrategy.java # Layout strategy interface
32 | │ ├── LayoutFactory.java # Layout factory
33 | │ ├── HorizontalLayout.java # Horizontal layout implementation
34 | │ ├── VerticalLayout.java # Vertical layout implementation
35 | │ ├── RadialLayout.java # Radial layout implementation
36 | │ ├── TreeLayout.java # Tree layout implementation
37 | │ └── BalancedLayout.java # Balanced layout implementation
38 | ├── model/ # Data models
39 | │ └── MindNodeBuilder.java # Node builder
40 | ├── MindMapView.java # Custom view for mind map
41 | ├── MindNode.java # Node data structure
42 | ├── LayoutType.java # Layout type enum
43 | ├── LineStyle.java # Line style enum
44 | └── MainActivity.kt # Main activity
45 | ```
46 |
47 | ### ✨ Features
48 | - 📋 **Multiple Layout Styles**:
49 | - 🔄 Horizontal Layout
50 | - ↕️ Vertical Layout (Up/Down)
51 | - 🌟 Radial Layout (Clockwise/Anticlockwise)
52 | - 🌲 Tree Layout
53 | - ⚖️ Balanced Layout
54 | - 🎨 **Visual Effects**:
55 | - 🎯 Node Selection
56 | - 🌈 Gradient Colors
57 | - 💫 Smooth Animations
58 | - 🔧 **Interactive Operations**:
59 | - 👆 Node Click
60 | - ✋ Node Drag
61 | - 🔍 Zoom & Pan
62 | - 〽️ **Connection Styles**:
63 | - 📏 Straight Lines
64 | - 💫 Bezier Curves
65 |
66 | ### 📝 Usage Example
67 | ```kotlin
68 | // 1. Add MindMapView to your layout
69 |
73 |
74 | // 2. Create mind map in your activity
75 | class MainActivity : AppCompatActivity(), MindMapView.NodeEventListener {
76 | override fun onCreate(savedInstanceState: Bundle?) {
77 | super.onCreate(savedInstanceState)
78 | setContentView(R.layout.activity_main)
79 |
80 | mindMapView = findViewById(R.id.mindMapView)
81 | mindMapView.addNodeEventListener(this)
82 |
83 | // Create root node using builder pattern
84 | val rootNode = MindNodeBuilder("Android Development")
85 | .setPosition(screenWidth / 2, screenHeight / 2)
86 | .setLevel(0)
87 | .build()
88 |
89 | mindMapView.setRootNode(rootNode)
90 | }
91 |
92 | override fun onNodeClick(node: MindNode) {
93 | // Handle node click
94 | }
95 |
96 | override fun onNodeDrag(node: MindNode, dx: Float, dy: Float) {
97 | // Handle node drag
98 | }
99 | }
100 | ```
101 |
102 | ### ⚠️ Notes
103 | - 💻 Recommended for larger screens
104 | - 🚀 Consider performance with many nodes
105 | - 🔧 Adjust spacing parameters as needed
106 |
107 | ### 📅 Roadmap
108 | - [ ] 📝 Node editing
109 | - [ ] 💾 Data persistence
110 | - [ ] 📤 Import/Export
111 | - [ ] 🎨 Theme customization
112 | - [ ] ➕ More layout algorithms
113 |
114 | ---
115 |
116 | ## 🌟 中文
117 |
118 | ### 📱 简介
119 | 一个支持多种布局样式和交互操作的 Android 思维导图应用。采用设计模式构建,具有良好的可维护性和扩展性。
120 |
121 | ### 🏗️ 架构与设计模式
122 | - **策略模式**: 实现不同的布局算法
123 | - **工厂模式**: 创建布局策略
124 | - **建造者模式**: 构建思维导图节点
125 | - **观察者模式**: 处理节点事件
126 | - **MVC模式**: 基本项目架构
127 |
128 | ### 📂 项目结构
129 | ```
130 | app/src/main/java/org/fireking/xmind/
131 | ├── layout/ # 布局策略
132 | │ ├── LayoutStrategy.java # 布局策略接口
133 | │ ├── LayoutFactory.java # 布局工厂
134 | │ ├── HorizontalLayout.java # 水平布局实现
135 | │ ├── VerticalLayout.java # 垂直布局实现
136 | │ ├── RadialLayout.java # 辐射布局实现
137 | │ ├── TreeLayout.java # 树形布局实现
138 | │ └── BalancedLayout.java # 均衡布局实现
139 | ├── model/ # 数据模型
140 | │ └── MindNodeBuilder.java # 节点构建器
141 | ├── MindMapView.java # 自定义视图
142 | ├── MindNode.java # 节点数据结构
143 | ├── LayoutType.java # 布局类型枚举
144 | ├── LineStyle.java # 连线样式枚举
145 | └── MainActivity.kt # 主活动
146 | ```
147 |
148 | ### ✨ 功能特点
149 | - 📋 **多种布局样式**:
150 | - 🔄 水平布局
151 | - ↕️ 垂直布局(向上/向下)
152 | - 🌟 辐射布局(顺时针/逆时针)
153 | - 🌲 树形布局
154 | - ⚖️ 均衡布局
155 | - 🎨 **视觉效果**:
156 | - 🎯 节点选中
157 | - 🌈 渐变色彩
158 | - 💫 平滑动画
159 | - 🔧 **交互操作**:
160 | - 👆 节点点击
161 | - ✋ 节点拖拽
162 | - 🔍 缩放平移
163 | - 〽️ **连接样式**:
164 | - 📏 直线连接
165 | - 💫 贝塞尔曲线
166 |
167 | ### ⚠️ 注意事项
168 | - 💻 建议在较大屏幕设备上使用
169 | - 🚀 节点数量过多时注意性能影响
170 | - 🔧 可以通过调整间距参数优化显示效果
171 |
172 | ### 📅 后续规划
173 | - [ ] 📝 支持节点编辑
174 | - [ ] 💾 数据持久化
175 | - [ ] 📤 导入导出功能
176 | - [ ] 🎨 主题定制
177 | - [ ] ➕ 更多布局算法
178 |
179 | ---
180 |
181 |
182 |
183 | **Made with ❤️ by [FireKing]**
184 |
185 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/org/fireking/xmind/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.fireking.xmind
2 |
3 | import android.os.Bundle
4 | import android.widget.Button
5 | import android.widget.Toast
6 | import androidx.appcompat.app.AppCompatActivity
7 | import org.fireking.xmind.model.MindNodeBuilder
8 |
9 | class MainActivity : AppCompatActivity(), MindMapView.NodeEventListener {
10 |
11 | private lateinit var mindMapView: MindMapView
12 | private lateinit var btnLineStyle: Button
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 |
18 | mindMapView = findViewById(R.id.mindMapView)
19 | mindMapView.addNodeEventListener(this)
20 | btnLineStyle = findViewById(R.id.btnLineStyle)
21 |
22 | // 初始化布局按钮点击事件
23 | setupLayoutButtons()
24 |
25 | // 创建示例数据
26 | createDemoMindMap()
27 |
28 | // 确保初始状态是曲线
29 | mindMapView.setLineStyle(LineStyle.BEZIER)
30 | }
31 |
32 | private fun setupLayoutButtons() {
33 | // 布局按钮映射
34 | val layoutButtons = mapOf(
35 | R.id.btnHorizontal to LayoutType.HORIZONTAL,
36 | R.id.btnVerticalDown to LayoutType.VERTICAL_DOWN,
37 | R.id.btnVerticalUp to LayoutType.VERTICAL_UP,
38 | R.id.btnRadialClockwise to LayoutType.RADIAL_CLOCKWISE,
39 | R.id.btnRadialAnticlockwise to LayoutType.RADIAL_ANTICLOCKWISE,
40 | R.id.btnTree to LayoutType.TREE,
41 | R.id.btnBalancedHorizontal to LayoutType.BALANCED_HORIZONTAL,
42 | R.id.btnBalancedVertical to LayoutType.BALANCED_VERTICAL
43 | )
44 |
45 | // 设置布局按钮点击事件
46 | layoutButtons.forEach { (buttonId, layoutType) ->
47 | findViewById