├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
├── publish-mavencentral.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── graphview
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── dev
│ └── bandb
│ └── graphview
│ ├── AbstractGraphAdapter.kt
│ ├── decoration
│ └── edge
│ │ ├── ArrowDecoration.kt
│ │ ├── ArrowEdgeDecoration.kt
│ │ └── StraightEdgeDecoration.kt
│ ├── graph
│ ├── Edge.kt
│ ├── Graph.kt
│ └── Node.kt
│ ├── layouts
│ ├── GraphLayoutManager.kt
│ ├── energy
│ │ └── FruchtermanReingoldLayoutManager.kt
│ ├── layered
│ │ ├── SugiyamaArrowEdgeDecoration.kt
│ │ ├── SugiyamaConfiguration.kt
│ │ ├── SugiyamaEdgeData.kt
│ │ ├── SugiyamaLayoutManager.kt
│ │ └── SugiyamaNodeData.kt
│ └── tree
│ │ ├── BuchheimWalkerConfiguration.kt
│ │ ├── BuchheimWalkerLayoutManager.kt
│ │ ├── BuchheimWalkerNodeData.kt
│ │ └── TreeEdgeDecoration.kt
│ └── util
│ ├── Size.kt
│ └── VectorF.kt
├── image
├── Graph.png
├── GraphView_logo.jpg
├── LayeredGraph.png
└── Tree.png
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── dev
│ │ └── bandb
│ │ └── graphview
│ │ └── sample
│ │ ├── GraphActivity.kt
│ │ ├── MainActivity.kt
│ │ ├── MainContent.kt
│ │ └── algorithms
│ │ ├── BuchheimWalkerActivity.kt
│ │ ├── FruchtermanReingoldActivity.kt
│ │ └── SugiyamaActivity.kt
│ └── res
│ ├── drawable
│ ├── circle.xml
│ ├── ic_add_white_24dp.xml
│ └── ic_arrow_back.xml
│ ├── layout
│ ├── activity_graph.xml
│ ├── activity_main.xml
│ ├── main_item.xml
│ └── node.xml
│ ├── menu
│ └── menu_buchheim_walker_orientations.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | *.ipr
38 | *.iws
39 | .idea
40 |
41 | # Keystore files
42 | *.jks
43 |
44 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | GraphView
2 | ===========
3 |
4 | Android GraphView is used to display data in graph structures.
5 |
6 | 
7 |
8 | Overview
9 | ========
10 | The library can be used within `RecyclerView` and currently works with small graphs only.
11 |
12 | **This project is currently experimental and the API subject to breaking changes without notice.**
13 |
14 | Download
15 | ========
16 | The library is only available on MavenCentral. Please add this code to your build.gradle file on project level:
17 | ```gradle
18 | allprojects {
19 | repositories {
20 | ...
21 | mavenCentral()
22 | }
23 | }
24 | ```
25 |
26 | And add the dependency to the build.gradle file within the app module:
27 | ```gradle
28 | dependencies {
29 | implementation 'dev.bandb.graphview:graphview:0.8.1'
30 | }
31 | ```
32 | Layouts
33 | ======
34 | ### Tree
35 | Uses Walker's algorithm with Buchheim's runtime improvements (`BuchheimWalkerLayoutManager` class). Currently only the `TreeEdgeDecoration` can be used to draw the edges. Supports different orientations. All you have to do is using the `BuchheimWalkerConfiguration.Builder.setOrientation(int)` with either `ORIENTATION_LEFT_RIGHT`, `ORIENTATION_RIGHT_LEFT`, `ORIENTATION_TOP_BOTTOM` and
36 | `ORIENTATION_BOTTOM_TOP` (default). Furthermore parameters like sibling-, level-, subtree separation can be set.
37 | ### Directed graph
38 | Directed graph drawing by simulating attraction/repulsion forces. For this the algorithm by Fruchterman and Reingold (`FruchtermanReingoldLayoutManager` class) was implemented. To draw the edges you can use `ArrowEdgeDecoration` or `StraightEdgeDecoration`.
39 | ### Layered graph
40 | Algorithm from Sugiyama et al. for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph (`SugiyamaLayoutManager` class). Currently only the `SugiyamaArrowEdgeDecoration` can be used to draw the edges. You can also set the parameters for node and level separation using the `SugiyamaConfiguration.Builder`.
41 |
42 | Usage
43 | ======
44 | GraphView must be integrated with `RecyclerView`.
45 | For this you’ll need to add a `RecyclerView` to your layout and create an item layout like usually when working with `RecyclerView`.
46 |
47 | ```xml
48 |
52 |
53 |
57 |
58 |
59 | ```
60 |
61 | Currently GraphView must be used together with a Zoom Engine like [ZoomLayout](https://github.com/natario1/ZoomLayout). To change the zoom values just use the different attributes described in the ZoomLayout project site.
62 |
63 | To create a graph, we need to instantiate the `Graph` class. Next submit your graph to your Adapter, for that you must extend from the `AbstractGraphAdapter` class.
64 |
65 | ```kotlin
66 | private void setupGraphView {
67 | val recycler = findViewById(R.id.recycler)
68 |
69 | // 1. Set a layout manager of the ones described above that the RecyclerView will use.
70 | val configuration = BuchheimWalkerConfiguration.Builder()
71 | .setSiblingSeparation(100)
72 | .setLevelSeparation(100)
73 | .setSubtreeSeparation(100)
74 | .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM)
75 | .build()
76 | recycler.layoutManager = BuchheimWalkerLayoutManager(context, configuration)
77 |
78 | // 2. Attach item decorations to draw edges
79 | recycler.addItemDecoration(TreeEdgeDecoration())
80 |
81 | // 3. Build your graph
82 | val graph = Graph()
83 | val node1 = Node("Parent")
84 | val node2 = Node("Child 1")
85 | val node3 = Node("Child 2")
86 |
87 | graph.addEdge(node1, node2)
88 | graph.addEdge(node1, node3)
89 |
90 | // 4. You will need a simple Adapter/ViewHolder.
91 | // 4.1 Your Adapter class should extend from `AbstractGraphAdapter`
92 | adapter = object : AbstractGraphAdapter() {
93 |
94 | // 4.2 ViewHolder should extend from `RecyclerView.ViewHolder`
95 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
96 | val view = LayoutInflater.from(parent.context)
97 | .inflate(R.layout.node, parent, false)
98 | return NodeViewHolder(view)
99 | }
100 |
101 | override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
102 | holder.textView.text = getNodeData(position).toString()
103 | }
104 | }.apply {
105 | // 4.3 Submit the graph
106 | this.submitGraph(graph)
107 | recycler.adapter = this
108 | }
109 | }
110 | ```
111 |
112 | Customization
113 | ======
114 | You can change the edge design by supplying your custom paint object to your edge decorator.
115 | ```kotlin
116 | val edgeStyle = Paint(Paint.ANTI_ALIAS_FLAG).apply {
117 | strokeWidth = 5f
118 | color = Color.BLACK
119 | style = Paint.Style.STROKE
120 | strokeJoin = Paint.Join.ROUND
121 | pathEffect = CornerPathEffect(10f)
122 | }
123 |
124 | recyclerView.addItemDecoration(TreeEdgeDecoration(edgeStyle))
125 | ```
126 |
127 | If you want that your nodes are all the same size you can set `useMaxSize` to `true`. The biggest node defines the size for all the other nodes.
128 | ```kotlin
129 | recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, configuration).apply {
130 | useMaxSize = true
131 | }
132 | ```
133 |
134 | Examples
135 | ========
136 | #### Rooted Tree
137 | 
138 |
139 | #### Directed Graph
140 | 
141 |
142 | #### Layered Graph
143 | 
144 |
145 | License
146 | =======
147 |
148 | Copyright 2019 - 2021 Block & Block
149 |
150 | Licensed under the Apache License, Version 2.0 (the "License");
151 | you may not use this file except in compliance with the License.
152 | You may obtain a copy of the License at
153 |
154 | http://www.apache.org/licenses/LICENSE-2.0
155 |
156 | Unless required by applicable law or agreed to in writing, software
157 | distributed under the License is distributed on an "AS IS" BASIS,
158 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
159 | See the License for the specific language governing permissions and
160 | limitations under the License.
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = '1.4.31'
4 | repositories {
5 | google()
6 | mavenCentral()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:4.1.3'
11 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 |
14 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0'
15 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.10.2'
16 | }
17 | }
18 |
19 | allprojects {
20 | repositories {
21 | google()
22 | mavenCentral()
23 | jcenter()
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
31 | subprojects {
32 | tasks.withType(Javadoc).all { enabled = false }
33 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 | # When configured, Gradle will run in incubating parallel mode.
14 | # This option should only be used with decoupled projects. More details, visit
15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
16 | # org.gradle.parallel=true
17 | android.useAndroidX=true
18 | GROUP=dev.bandb.graphview
19 | POM_ARTIFACT_ID=graphview
20 | VERSION_NAME=0.8.1
21 | POM_NAME=graphview
22 | POM_PACKAGING=aar
23 | POM_DESCRIPTION=Android GraphView is used to display data in graph structures.
24 | POM_INCEPTION_YEAR=2021
25 | POM_URL=https://github.com/oss-bandb/GraphView
26 | POM_SCM_URL=https://github.com/oss-bandb/GraphView
27 | POM_SCM_CONNECTION=scm:git@github.com:oss-bandb/GraphView.git
28 | POM_SCM_DEV_CONNECTION=scm:git@github.com:oss-bandb/GraphView.git
29 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
30 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
31 | POM_LICENCE_DIST=repo
32 | POM_DEVELOPER_ID=bandb
33 | POM_DEVELOPER_NAME=Block & Block
34 | POM_DEVELOPER_URL=https://github.com/oss-bandb
--------------------------------------------------------------------------------
/gradle/publish-mavencentral.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 |
3 | configure(subprojects) {
4 | apply()
5 |
6 | configure {
7 | withJavadocJar()
8 | withSourcesJar()
9 | }
10 |
11 | configure {
12 | publications {
13 | val main by creating(MavenPublication::class) {
14 | from(components["java"])
15 |
16 | pom {
17 | name.set("…")
18 | description.set("…")
19 | url.set("…")
20 | licenses {
21 | license {
22 | name.set("…")
23 | url.set("…")
24 | }
25 | }
26 | developers {
27 | developer {
28 | id.set("…")
29 | name.set("…")
30 | email.set("…")
31 | }
32 | }
33 | scm {
34 | connection.set("…")
35 | developerConnection.set("…")
36 | url.set("…")
37 | }
38 | }
39 | }
40 | }
41 | repositories {
42 | maven {
43 | name = "OSSRH"
44 | setUrl("https://oss.sonatype.org/service/local/staging/deploy/maven2")
45 | credentials {
46 | username = System.getenv("OSSRH_USER")
47 | password = System.getenv("OSSRH_PASSWORD")
48 | }
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 14 15:44:09 CET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/graphview/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/graphview/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'com.vanniktech.maven.publish'
4 |
5 | android {
6 | compileSdkVersion 30
7 |
8 | defaultConfig {
9 | minSdkVersion 16
10 | targetSdkVersion 30
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 |
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility = '1.8'
26 | targetCompatibility = '1.8'
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: 'libs', include: ['*.jar'])
32 | implementation "androidx.appcompat:appcompat:1.2.0"
33 | implementation "androidx.annotation:annotation:1.2.0"
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
35 | implementation "androidx.recyclerview:recyclerview:1.2.0"
36 | }
--------------------------------------------------------------------------------
/graphview/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\DennisBlock\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/graphview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/AbstractGraphAdapter.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview
2 |
3 | import androidx.annotation.Nullable
4 | import androidx.recyclerview.widget.RecyclerView
5 | import dev.bandb.graphview.graph.Graph
6 | import dev.bandb.graphview.graph.Node
7 |
8 | abstract class AbstractGraphAdapter : RecyclerView.Adapter() {
9 | var graph: Graph? = null
10 | override fun getItemCount(): Int = graph?.nodeCount ?: 0
11 |
12 | open fun getNode(position: Int): Node? = graph?.getNodeAtPosition(position)
13 | open fun getNodeData(position: Int): Any? = graph?.getNodeAtPosition(position)?.data
14 |
15 | /**
16 | * Submits a new graph to be displayed.
17 | *
18 | *
19 | * If a graph is already being displayed, you need to dispatch Adapter.notifyItem.
20 | *
21 | * @param graph The new graph to be displayed.
22 | */
23 | open fun submitGraph(@Nullable graph: Graph?) {
24 | this.graph = graph
25 | }
26 | }
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowDecoration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.decoration.edge
2 |
3 | import android.graphics.*
4 | import androidx.recyclerview.widget.RecyclerView
5 | import dev.bandb.graphview.AbstractGraphAdapter
6 | import dev.bandb.graphview.graph.Node
7 |
8 | open class ArrowDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
9 | strokeWidth = 5f
10 | color = Color.BLACK
11 | style = Paint.Style.STROKE
12 | strokeJoin = Paint.Join.ROUND
13 | pathEffect = CornerPathEffect(10f)
14 | }) : RecyclerView.ItemDecoration() {
15 |
16 | private val trianglePath = Path()
17 |
18 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
19 | if (parent.layoutManager == null) {
20 | return
21 | }
22 | val adapter = parent.adapter
23 | if (adapter !is AbstractGraphAdapter) {
24 | throw RuntimeException(
25 | "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}")
26 | }
27 |
28 | val graph = adapter.graph
29 | val trianglePaint = Paint(this.linePaint).apply {
30 | style = Paint.Style.FILL
31 | }
32 | graph?.edges?.forEach { (source, destination) ->
33 | val (x1, y1) = source.position
34 | val (x2, y2) = destination.position
35 |
36 | val startX = x1 + source.width / 2f
37 | val startY = y1 + source.height / 2f
38 | val stopX = x2 + destination.width / 2f
39 | val stopY = y2 + destination.height / 2f
40 |
41 | val clippedLine = clipLine(startX, startY, stopX, stopY, destination)
42 |
43 | drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3])
44 | }
45 | }
46 |
47 | protected fun clipLine(
48 | startX: Float,
49 | startY: Float,
50 | stopX: Float,
51 | stopY: Float,
52 | destination: Node
53 | ): FloatArray {
54 | val resultLine = FloatArray(4)
55 | resultLine[0] = startX
56 | resultLine[1] = startY
57 |
58 | val slope = (startY - stopY) / (startX - stopX)
59 | val halfHeight = destination.height / 2f
60 | val halfWidth = destination.width / 2f
61 | val halfSlopeWidth = slope * halfWidth
62 | val halfSlopeHeight = halfHeight / slope
63 |
64 | if (-halfHeight <= halfSlopeWidth && halfSlopeWidth <= halfHeight) {
65 | // line intersects with ...
66 | if (destination.x > startX) {
67 | // left edge
68 | resultLine[2] = stopX - halfWidth
69 | resultLine[3] = stopY - halfSlopeWidth
70 | } else if (destination.x < startX) {
71 | // right edge
72 | resultLine[2] = stopX + halfWidth
73 | resultLine[3] = stopY + halfSlopeWidth
74 | }
75 | }
76 |
77 | if (-halfWidth <= halfSlopeHeight && halfSlopeHeight <= halfWidth) {
78 | // line intersects with ...
79 | if (destination.y < startY) {
80 | // bottom edge
81 | resultLine[2] = stopX + halfSlopeHeight
82 | resultLine[3] = stopY + halfHeight
83 | } else if (destination.y > startY) {
84 | // top edge
85 | resultLine[2] = stopX - halfSlopeHeight
86 | resultLine[3] = stopY - halfHeight
87 | }
88 | }
89 |
90 | return resultLine
91 | }
92 |
93 | /**
94 | * Draws a triangle.
95 | *
96 | * @param canvas
97 | * @param paint
98 | * @param x1
99 | * @param y1
100 | * @param x2
101 | * @param y2
102 | */
103 | protected fun drawTriangle(canvas: Canvas, paint: Paint?, x1: Float, y1: Float, x2: Float, y2: Float): FloatArray {
104 | val angle = (Math.atan2(y2 - y1.toDouble(), x2 - x1.toDouble()) + Math.PI).toFloat()
105 | val x3 = (x2 + ARROW_LENGTH * Math.cos((angle - ARROW_DEGREES).toDouble())).toFloat()
106 | val y3 = (y2 + ARROW_LENGTH * Math.sin((angle - ARROW_DEGREES).toDouble())).toFloat()
107 | val x4 = (x2 + ARROW_LENGTH * Math.cos((angle + ARROW_DEGREES).toDouble())).toFloat()
108 | val y4 = (y2 + ARROW_LENGTH * Math.sin((angle + ARROW_DEGREES).toDouble())).toFloat()
109 | trianglePath.moveTo(x2, y2) // Top
110 | trianglePath.lineTo(x3, y3) // Bottom left
111 | trianglePath.lineTo(x4, y4) // Bottom right
112 | trianglePath.close()
113 | canvas.drawPath(trianglePath, paint!!)
114 |
115 | // calculate centroid of the triangle
116 | val x = (x2 + x3 + x4) / 3
117 | val y = (y2 + y3 + y4) / 3
118 | val triangleCentroid = floatArrayOf(x, y)
119 | trianglePath.reset()
120 | return triangleCentroid
121 | }
122 |
123 | companion object {
124 | //TODO: expose
125 | private const val ARROW_DEGREES = 0.5f
126 | private const val ARROW_LENGTH = 50f
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowEdgeDecoration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.decoration.edge
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.CornerPathEffect
6 | import android.graphics.Paint
7 | import androidx.recyclerview.widget.RecyclerView
8 | import dev.bandb.graphview.AbstractGraphAdapter
9 |
10 | open class ArrowEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
11 | strokeWidth = 5f // TODO: move default values res xml
12 | color = Color.BLACK
13 | style = Paint.Style.STROKE
14 | strokeJoin = Paint.Join.ROUND
15 | pathEffect = CornerPathEffect(10f)
16 | }) : ArrowDecoration(linePaint) {
17 |
18 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
19 | if (parent.layoutManager == null) {
20 | return
21 | }
22 | val adapter = parent.adapter
23 | if (adapter !is AbstractGraphAdapter) {
24 | throw RuntimeException(
25 | "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}")
26 | }
27 |
28 | val graph = adapter.graph
29 | val trianglePaint = Paint(linePaint).apply {
30 | style = Paint.Style.FILL
31 | }
32 | graph?.edges?.forEach { (source, destination) ->
33 | val (x1, y1) = source.position
34 | val (x2, y2) = destination.position
35 |
36 | val startX = x1 + source.width / 2f
37 | val startY = y1 + source.height / 2f
38 | val stopX = x2 + destination.width / 2f
39 | val stopY = y2 + destination.height / 2f
40 |
41 | val clippedLine = clipLine(startX, startY, stopX, stopY, destination)
42 |
43 | //TODO: modularization
44 | val triangleCentroid: FloatArray = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3])
45 |
46 | c.drawLine(clippedLine[0],
47 | clippedLine[1],
48 | triangleCentroid[0],
49 | triangleCentroid[1], linePaint)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/decoration/edge/StraightEdgeDecoration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.decoration.edge
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.CornerPathEffect
6 | import android.graphics.Paint
7 | import androidx.recyclerview.widget.RecyclerView
8 | import dev.bandb.graphview.AbstractGraphAdapter
9 |
10 | open class StraightEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
11 | strokeWidth = 5f
12 | color = Color.BLACK
13 | style = Paint.Style.STROKE
14 | strokeJoin = Paint.Join.ROUND
15 | pathEffect = CornerPathEffect(10f)
16 | }) : RecyclerView.ItemDecoration() {
17 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
18 | if (parent.layoutManager == null) {
19 | return
20 | }
21 | val adapter = parent.adapter
22 | if (adapter !is AbstractGraphAdapter) {
23 | throw RuntimeException(
24 | "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}")
25 | }
26 |
27 | val graph = adapter.graph
28 |
29 | graph?.edges?.forEach { (source, destination) ->
30 | val (x1, y1) = source.position
31 | val (x2, y2) = destination.position
32 |
33 | c.drawLine(
34 | x1 + source.width / 2f,
35 | y1 + source.height / 2f,
36 | x2 + destination.width / 2f,
37 | y2 + destination.height / 2f, linePaint
38 | )
39 | }
40 | super.onDraw(c, parent, state)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/graph/Edge.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.graph
2 |
3 | data class Edge(val source: Node, val destination: Node)
4 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/graph/Graph.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.graph
2 |
3 | class Graph {
4 | private val _nodes = arrayListOf()
5 | private val _edges = arrayListOf()
6 | val nodes: List = _nodes
7 | val edges: List = _edges
8 |
9 | private var isTree = false
10 |
11 | val nodeCount: Int
12 | get() = _nodes.size
13 |
14 | fun addNode(node: Node) {
15 | if (node !in _nodes) {
16 | _nodes.add(node)
17 | }
18 | }
19 |
20 | fun addNodes(vararg nodes: Node) = nodes.forEach { addNode(it) }
21 |
22 | fun removeNode(node: Node) {
23 | if (!_nodes.contains(node)) {
24 | throw IllegalArgumentException("Unable to find node in graph.")
25 | }
26 |
27 | if (isTree) {
28 | for (n in successorsOf(node)) {
29 | removeNode(n)
30 | }
31 | }
32 |
33 | _nodes.remove(node)
34 |
35 | val iterator = _edges.iterator()
36 | while (iterator.hasNext()) {
37 | val (source, destination) = iterator.next()
38 | if (source == node || destination == node) {
39 | iterator.remove()
40 | }
41 | }
42 | }
43 |
44 | fun removeNodes(vararg nodes: Node) = nodes.forEach { removeNode(it) }
45 |
46 | fun addEdge(source: Node, destination: Node): Edge {
47 | val edge = Edge(source, destination)
48 | addEdge(edge)
49 |
50 | return edge
51 | }
52 |
53 | fun addEdge(edge: Edge) {
54 | addNode(edge.source)
55 | addNode(edge.destination)
56 |
57 | if (edge !in _edges) {
58 | _edges.add(edge)
59 | }
60 | }
61 |
62 | fun addEdges(vararg edges: Edge) = edges.forEach { addEdge(it) }
63 |
64 | fun removeEdge(edge: Edge) = _edges.remove(edge)
65 |
66 | fun removeEdges(vararg edges: Edge) = edges.forEach { removeEdge(it) }
67 |
68 | fun removeEdge(predecessor: Node, current: Node) {
69 | val iterator = _edges.iterator()
70 | while (iterator.hasNext()) {
71 | val (source, destination) = iterator.next()
72 | if (source == predecessor && destination == current) {
73 | iterator.remove()
74 | }
75 | }
76 | }
77 |
78 | fun hasNodes(): Boolean = _nodes.isNotEmpty()
79 |
80 | fun getNodeAtPosition(position: Int): Node {
81 | if (position < 0) {
82 | throw IllegalArgumentException("position can't be negative")
83 | }
84 |
85 | val size = _nodes.size
86 | if (position >= size) {
87 | throw IndexOutOfBoundsException("Position: $position, Size: $size")
88 | }
89 |
90 | return _nodes[position]
91 | }
92 |
93 | fun getEdgeBetween(source: Node, destination: Node): Edge? =
94 | _edges.find { it.source == source && it.destination == destination }
95 |
96 | fun hasSuccessor(node: Node): Boolean = _edges.any { it.source == node }
97 |
98 | fun successorsOf(node: Node) =
99 | _edges.filter { it.source == node }.map { edge -> edge.destination }
100 |
101 | fun hasPredecessor(node: Node): Boolean = _edges.any { it.destination == node }
102 |
103 | fun predecessorsOf(node: Node) =
104 | _edges.filter { it.destination == node }.map { edge -> edge.source }
105 |
106 | operator fun contains(node: Node): Boolean = _nodes.contains(node)
107 | operator fun contains(edge: Edge): Boolean = _edges.contains(edge)
108 |
109 | fun containsData(data: Any) = _nodes.any { it.data == data }
110 |
111 | fun getNodeAtPosition(data: Any) = _nodes.find { node -> node.data == data }
112 |
113 | fun getOutEdges(node: Node): List = _edges.filter { it.source == node }
114 |
115 | fun getInEdges(node: Node): List = _edges.filter { it.destination == node }
116 |
117 | // Todo this is a quick fix and should be removed later
118 | internal fun setAsTree(isTree: Boolean) {
119 | this.isTree = isTree
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/graph/Node.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.graph
2 |
3 | import dev.bandb.graphview.util.Size
4 | import dev.bandb.graphview.util.VectorF
5 |
6 | data class Node(var data: Any) {
7 | // TODO make private
8 | val position: VectorF = VectorF()
9 | val size: Size = Size()
10 |
11 | var height: Int
12 | get() = size.height
13 | set(value) {
14 | size.height = value
15 | }
16 |
17 | var width: Int
18 | get() = size.width
19 | set(value) {
20 | size.width = value
21 | }
22 |
23 | var x: Float
24 | get() = position.x
25 | set(value) {
26 | position.x = value
27 | }
28 |
29 | var y: Float
30 | get() = position.y
31 | set(value) {
32 | position.y = value
33 | }
34 |
35 | fun setSize(width: Int, height: Int) {
36 | this.width = width
37 | this.height = height
38 | }
39 |
40 | fun setPosition(x: Float, y: Float) {
41 | this.x = x
42 | this.y = y
43 | }
44 |
45 | fun setPosition(position: VectorF) {
46 | this.x = position.x
47 | this.y = position.y
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/GraphLayoutManager.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import dev.bandb.graphview.AbstractGraphAdapter
9 | import dev.bandb.graphview.graph.Graph
10 | import dev.bandb.graphview.util.Size
11 | import kotlin.math.max
12 |
13 | abstract class GraphLayoutManager internal constructor(context: Context)
14 | : RecyclerView.LayoutManager() {
15 |
16 | var useMaxSize: Boolean = DEFAULT_USE_MAX_SIZE
17 | set(value) {
18 | field = value
19 | requestLayout()
20 | }
21 |
22 | private var adapter: AbstractGraphAdapter<*>? = null
23 |
24 | override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams =
25 | RecyclerView.LayoutParams(
26 | RecyclerView.LayoutParams.WRAP_CONTENT,
27 | RecyclerView.LayoutParams.WRAP_CONTENT
28 | )
29 |
30 | override fun onAdapterChanged(oldAdapter: RecyclerView.Adapter<*>?,
31 | newAdapter: RecyclerView.Adapter<*>?) {
32 | super.onAdapterChanged(oldAdapter, newAdapter)
33 |
34 | if (newAdapter !is AbstractGraphAdapter) {
35 | throw RuntimeException(
36 | "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}")
37 | }
38 |
39 | adapter = newAdapter
40 | }
41 |
42 | override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
43 | detachAndScrapAttachedViews(recycler)
44 | positionItems(recycler, state.itemCount)
45 | }
46 |
47 | override fun canScrollHorizontally(): Boolean {
48 | return false
49 | }
50 |
51 | override fun canScrollVertically(): Boolean {
52 | return false
53 | }
54 |
55 | override fun onMeasure(recycler: RecyclerView.Recycler, state: RecyclerView.State,
56 | widthSpec: Int, heightSpec: Int) {
57 | val adapter = adapter
58 | if (adapter == null) {
59 | Log.e("GraphLayoutManager", "No adapter attached; skipping layout")
60 | super.onMeasure(recycler, state, widthSpec, heightSpec)
61 | return
62 | }
63 |
64 | val graph = adapter.graph
65 | if (graph == null || !graph.hasNodes()) {
66 | Log.e("GraphLayoutManager", "No graph set; skipping layout")
67 | super.onMeasure(recycler, state, widthSpec, heightSpec)
68 | return
69 | }
70 |
71 | var maxWidth = 0
72 | var maxHeight = 0
73 |
74 | for (i in 0 until state.itemCount) {
75 | val child = recycler.getViewForPosition(i)
76 |
77 | var params: ViewGroup.MarginLayoutParams? = child.layoutParams as? ViewGroup.MarginLayoutParams
78 | if (params == null) {
79 | params = ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
80 | }
81 |
82 | addView(child)
83 |
84 | val childWidthSpec = makeMeasureSpec(params.width)
85 | val childHeightSpec = makeMeasureSpec(params.height)
86 | child.measure(childWidthSpec, childHeightSpec)
87 |
88 | val measuredWidth = child.measuredWidth
89 | val measuredHeight = child.measuredHeight
90 |
91 | val node = adapter.getNode(i)
92 | node?.size?.apply {
93 | width = measuredWidth
94 | height = measuredHeight
95 | }
96 |
97 | maxWidth = max(maxWidth, measuredWidth)
98 | maxHeight = max(maxHeight, measuredHeight)
99 | }
100 |
101 | if (useMaxSize) {
102 | detachAndScrapAttachedViews(recycler)
103 | for (i in 0 until state.itemCount) {
104 | val child = recycler.getViewForPosition(i)
105 |
106 | addView(child)
107 |
108 | val childWidthSpec = makeMeasureSpec(maxWidth)
109 | val childHeightSpec = makeMeasureSpec(maxHeight)
110 | child.measure(childWidthSpec, childHeightSpec)
111 |
112 | val node = adapter.getNode(i)
113 | node?.size?.apply {
114 | width = child.measuredWidth
115 | height = child.measuredHeight
116 | }
117 | }
118 | }
119 |
120 | val size = run(graph, paddingLeft.toFloat(), paddingTop.toFloat())
121 | setMeasuredDimension(size.width + paddingRight + paddingLeft, size.height + paddingBottom + paddingTop)
122 | }
123 |
124 | private fun positionItems(recycler: RecyclerView.Recycler,
125 | itemCount: Int) {
126 | for (index in 0 until itemCount) {
127 | val child = recycler.getViewForPosition(index)
128 |
129 | adapter?.getNode(index)?.let {
130 | val width = it.width
131 | val height = it.height
132 | val (x, y) = it.position
133 |
134 | addView(child)
135 | val childWidthSpec = makeMeasureSpec(it.width)
136 | val childHeightSpec = makeMeasureSpec(it.height)
137 | child.measure(childWidthSpec, childHeightSpec)
138 |
139 | // calculate the size and position of this child
140 | val left = x.toInt()
141 | val top = y.toInt()
142 | val right = left + width
143 | val bottom = top + height
144 |
145 | child.layout(left, top, right, bottom)
146 | }
147 | }
148 | }
149 |
150 | /**
151 | * Executes the algorithm.
152 | * @param shiftY Shifts the y-coordinate origin
153 | * @param shiftX Shifts the x-coordinate origin
154 | * @return The size of the graph
155 | */
156 | abstract fun run(graph: Graph, shiftX: Float, shiftY: Float): Size
157 |
158 | companion object {
159 | const val DEFAULT_USE_MAX_SIZE = false
160 |
161 | private fun makeMeasureSpec(dimension: Int): Int {
162 | return if (dimension > 0) {
163 | View.MeasureSpec.makeMeasureSpec(dimension, View.MeasureSpec.EXACTLY)
164 | } else {
165 | View.MeasureSpec.UNSPECIFIED
166 | }
167 | }
168 | }
169 |
170 | }
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/energy/FruchtermanReingoldLayoutManager.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.energy
2 |
3 | import android.content.Context
4 | import android.graphics.RectF
5 | import dev.bandb.graphview.graph.Edge
6 | import dev.bandb.graphview.graph.Graph
7 | import dev.bandb.graphview.graph.Node
8 | import dev.bandb.graphview.layouts.GraphLayoutManager
9 | import dev.bandb.graphview.util.Size
10 | import dev.bandb.graphview.util.VectorF
11 | import java.util.*
12 | import kotlin.math.max
13 | import kotlin.math.min
14 | import kotlin.math.sqrt
15 |
16 | class FruchtermanReingoldLayoutManager @JvmOverloads constructor(private val context: Context, private val iterations: Int = DEFAULT_ITERATIONS) : GraphLayoutManager(context) {
17 | private val disps: MutableMap = HashMap()
18 | private val rand = Random(SEED)
19 | private var w: Int = 0 // width
20 | private var h: Int = 0 // height
21 | private var k: Float = 0.toFloat()
22 | private var t: Float = 0.toFloat()
23 | private var attraction_k: Float = 0.toFloat()
24 | private var repulsion_k: Float = 0.toFloat()
25 |
26 | private fun randomize(nodes: List) {
27 | nodes.forEach { node ->
28 | // create meta data for each node
29 | disps[node] = VectorF()
30 | node.setPosition(randInt(rand, 0, w / 2).toFloat(), randInt(rand, 0, h / 2).toFloat())
31 | }
32 | }
33 |
34 | private fun cool(currentIteration: Int) {
35 | t *= (1.0f - currentIteration / iterations.toFloat())
36 | }
37 |
38 | private fun limitMaximumDisplacement(nodes: List) {
39 | nodes.forEach {
40 | val dispLength = max(EPSILON, getDisp(it).length().toDouble()).toFloat()
41 | it.setPosition(it.position.add(getDisp(it).divide(dispLength).multiply(min(dispLength, t))))
42 | }
43 | }
44 |
45 | private fun calculateAttraction(edges: List) {
46 | edges.forEach { (v, u) ->
47 | val delta = v.position.subtract(u.position)
48 | val deltaLength = max(EPSILON, delta.length().toDouble()).toFloat()
49 | setDisp(v, getDisp(v).subtract(delta.divide(deltaLength).multiply(forceAttraction(deltaLength))))
50 | setDisp(u, getDisp(u).add(delta.divide(deltaLength).multiply(forceAttraction(deltaLength))))
51 | }
52 | }
53 |
54 | private fun calculateRepulsion(nodes: List) {
55 | nodes.forEach { v ->
56 | nodes.forEach { u ->
57 | if (u != v) {
58 | val delta = v.position.subtract(u.position)
59 | val deltaLength = max(EPSILON, delta.length().toDouble()).toFloat()
60 | setDisp(v, getDisp(v).add(delta.divide(deltaLength).multiply(forceRepulsion(deltaLength))))
61 | }
62 | }
63 | }
64 | }
65 |
66 | private fun forceAttraction(x: Float): Float {
67 | return x * x / attraction_k
68 | }
69 |
70 | private fun forceRepulsion(x: Float): Float {
71 | return repulsion_k * repulsion_k / x
72 | }
73 |
74 | private fun getDisp(node: Node): VectorF {
75 | return disps.getValue(node)
76 | }
77 |
78 | private fun setDisp(node: Node, disp: VectorF) {
79 | disps[node] = disp
80 | }
81 |
82 | override fun run(graph: Graph, shiftX: Float, shiftY: Float): Size {
83 | val size = findBiggestSize(graph) * graph.nodeCount
84 | w = size
85 | h = size
86 |
87 | val nodes = graph.nodes
88 | val edges = graph.edges
89 |
90 | t = (0.1 * sqrt((w / 2f * h / 2f).toDouble())).toFloat()
91 | k = (0.75 * sqrt((w * h / nodes.size.toFloat()).toDouble())).toFloat()
92 |
93 | attraction_k = 0.75f * k
94 | repulsion_k = 0.75f * k
95 |
96 | randomize(nodes)
97 |
98 | (0 until iterations).forEach { i ->
99 | calculateRepulsion(nodes)
100 |
101 | calculateAttraction(edges)
102 |
103 | limitMaximumDisplacement(nodes)
104 |
105 | cool(i)
106 |
107 | if (done()) {
108 | return@forEach
109 | }
110 | }
111 |
112 | positionNodes(graph)
113 |
114 | shiftCoordinates(graph, shiftX, shiftY)
115 |
116 | return calculateGraphSize(graph)
117 | }
118 |
119 | private fun shiftCoordinates(graph: Graph, shiftX: Float, shiftY: Float) {
120 | graph.nodes.forEach { node ->
121 | node.setPosition(VectorF(node.x + shiftX, node.y + shiftY))
122 | }
123 | }
124 |
125 | private fun positionNodes(graph: Graph) {
126 | val (x, y) = getOffset(graph)
127 | val nodesVisited = ArrayList()
128 | val nodeClusters = ArrayList()
129 | graph.nodes.forEach { node ->
130 | node.setPosition(VectorF(node.x - x, node.y - y))
131 | }
132 |
133 | graph.nodes.forEach { node ->
134 | if (nodesVisited.contains(node)) {
135 | return@forEach
136 | }
137 |
138 | nodesVisited.add(node)
139 | var cluster = findClusterOf(nodeClusters, node)
140 | if (cluster == null) {
141 | cluster = NodeCluster()
142 | cluster.add(node)
143 | nodeClusters.add(cluster)
144 | }
145 |
146 | followEdges(graph, cluster, node, nodesVisited)
147 | }
148 |
149 | positionCluster(nodeClusters)
150 | }
151 |
152 | private fun positionCluster(nodeClusters: MutableList) {
153 | combineSingleNodeCluster(nodeClusters)
154 |
155 | var cluster = nodeClusters[0]
156 | // move first cluster to 0,0
157 | cluster.offset(-cluster.rect.left, -cluster.rect.top)
158 |
159 | for (i in 1 until nodeClusters.size) {
160 | val nextCluster = nodeClusters[i]
161 | val xDiff = nextCluster.rect.left - cluster.rect.right - CLUSTER_PADDING.toFloat()
162 | val yDiff = nextCluster.rect.top - cluster.rect.top
163 | nextCluster.offset(-xDiff, -yDiff)
164 | cluster = nextCluster
165 | }
166 | }
167 |
168 | private fun combineSingleNodeCluster(nodeClusters: MutableList) {
169 | var firstSingleNodeCluster: NodeCluster? = null
170 | val iterator = nodeClusters.iterator()
171 | while (iterator.hasNext()) {
172 | val cluster = iterator.next()
173 | if (cluster.size() == 1) {
174 | if (firstSingleNodeCluster == null) {
175 | firstSingleNodeCluster = cluster
176 | continue
177 | }
178 |
179 | firstSingleNodeCluster.concat(cluster)
180 | iterator.remove()
181 | }
182 | }
183 | }
184 |
185 | private fun followEdges(
186 | graph: Graph,
187 | cluster: NodeCluster,
188 | node: Node,
189 | nodesVisited: MutableList
190 | ) {
191 | graph.successorsOf(node).forEach { successor ->
192 | if (nodesVisited.contains(successor)) {
193 | return@forEach
194 | }
195 |
196 | nodesVisited.add(successor)
197 | cluster.add(successor)
198 |
199 | followEdges(graph, cluster, successor, nodesVisited)
200 | }
201 |
202 | graph.predecessorsOf(node).forEach { predecessor ->
203 | if (nodesVisited.contains(predecessor)) {
204 | return@forEach
205 | }
206 |
207 | nodesVisited.add(predecessor)
208 | cluster.add(predecessor)
209 |
210 | followEdges(graph, cluster, predecessor, nodesVisited)
211 | }
212 | }
213 |
214 | private fun findClusterOf(clusters: List, node: Node): NodeCluster? {
215 | return clusters.firstOrNull { it.contains(node) }
216 | }
217 |
218 | private fun findBiggestSize(graph: Graph): Int {
219 | return graph.nodes
220 | .map { max(it.height, it.width) }
221 | .maxOrNull()
222 | ?: 0
223 | }
224 |
225 | private fun getOffset(graph: Graph): VectorF {
226 | var offsetX = java.lang.Float.MAX_VALUE
227 | var offsetY = java.lang.Float.MAX_VALUE
228 | graph.nodes.forEach { node ->
229 | offsetX = min(offsetX, node.x)
230 | offsetY = min(offsetY, node.y)
231 | }
232 | return VectorF(offsetX, offsetY)
233 | }
234 |
235 | private fun done(): Boolean {
236 | return t < 1.0 / max(h, w)
237 | }
238 |
239 | private fun calculateGraphSize(graph: Graph): Size {
240 |
241 | var left = Integer.MAX_VALUE
242 | var top = Integer.MAX_VALUE
243 | var right = Integer.MIN_VALUE
244 | var bottom = Integer.MIN_VALUE
245 | for (node in graph.nodes) {
246 | left = min(left.toFloat(), node.x).toInt()
247 | top = min(top.toFloat(), node.y).toInt()
248 | right = max(right.toFloat(), node.x + node.width).toInt()
249 | bottom = max(bottom.toFloat(), node.y + node.height).toInt()
250 | }
251 |
252 | return Size(right - left, bottom - top)
253 | }
254 |
255 | private class NodeCluster {
256 | val nodes = arrayListOf()
257 | var rect: RectF = RectF()
258 |
259 | fun add(node: Node) {
260 | nodes.add(node)
261 |
262 | if (nodes.size == 1) {
263 | rect.apply {
264 | left = node.x
265 | top = node.y
266 | right = node.x + node.width
267 | bottom = node.y + node.height
268 | }
269 | } else {
270 | rect.apply {
271 | left = min(left, node.x)
272 | top = min(top, node.y)
273 | right = max(right, node.x + node.width)
274 | bottom = max(bottom, node.y + node.height)
275 | }
276 | }
277 | }
278 |
279 | operator fun contains(node: Node): Boolean {
280 | return nodes.contains(node)
281 | }
282 |
283 | fun size(): Int {
284 | return nodes.size
285 | }
286 |
287 | fun concat(cluster: NodeCluster) {
288 | cluster.nodes.forEach { node ->
289 | node.setPosition(VectorF(rect.right + CLUSTER_PADDING, rect.top))
290 | add(node)
291 | }
292 | }
293 |
294 | fun offset(xDiff: Float, yDiff: Float) {
295 | nodes.forEach { node ->
296 | node.setPosition(node.position.add(xDiff, yDiff))
297 | }
298 |
299 | rect.offset(xDiff, yDiff)
300 | }
301 | }
302 |
303 | companion object {
304 | //TODO: builder?
305 | const val DEFAULT_ITERATIONS = 1000
306 | const val CLUSTER_PADDING = 100
307 | private const val EPSILON = 0.0001
308 | private const val SEED = 401678L
309 |
310 | private fun randInt(rand: Random, min: Int, max: Int): Int {
311 | // nextInt is normally exclusive of the top value,
312 | // so add 1 to make it inclusive
313 | return rand.nextInt(max - min + 1) + min
314 | }
315 | }
316 | }
317 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaArrowEdgeDecoration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.layered
2 |
3 | import android.graphics.*
4 | import androidx.recyclerview.widget.RecyclerView
5 | import dev.bandb.graphview.AbstractGraphAdapter
6 | import dev.bandb.graphview.decoration.edge.ArrowDecoration
7 |
8 | //TODO throw UnsupportedOperationException("SugiyamaAlgorithm currently not support custom edge renderer!")
9 | class SugiyamaArrowEdgeDecoration @JvmOverloads constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
10 | strokeWidth = 5f
11 | color = Color.BLACK
12 | style = Paint.Style.STROKE
13 | strokeJoin = Paint.Join.ROUND
14 | pathEffect = CornerPathEffect(10f)
15 | }) : ArrowDecoration(linePaint) {
16 |
17 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
18 | if (parent.layoutManager == null) {
19 | return
20 | }
21 | val adapter = parent.adapter
22 | if (adapter !is AbstractGraphAdapter) {
23 | throw RuntimeException(
24 | "SugiyamaArrowEdgeDecoration only works with ${AbstractGraphAdapter::class.simpleName}")
25 | }
26 | val layout = parent.layoutManager
27 | if (layout !is SugiyamaLayoutManager) {
28 | throw RuntimeException(
29 | "SugiyamaArrowEdgeDecoration only works with ${SugiyamaLayoutManager::class.simpleName}")
30 | }
31 |
32 | val graph = adapter.graph
33 | val edgeData = layout.edgeData
34 | val nodeData = layout.nodeData
35 | val path = Path()
36 | val trianglePaint = Paint(linePaint)
37 | trianglePaint.style = Paint.Style.FILL
38 |
39 | graph?.edges?.forEach { edge ->
40 | val source = edge.source
41 | val (x, y) = source.position
42 | val destination = edge.destination
43 | val (x1, y1) = destination.position
44 | val clippedLine: FloatArray
45 |
46 | if (edgeData.containsKey(edge) && edgeData.getValue(edge).bendPoints.isNotEmpty()) {
47 | // draw bend points
48 | val bendPoints = edgeData.getValue(edge).bendPoints
49 | val size = bendPoints.size
50 |
51 | clippedLine = if (nodeData.getValue(source).isReversed) {
52 | clipLine(
53 | bendPoints[2],
54 | bendPoints[3],
55 | bendPoints[0],
56 | bendPoints[1],
57 | destination
58 | )
59 | } else {
60 | clipLine(
61 | bendPoints[size - 4],
62 | bendPoints[size - 3],
63 | bendPoints[size - 2],
64 | bendPoints[size - 1],
65 | destination
66 | )
67 | }
68 | val triangleCentroid = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3])
69 |
70 | path.reset()
71 | path.moveTo(bendPoints[0], bendPoints[1])
72 | for (i in 3 until size - 2 step 2) {
73 | path.lineTo(bendPoints[i - 1], bendPoints[i])
74 | }
75 | path.lineTo(triangleCentroid[0], triangleCentroid[1])
76 | c.drawPath(path, linePaint)
77 | } else {
78 | val startX = x + source.width / 2f
79 | val startY = y + source.height / 2f
80 | val stopX = x1 + destination.width / 2f
81 | val stopY = y1 + destination.height / 2f
82 |
83 | clippedLine = clipLine(startX, startY, stopX, stopY, destination)
84 |
85 | val triangleCentroid = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3])
86 |
87 | c.drawLine(clippedLine[0],
88 | clippedLine[1],
89 | triangleCentroid[0],
90 | triangleCentroid[1], linePaint)
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaConfiguration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.layered
2 |
3 | class SugiyamaConfiguration private constructor(builder: Builder) {
4 | val levelSeparation: Int = builder.levelSeparation
5 | val nodeSeparation: Int = builder.nodeSeparation
6 |
7 | class Builder {
8 | var levelSeparation = Y_SEPARATION
9 | private set
10 | var nodeSeparation = X_SEPARATION
11 | private set
12 |
13 | fun setLevelSeparation(levelSeparation: Int) = apply {
14 | this.levelSeparation = levelSeparation
15 | }
16 |
17 | fun setNodeSeparation(nodeSeparation: Int) = apply {
18 | this.nodeSeparation = nodeSeparation
19 | }
20 |
21 | fun build() = SugiyamaConfiguration(this)
22 | }
23 |
24 | companion object {
25 | const val X_SEPARATION = 100
26 | const val Y_SEPARATION = 100
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaEdgeData.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.layered
2 |
3 | internal class SugiyamaEdgeData {
4 | var bendPoints = mutableListOf()
5 | }
6 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaLayoutManager.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.layered
2 |
3 | import android.content.Context
4 | import dev.bandb.graphview.graph.Edge
5 | import dev.bandb.graphview.graph.Graph
6 | import dev.bandb.graphview.graph.Node
7 | import dev.bandb.graphview.layouts.GraphLayoutManager
8 | import dev.bandb.graphview.util.Size
9 | import java.util.*
10 | import kotlin.collections.ArrayList
11 | import kotlin.math.*
12 |
13 | class SugiyamaLayoutManager @JvmOverloads constructor(private val context: Context, val configuration: SugiyamaConfiguration = SugiyamaConfiguration.Builder().build()) : GraphLayoutManager(context) {
14 | internal val nodeData: MutableMap = HashMap()
15 | internal val edgeData: MutableMap = HashMap()
16 | private val stack: MutableSet = HashSet()
17 | private val visited: MutableSet = HashSet()
18 | private var layers: MutableList> = mutableListOf()
19 | private lateinit var graph: Graph
20 |
21 | private var nodeCount = 1
22 |
23 | private val dummyText: String
24 | get() = "Dummy " + nodeCount++
25 |
26 | override fun run(graph: Graph, shiftX: Float, shiftY: Float): Size {
27 | this.graph = copyGraph(graph)
28 |
29 | reset()
30 |
31 | initSugiyamaData()
32 |
33 | cycleRemoval()
34 |
35 | layerAssignment()
36 |
37 | nodeOrdering()
38 |
39 | coordinateAssignment()
40 |
41 | shiftCoordinates(shiftX, shiftY)
42 |
43 | val graphSize = calculateGraphSize(this.graph)
44 |
45 | denormalize()
46 |
47 | restoreCycle()
48 |
49 | return graphSize
50 | }
51 |
52 | private fun calculateGraphSize(graph: Graph): Size {
53 | var left = Integer.MAX_VALUE
54 | var top = Integer.MAX_VALUE
55 | var right = Integer.MIN_VALUE
56 | var bottom = Integer.MIN_VALUE
57 | graph.nodes.forEach { node ->
58 | left = min(left.toFloat(), node.x).toInt()
59 | top = min(top.toFloat(), node.y).toInt()
60 | right = max(right.toFloat(), node.x + node.width).toInt()
61 | bottom = max(bottom.toFloat(), node.y + node.height).toInt()
62 | }
63 |
64 | return Size(right - left, bottom - top)
65 | }
66 |
67 | private fun shiftCoordinates(shiftX: Float, shiftY: Float) {
68 | layers.forEach { arrayList: ArrayList ->
69 | arrayList.forEach {
70 | it.x += shiftX
71 | it.y += shiftY
72 | }
73 | }
74 | }
75 |
76 | private fun reset() {
77 | layers.clear()
78 | stack.clear()
79 | visited.clear()
80 | nodeData.clear()
81 | edgeData.clear()
82 | nodeCount = 1
83 | }
84 |
85 | private fun initSugiyamaData() {
86 | graph.nodes.forEach { node ->
87 | node.x = 0f
88 | node.y = 0f
89 | nodeData[node] = SugiyamaNodeData()
90 | }
91 | graph.edges.forEach { edge ->
92 | edgeData[edge] = SugiyamaEdgeData()
93 | }
94 | }
95 |
96 | private fun cycleRemoval() {
97 | graph.nodes.forEach { node ->
98 | dfs(node)
99 | }
100 | }
101 |
102 | private fun dfs(node: Node) {
103 | if (visited.contains(node)) {
104 | return
105 | }
106 |
107 | visited.add(node)
108 | stack.add(node)
109 |
110 | graph.getOutEdges(node).forEach { edge ->
111 | val target = edge.destination
112 | if (stack.contains(target)) {
113 | graph.removeEdge(edge)
114 | graph.addEdge(target, node)
115 | nodeData.getValue(node).reversed.add(target)
116 | } else {
117 | dfs(target)
118 | }
119 | }
120 | stack.remove(node)
121 | }
122 |
123 | // top sort + add dummy nodes
124 | private fun layerAssignment() {
125 | if (graph.nodes.isEmpty()) {
126 | return
127 | }
128 |
129 | // build layers
130 | val copyGraph = copyGraph(graph)
131 | var roots = getRootNodes(copyGraph)
132 |
133 | // val rootDummy = Node(dummyText)
134 | // val dummyNodeData = SugiyamaNodeData()
135 | // dummyNodeData.isDummy = true
136 | // nodeData[rootDummy] = dummyNodeData
137 | //
138 | // for(node in roots) {
139 | // val edge = copyGraph.addEdge(rootDummy, node)
140 | // edgeData[edge] = SugiyamaEdgeData()
141 | // }
142 | //
143 | // roots = getRootNodes(copyGraph)
144 |
145 | while (roots.isNotEmpty()) {
146 | layers.add(roots)
147 | copyGraph.removeNodes(*roots.toTypedArray())
148 | roots = getRootNodes(copyGraph)
149 | }
150 |
151 | // add dummy's
152 | for (i in 0 until layers.size - 1) {
153 | val indexNextLayer = i + 1
154 | val currentLayer = layers[i]
155 | val nextLayer = layers[indexNextLayer]
156 |
157 | for (node in currentLayer) {
158 | val edges = this.graph.edges
159 | .filter { (source) -> source == node }
160 | .filter { (_, destination) -> abs(nodeData.getValue(destination).layer - nodeData.getValue(node).layer) > 1 }
161 | .toMutableList()
162 | val iterator = edges.iterator()
163 | while (iterator.hasNext()) {
164 | val edge = iterator.next()
165 | val dummy = Node(dummyText)
166 | val dummyNodeData = SugiyamaNodeData()
167 | dummyNodeData.isDummy = true
168 | dummyNodeData.layer = indexNextLayer
169 | nextLayer.add(dummy)
170 | nodeData[dummy] = dummyNodeData
171 | dummy.setSize(edge.source.width, 0) // TODO: calc avg layer height
172 | val dummyEdge1 = this.graph.addEdge(edge.source, dummy)
173 | val dummyEdge2 = this.graph.addEdge(dummy, edge.destination)
174 | edgeData[dummyEdge1] = SugiyamaEdgeData()
175 | edgeData[dummyEdge2] = SugiyamaEdgeData()
176 | this.graph.removeEdge(edge)
177 |
178 | iterator.remove()
179 | }
180 | }
181 | }
182 | }
183 |
184 | private fun getRootNodes(graph: Graph): ArrayList {
185 | val roots = arrayListOf()
186 |
187 | graph.nodes.forEach { node ->
188 | var inDegree = 0
189 | graph.edges.forEach { (_, destination) ->
190 | if (destination == node) {
191 | inDegree++
192 | }
193 | }
194 | if (inDegree == 0) {
195 | roots.add(node)
196 | nodeData.getValue(node).layer = layers.size
197 | }
198 | }
199 | return roots
200 | }
201 |
202 | private fun copyGraph(graph: Graph): Graph {
203 | val copy = Graph()
204 | copy.addNodes(*graph.nodes.toTypedArray())
205 | copy.addEdges(*graph.edges.toTypedArray())
206 | return copy
207 | }
208 |
209 | private fun nodeOrdering() {
210 | val best = ArrayList(layers)
211 | (0..23).forEach { i ->
212 | median(best, i)
213 | transpose(best)
214 | if (crossing(best) < crossing(layers)) {
215 | layers = best
216 | }
217 | }
218 | }
219 |
220 | private fun median(layers: ArrayList>, currentIteration: Int) {
221 | if (currentIteration % 2 == 0) {
222 | for (i in 1 until layers.size) {
223 | val currentLayer = layers[i]
224 | val previousLayer = layers[i - 1]
225 | for (node in currentLayer) {
226 | val positions = graph.edges
227 | .filter { (source) -> previousLayer.contains(source) }
228 | .map { (source) -> previousLayer.indexOf(source) }.toMutableList()
229 | positions.sort()
230 | val median = positions.size / 2
231 | if (positions.isNotEmpty()) {
232 | if (positions.size == 1) {
233 | nodeData.getValue(node).median = -1
234 | } else if (positions.size == 2) {
235 | nodeData.getValue(node).median = (positions[0] + positions[1]) / 2
236 | } else if (positions.size % 2 == 1) {
237 | nodeData.getValue(node).median = positions[median]
238 | } else {
239 | val left = positions[median - 1] - positions[0]
240 | val right = positions[positions.size - 1] - positions[median]
241 | if (left + right != 0) {
242 | nodeData.getValue(node).median =
243 | (positions[median - 1] * right + positions[median] * left) / (left + right)
244 | }
245 | }
246 | }
247 | }
248 | currentLayer.sortWith { n1, n2 ->
249 | val nodeData1 = nodeData.getValue(n1)
250 | val nodeData2 = nodeData.getValue(n2)
251 | nodeData1.median - nodeData2.median
252 | }
253 | }
254 | } else {
255 | for (l in 1 until layers.size) {
256 | val currentLayer = layers[l]
257 | val previousLayer = layers[l - 1]
258 | for (i in currentLayer.size - 1 downTo 1) {
259 | val node = currentLayer[i]
260 | val positions = graph.edges
261 | .filter { (source) -> previousLayer.contains(source) }
262 | .map { (source) -> previousLayer.indexOf(source) }.toMutableList()
263 | positions.sort()
264 | if (positions.isNotEmpty()) {
265 | if (positions.size == 1) {
266 | nodeData.getValue(node).median = positions[0]
267 | } else {
268 | nodeData.getValue(node).median = (positions[ceil(positions.size / 2.0).toInt()] + positions[ceil(positions.size / 2.0).toInt() - 1]) / 2
269 | }
270 | }
271 | }
272 | currentLayer.sortWith { n1, n2 ->
273 | val nodeData1 = nodeData.getValue(n1)
274 | val nodeData2 = nodeData.getValue(n2)
275 | nodeData1.median - nodeData2.median
276 | }
277 | }
278 | }
279 | }
280 |
281 | private fun transpose(layers: List>) {
282 | var improved = true
283 | while (improved) {
284 | improved = false
285 | (0 until layers.size - 1).forEach { l ->
286 | val northernNodes = layers[l]
287 | val southernNodes = layers[l + 1]
288 | (0 until southernNodes.size - 1).forEach { i ->
289 | val v = southernNodes[i]
290 | val w = southernNodes[i + 1]
291 | if (crossing(northernNodes, v, w) > crossing(northernNodes, w, v)) {
292 | improved = true
293 | exchange(southernNodes, v, w)
294 | }
295 | }
296 | }
297 | }
298 | }
299 |
300 | private fun exchange(nodes: List, v: Node, w: Node) {
301 | Collections.swap(nodes, nodes.indexOf(v), nodes.indexOf(w))
302 | }
303 |
304 | // counts the number of edge crossings if n2 appears to the left of n1 in their layer.
305 | private fun crossing(northernNodes: List, n1: Node, n2: Node): Int {
306 | var crossing = 0
307 |
308 | val parentNodesN1 = graph.edges
309 | .filter { (_, destination) -> destination == n1 }
310 | .map { it.source }
311 | .toList()
312 |
313 | val parentNodesN2 = graph.edges
314 | .filter { (_, destination) -> destination == n2 }
315 | .map { it.source }
316 | .toList()
317 |
318 | parentNodesN2.forEach { pn2 ->
319 | val indexOfPn2 = northernNodes.indexOf(pn2)
320 | repeat(
321 | parentNodesN1
322 | .filter { indexOfPn2 < northernNodes.indexOf(it) }.size
323 | ) { crossing++ }
324 | }
325 | return crossing
326 | }
327 |
328 | private fun crossing(layers: List>): Int {
329 | var crossing = 0
330 | (0 until layers.size - 1).forEach { l ->
331 | val southernNodes = layers[l]
332 | val northernNodes = layers[l + 1]
333 | for (i in 0 until southernNodes.size - 2) {
334 | val v = southernNodes[i]
335 | val w = southernNodes[i + 1]
336 | crossing += crossing(northernNodes, v, w)
337 | }
338 | }
339 | return crossing
340 | }
341 |
342 | private fun coordinateAssignment() {
343 | assignX()
344 | assignY()
345 | }
346 |
347 | private fun assignX() {
348 | // each node points to the root of the block.
349 | val root = ArrayList>(4)
350 | // each node points to its aligned neighbor in the layer below.
351 | val align = ArrayList>(4)
352 | val sink = ArrayList>(4)
353 | val x = ArrayList>(4)
354 | // minimal separation between the roots of different classes.
355 | val shift = ArrayList>(4)
356 | // the width of each block (max width of node in block)
357 | val blockWidth = ArrayList>(4)
358 |
359 |
360 | (0..3).forEach { i ->
361 | root.add(HashMap())
362 | align.add(HashMap())
363 | sink.add(HashMap())
364 | shift.add(HashMap())
365 | x.add(HashMap())
366 | blockWidth.add(HashMap())
367 | graph.nodes.forEach { n ->
368 | root[i][n] = n
369 | align[i][n] = n
370 | sink[i][n] = n
371 | shift[i][n] = java.lang.Float.MAX_VALUE
372 | x[i][n] = java.lang.Float.MIN_VALUE
373 | blockWidth[i][n] = 0f
374 | }
375 | }
376 | // calc the layout for down/up and leftToRight/rightToLeft
377 | (0..1).forEach { downward ->
378 | val type1Conflicts = markType1Conflicts(downward == 0)
379 | for (leftToRight in 0..1) {
380 | val k = 2 * downward + leftToRight
381 |
382 | verticalAlignment(
383 | root[k],
384 | align[k],
385 | type1Conflicts,
386 | downward == 0,
387 | leftToRight == 0
388 | )
389 | computeBlockWidths(root[k], blockWidth[k])
390 | horizontalCompactation(
391 | align[k],
392 | root[k],
393 | sink[k],
394 | shift[k],
395 | blockWidth[k],
396 | x[k],
397 | leftToRight == 0,
398 | downward == 0
399 | )
400 | }
401 | }
402 | balance(x, blockWidth)
403 | }
404 |
405 | private fun balance(
406 | x: List>,
407 | blockWidth: List>
408 | ) {
409 | val coordinates = HashMap()
410 |
411 | var minWidth = java.lang.Float.MAX_VALUE
412 | var smallestWidthLayout = 0
413 | val min = FloatArray(4)
414 | val max = FloatArray(4)
415 |
416 | // get the layout with smallest width and set minimum and maximum value
417 | // for each direction
418 | (0..3).forEach { i ->
419 | min[i] = Integer.MAX_VALUE.toFloat()
420 | max[i] = 0f
421 | graph.nodes.forEach { v ->
422 | val bw = 0.5f * blockWidth[i].getValue(v)
423 | var xp = x[i].getValue(v) - bw
424 | if (xp < min[i]) {
425 | min[i] = xp
426 | }
427 | xp = x[i].getValue(v) + bw
428 | if (xp > max[i]) {
429 | max[i] = xp
430 | }
431 | }
432 | val width = max[i] - min[i]
433 | if (width < minWidth) {
434 | minWidth = width
435 | smallestWidthLayout = i
436 | }
437 | }
438 |
439 | // align the layouts to the one with smallest width
440 | (0..3).filter { it != smallestWidthLayout }
441 | .forEach {
442 | // align the left to right layouts to the left border of the
443 | // smallest layout
444 | if (it == 0 || it == 1) {
445 | val diff = min[it] - min[smallestWidthLayout]
446 | for (n in x[it].keys) {
447 | if (diff > 0) {
448 | x[it][n] = x[it].getValue(n) - diff
449 | } else {
450 | x[it][n] = x[it].getValue(n) + diff
451 | }
452 | }
453 |
454 | // align the right to left layouts to the right border of
455 | // the smallest layout
456 | } else {
457 | val diff = max[it] - max[smallestWidthLayout]
458 | x[it].keys.forEach { n ->
459 | if (diff > 0) {
460 | x[it][n] = x[it].getValue(n) - diff
461 | } else {
462 | x[it][n] = x[it].getValue(n) + diff
463 | }
464 | }
465 | }
466 | }
467 |
468 | // get the minimum coordinate value
469 | var minValue = (0..3)
470 | .flatMap { x[it].values }
471 | .minOrNull()
472 | ?: java.lang.Float.MAX_VALUE
473 |
474 | // set left border to 0
475 | if (minValue != 0f) {
476 | (0..3).forEach { i ->
477 | x[i].keys.forEach { n ->
478 | x[i][n] = x[i].getValue(n) - minValue
479 | }
480 | }
481 | }
482 |
483 | // get the average median of each coordinate
484 | this.graph.nodes.forEach { n ->
485 | val values = FloatArray(4)
486 | (0..3).forEach { i ->
487 | values[i] = x[i].getValue(n)
488 | }
489 | Arrays.sort(values)
490 | val average = (values[1] + values[2]) / 2
491 | coordinates[n] = average
492 | }
493 |
494 | // get the minimum coordinate value
495 | minValue = coordinates.values.minOrNull()
496 | ?: Integer.MAX_VALUE.toFloat()
497 |
498 | // set left border to 0
499 | when {
500 | minValue != 0f -> coordinates.keys.forEach { n ->
501 | coordinates[n] = coordinates.getValue(n) - minValue
502 | }
503 | }
504 |
505 | graph.nodes.forEach { v ->
506 | v.x = coordinates.getValue(v)
507 | }
508 | }
509 |
510 | private fun markType1Conflicts(downward: Boolean): List> {
511 | val type1Conflicts = ArrayList>()
512 |
513 | for (i in graph.nodes.indices) {
514 | type1Conflicts.add(ArrayList())
515 | for (l in graph.edges.indices) {
516 | type1Conflicts[i].add(false)
517 | }
518 | }
519 |
520 | if (layers.size >= 4) {
521 | val upper: Int
522 | val lower: Int // iteration bounds
523 | var k1: Int // node position boundaries of closest inner segments
524 | if (downward) {
525 | lower = 1
526 | upper = layers.size - 2
527 | } else {
528 | lower = layers.size - 1
529 | upper = 2
530 | }
531 |
532 | /*
533 | * iterate level[2..h-2] in the given direction
534 | * available levels: 1 to h
535 | */
536 | var i = lower
537 | while (downward && i <= upper || !downward && i >= upper) {
538 | var k0 = 0
539 | var firstIndex = 0 // index of first node on layer
540 | val currentLevel = layers[i]
541 | val nextLevel = if (downward) layers[i + 1] else layers[i - 1]
542 |
543 | // for all nodes on next level
544 | (0 until nextLevel.size).forEach { l1 ->
545 | val virtualTwin = virtualTwinNode(nextLevel[l1], downward)
546 | if (l1 == nextLevel.size - 1 || virtualTwin != null) {
547 | k1 = currentLevel.size - 1
548 |
549 | if (virtualTwin != null) {
550 | k1 = pos(virtualTwin)
551 | }
552 |
553 | while (firstIndex <= l1) {
554 | val upperNeighbours = getAdjNodes(nextLevel[l1], downward)
555 |
556 | for (currentNeighbour in upperNeighbours) {
557 | /*
558 | * XXX: < 0 in first iteration is still ok for indizes starting
559 | * with 0 because no index can be smaller than 0
560 | */
561 | val currentNeighbourIndex = pos(currentNeighbour)
562 | if (currentNeighbourIndex < k0 || currentNeighbourIndex > k1) {
563 | type1Conflicts[l1][currentNeighbourIndex] = true
564 | }
565 | }
566 | firstIndex++
567 | }
568 | k0 = k1
569 | }
570 | }
571 | i = if (downward) i + 1 else i - 1
572 | }
573 | }
574 | return type1Conflicts
575 | }
576 |
577 | private fun verticalAlignment(
578 | root: MutableMap,
579 | align: MutableMap,
580 | type1Conflicts: List>,
581 | downward: Boolean,
582 | leftToRight: Boolean
583 | ) {
584 | // for all Level
585 | var i = if (downward) 0 else layers.size - 1
586 | while (downward && i <= layers.size - 1 || !downward && i >= 0) {
587 | val currentLevel = layers[i]
588 | var r = if (leftToRight) -1 else Integer.MAX_VALUE
589 | // for all nodes on Level i (with direction leftToRight)
590 | var k = if (leftToRight) 0 else currentLevel.size - 1
591 | while (leftToRight && k <= currentLevel.size - 1 || !leftToRight && k >= 0) {
592 |
593 | val v = currentLevel[k]
594 | val adjNodes = getAdjNodes(v, downward)
595 | if (adjNodes.isNotEmpty()) {
596 | // the first median
597 | val median = floor((adjNodes.size + 1) / 2.0).toInt()
598 | val medianCount = if (adjNodes.size % 2 == 1) 1 else 2
599 |
600 | // for all median neighbours in direction of H
601 | (0 until medianCount).forEach { count ->
602 | val m = adjNodes[median + count - 1]
603 | val posM = pos(m)
604 |
605 | if (align[v] == v
606 | // if segment (u,v) not marked by type1 conflicts AND ...
607 | && !type1Conflicts[pos(v)][posM]
608 | && (leftToRight && r < posM || !leftToRight && r > posM)
609 | ) {
610 | align[m] = v
611 | root[v] = root.getValue(m)
612 | align[v] = root.getValue(v)
613 | r = posM
614 | }
615 | }
616 | }
617 | k = if (leftToRight) k + 1 else k - 1
618 | }
619 | i = if (downward) i + 1 else i - 1
620 | }
621 | }
622 |
623 | private fun computeBlockWidths(
624 | root: MutableMap,
625 | blockWidth: MutableMap
626 | ) {
627 | graph.nodes.forEach { v ->
628 | val r = root.getValue(v)
629 | blockWidth[r] = max(blockWidth.getValue(r), v.width.toFloat())
630 | }
631 | }
632 |
633 | private fun horizontalCompactation(
634 | align: MutableMap,
635 | root: MutableMap,
636 | sink: MutableMap,
637 | shift: MutableMap,
638 | blockWidth: MutableMap,
639 | x: MutableMap,
640 | leftToRight: Boolean,
641 | downward: Boolean
642 | ) {
643 |
644 | // calculate class relative coordinates for all roots
645 | var i = if (downward) 0 else layers.size - 1
646 | while (downward && i <= layers.size - 1 || !downward && i >= 0) {
647 | val currentLevel = layers[i]
648 |
649 | var j = if (leftToRight) 0 else currentLevel.size - 1
650 | while (leftToRight && j <= currentLevel.size - 1 || !leftToRight && j >= 0) {
651 | val v = currentLevel[j]
652 | if (root[v] == v) {
653 | placeBlock(v, sink, shift, x, align, blockWidth, root, leftToRight)
654 | }
655 | j = if (leftToRight) j + 1 else j - 1
656 | }
657 | i = if (downward) i + 1 else i - 1
658 | }
659 | var d = 0f
660 | i = if (downward) 0 else layers.size - 1
661 | while (downward && i <= layers.size - 1 || !downward && i >= 0) {
662 | val currentLevel = layers[i]
663 |
664 | val v = currentLevel[if (leftToRight) 0 else currentLevel.size - 1]
665 |
666 | if (v == sink[root[v]]) {
667 | val oldShift = shift.getValue(v)
668 | if (oldShift < java.lang.Float.MAX_VALUE) {
669 | shift[v] = oldShift + d
670 | d += oldShift
671 | } else {
672 | shift[v] = 0f
673 | }
674 | }
675 | i = if (downward) i + 1 else i - 1
676 | }
677 |
678 | // apply root coordinates for all aligned nodes
679 | // (place block did this only for the roots)+
680 | graph.nodes.forEach { v ->
681 | x[v] = x.getValue(root.getValue(v))
682 |
683 | val shiftVal = shift.getValue(sink.getValue(root.getValue(v)))
684 | if (shiftVal < java.lang.Float.MAX_VALUE) {
685 | x[v] = x.getValue(v) + shiftVal // apply shift for each class
686 | }
687 | }
688 | }
689 |
690 | private fun placeBlock(
691 | v: Node,
692 | sink: MutableMap,
693 | shift: MutableMap,
694 | x: MutableMap,
695 | align: MutableMap,
696 | blockWidth: MutableMap,
697 | root: MutableMap,
698 | leftToRight: Boolean
699 | ) {
700 | if (x[v] == java.lang.Float.MIN_VALUE) {
701 | x[v] = 0f
702 | var w = v
703 | do {
704 | // if not first node on layer
705 | if (leftToRight && pos(w) > 0 || !leftToRight && pos(w) < layers[getLayerIndex(w)].size - 1) {
706 | val pred = pred(w, leftToRight)
707 | val u = root.getValue(pred!!)
708 |
709 | placeBlock(u, sink, shift, x, align, blockWidth, root, leftToRight)
710 |
711 | if (sink[v] == v) {
712 | sink[v] = sink.getValue(u)
713 | }
714 |
715 | if (sink[v] != sink[u]) {
716 | if (leftToRight) {
717 | shift[sink.getValue(u)] = min(shift.getValue(sink.getValue(u)), x.getValue(v) - x.getValue(u) - configuration.nodeSeparation.toFloat() - 0.5f * (blockWidth.getValue(u) + blockWidth.getValue(v)))
718 | } else {
719 | shift[sink.getValue(u)] = max(shift.getValue(sink.getValue(u)), x.getValue(v) - x.getValue(u) + configuration.nodeSeparation.toFloat() + 0.5f * (blockWidth.getValue(u) + blockWidth.getValue(v)))
720 | }
721 | } else {
722 | if (leftToRight) {
723 | x[v] = max(x.getValue(v), x.getValue(u) + configuration.nodeSeparation.toFloat() + 0.5f * (blockWidth.getValue(u) + blockWidth.getValue(v)))
724 | } else {
725 | x[v] = min(x.getValue(v), x.getValue(u) - configuration.nodeSeparation.toFloat() - 0.5f * (blockWidth.getValue(u) + blockWidth.getValue(v)))
726 | }
727 | }
728 | }
729 | w = align.getValue(w)
730 | } while (w != v)
731 | }
732 | }
733 |
734 | // predecessor
735 | private fun pred(v: Node, leftToRight: Boolean): Node? {
736 | val pos = pos(v)
737 | val rank = getLayerIndex(v)
738 |
739 | val level = layers[rank]
740 | return if (leftToRight && pos != 0 || !leftToRight && pos != level.size - 1) {
741 | level[if (leftToRight) pos - 1 else pos + 1]
742 | } else null
743 | }
744 |
745 |
746 | private fun virtualTwinNode(node: Node, downward: Boolean): Node? {
747 | if (!isLongEdgeDummy(node)) {
748 | return null
749 | }
750 | val adjNodes = getAdjNodes(node, downward)
751 |
752 | return if (adjNodes.isEmpty()) {
753 | null
754 | } else adjNodes[0]
755 | }
756 |
757 |
758 | private fun getAdjNodes(node: Node, downward: Boolean): List {
759 | return if (downward) graph.predecessorsOf(node) else graph.successorsOf(node)
760 | }
761 |
762 | // get node index in layer
763 | private fun pos(node: Node): Int {
764 | layers.forEach { l ->
765 | l.forEach { n ->
766 | if (node == n) {
767 | return l.indexOf(node)
768 | }
769 | }
770 | }
771 | return -1 // or exception?
772 | }
773 |
774 | private fun getLayerIndex(node: Node): Int {
775 | layers.indices.forEach { l ->
776 | layers[l].forEach { n ->
777 | if (node == n) {
778 | return l
779 | }
780 | }
781 | }
782 | return -1 // or exception?
783 | }
784 |
785 | private fun isLongEdgeDummy(v: Node): Boolean {
786 | val successors = graph.successorsOf(v)
787 | return nodeData.getValue(v).isDummy && successors.size == 1 && nodeData.getValue(successors[0]).isDummy
788 | }
789 |
790 | private fun assignY() {
791 | // compute y-coordinates
792 | val k = layers.size
793 |
794 | // compute height of each layer
795 |
796 | val height = FloatArray(graph.nodes.size)
797 | height.fill(0f)
798 |
799 | (0 until k).forEach { i ->
800 | val level = layers[i]
801 | for (j in level.indices) {
802 | val node = level[j]
803 | val h = if (nodeData.getValue(node).isDummy) 0f else node.height.toFloat()
804 | if (h > height[i])
805 | height[i] = h
806 | }
807 | }
808 |
809 | // assign y-coordinates
810 | var yPos = 0f
811 |
812 | var i = 0
813 | while (true) {
814 | val level = layers[i]
815 | for (j in level.indices)
816 | level[j].y = yPos
817 |
818 | if (i == k - 1)
819 | break
820 |
821 | yPos += (configuration.levelSeparation + 0.5 * (height[i] + height[i + 1])).toFloat()
822 | ++i
823 | }
824 | }
825 |
826 | private fun denormalize() {
827 | // remove dummy's
828 | for (i in 1 until layers.size - 1) {
829 | val iterator = layers[i].iterator()
830 | while (iterator.hasNext()) {
831 | val current = iterator.next()
832 | if (nodeData.getValue(current).isDummy) {
833 |
834 | val predecessor = graph.predecessorsOf(current)[0]
835 | val successor = graph.successorsOf(current)[0]
836 |
837 | val bendPoints =
838 | edgeData.getValue(graph.getEdgeBetween(predecessor, current)!!).bendPoints
839 |
840 | if (bendPoints.isEmpty() || !bendPoints.contains(current.x + predecessor.width / 2f)) {
841 | bendPoints.add(predecessor.x + predecessor.width / 2f)
842 | bendPoints.add(predecessor.y + predecessor.height / 2f)
843 |
844 | bendPoints.add(current.x + predecessor.width / 2f)
845 | bendPoints.add(current.y)
846 | }
847 |
848 | if (!nodeData.getValue(predecessor).isDummy) {
849 | bendPoints.add(current.x + predecessor.width / 2f)
850 | } else {
851 | bendPoints.add(current.x)
852 | }
853 | bendPoints.add(current.y)
854 |
855 | if (nodeData.getValue(successor).isDummy) {
856 | bendPoints.add(successor.x + predecessor.width / 2f)
857 | } else {
858 | bendPoints.add(successor.x + successor.width / 2f)
859 | }
860 | bendPoints.add(successor.y + successor.height / 2f)
861 |
862 | graph.removeEdge(predecessor, current)
863 | graph.removeEdge(current, successor)
864 | val edge = graph.addEdge(predecessor, successor)
865 | val sugiyamaEdgeData = SugiyamaEdgeData()
866 | sugiyamaEdgeData.bendPoints = bendPoints
867 | edgeData[edge] = sugiyamaEdgeData
868 |
869 | iterator.remove()
870 | graph.removeNode(current)
871 | }
872 | }
873 | }
874 | }
875 |
876 | private fun restoreCycle() {
877 | graph.nodes.forEach { n ->
878 | if (nodeData.getValue(n).isReversed) {
879 | nodeData.getValue(n).reversed.forEach { target ->
880 | val bendPoints = edgeData.getValue(graph.getEdgeBetween(target, n)!!).bendPoints
881 | graph.removeEdge(target, n)
882 | val edge = graph.addEdge(n, target)
883 | val edgeData = SugiyamaEdgeData()
884 | edgeData.bendPoints = bendPoints
885 | this.edgeData[edge] = edgeData
886 | }
887 | }
888 | }
889 | }
890 | }
891 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaNodeData.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.layered
2 |
3 | import dev.bandb.graphview.graph.Node
4 |
5 | internal class SugiyamaNodeData {
6 | val reversed = mutableSetOf()
7 | var isDummy = false
8 | var median = -1
9 | var layer = -1
10 |
11 | val isReversed: Boolean
12 | get() = reversed.isNotEmpty()
13 |
14 | override fun toString(): String {
15 | return "SugiyamaNodeData{" +
16 | ", reversed=" + reversed +
17 | ", dummy=" + isDummy +
18 | ", median=" + median +
19 | ", layer=" + layer +
20 | '}'.toString()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerConfiguration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.tree
2 |
3 | class BuchheimWalkerConfiguration private constructor(builder: Builder) {
4 | val siblingSeparation: Int
5 | val levelSeparation: Int
6 | val subtreeSeparation: Int
7 | val orientation: Int
8 |
9 | init {
10 | this.siblingSeparation = builder.siblingSeparation
11 | this.levelSeparation = builder.levelSeparation
12 | this.subtreeSeparation = builder.subtreeSeparation
13 | this.orientation = builder.orientation
14 | }
15 |
16 | class Builder {
17 | var siblingSeparation = DEFAULT_SIBLING_SEPARATION
18 | private set
19 | var levelSeparation = DEFAULT_LEVEL_SEPARATION
20 | private set
21 | var subtreeSeparation = DEFAULT_SUBTREE_SEPARATION
22 | private set
23 | var orientation = DEFAULT_ORIENTATION
24 | private set
25 |
26 | fun setSiblingSeparation(siblingSeparation: Int) = apply {
27 | this.siblingSeparation = siblingSeparation
28 | }
29 |
30 | fun setLevelSeparation(levelSeparation: Int) = apply {
31 | this.levelSeparation = levelSeparation
32 | }
33 |
34 | fun setSubtreeSeparation(subtreeSeparation: Int) = apply {
35 | this.subtreeSeparation = subtreeSeparation
36 | }
37 |
38 | fun setOrientation(orientation: Int) = apply {
39 | this.orientation = orientation
40 | }
41 |
42 | fun build() = BuchheimWalkerConfiguration(this)
43 | }
44 |
45 | companion object {
46 | // TODO: refactor to sealed class
47 | const val ORIENTATION_TOP_BOTTOM = 1
48 | const val ORIENTATION_BOTTOM_TOP = 2
49 | const val ORIENTATION_LEFT_RIGHT = 3
50 | const val ORIENTATION_RIGHT_LEFT = 4
51 |
52 | const val DEFAULT_SIBLING_SEPARATION = 100
53 | const val DEFAULT_SUBTREE_SEPARATION = 100
54 | const val DEFAULT_LEVEL_SEPARATION = 100
55 | const val DEFAULT_ORIENTATION = ORIENTATION_TOP_BOTTOM
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerLayoutManager.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.tree
2 |
3 | import android.content.Context
4 | import dev.bandb.graphview.graph.Graph
5 | import dev.bandb.graphview.graph.Node
6 | import dev.bandb.graphview.layouts.GraphLayoutManager
7 | import dev.bandb.graphview.util.Size
8 | import dev.bandb.graphview.util.VectorF
9 | import java.util.*
10 | import kotlin.math.max
11 | import kotlin.math.min
12 |
13 | class BuchheimWalkerLayoutManager @JvmOverloads constructor(private val context: Context, val configuration: BuchheimWalkerConfiguration = BuchheimWalkerConfiguration.Builder().build()) : GraphLayoutManager(context) {
14 | private val mNodeData: MutableMap = HashMap()
15 | private var minNodeHeight = Integer.MAX_VALUE
16 | private var minNodeWidth = Integer.MAX_VALUE
17 | private var maxNodeWidth = Integer.MIN_VALUE
18 | private var maxNodeHeight = Integer.MIN_VALUE
19 |
20 | private val isVertical: Boolean
21 | get() {
22 | val orientation = configuration.orientation
23 | return orientation == BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM || orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP
24 | }
25 |
26 | private fun compare(x: Int, y: Int): Int {
27 | return if (x < y) -1 else if (x == y) 0 else 1
28 | }
29 |
30 | private fun createNodeData(node: Node): BuchheimWalkerNodeData {
31 | val nodeData = BuchheimWalkerNodeData().apply {
32 | ancestor = node
33 | }
34 | mNodeData[node] = nodeData
35 |
36 | return nodeData
37 | }
38 |
39 | private fun getNodeData(node: Node): BuchheimWalkerNodeData {
40 | return mNodeData.getValue(node)
41 | }
42 |
43 | private fun firstWalk(graph: Graph, node: Node, depth: Int, number: Int) {
44 | val nodeData = createNodeData(node)
45 | nodeData.depth = depth
46 | nodeData.number = number
47 | minNodeHeight = min(minNodeHeight, node.height)
48 | minNodeWidth = min(minNodeWidth, node.width)
49 | maxNodeWidth = max(maxNodeWidth, node.width)
50 | maxNodeHeight = max(maxNodeHeight, node.height)
51 |
52 | if (isLeaf(graph, node)) {
53 | // if the node has no left sibling, prelim(node) should be set to 0, but we don't have to set it
54 | // here, because it's already initialized with 0
55 | if (hasLeftSibling(graph, node)) {
56 | val leftSibling = getLeftSibling(graph, node)
57 | nodeData.prelim = getPrelim(leftSibling!!) + getSpacing(graph, leftSibling, node)
58 | }
59 | } else {
60 | val leftMost = getLeftMostChild(graph, node)
61 | val rightMost = getRightMostChild(graph, node)
62 | var defaultAncestor = leftMost
63 |
64 | var next: Node? = leftMost
65 | var i = 1
66 | while (next != null) {
67 | firstWalk(graph, next, depth + 1, i++)
68 | defaultAncestor = apportion(graph, next, defaultAncestor)
69 |
70 | next = getRightSibling(graph, next)
71 | }
72 |
73 | executeShifts(graph, node)
74 |
75 | val isVertical = isVertical
76 | val midPoint = 0.5 * ((getPrelim(leftMost) + getPrelim(rightMost!!)
77 | + (if (isVertical) rightMost.width else rightMost.height).toDouble()) - if (isVertical) node.width else node.height)
78 |
79 | if (hasLeftSibling(graph, node)) {
80 | val leftSibling = getLeftSibling(graph, node)
81 | nodeData.prelim = getPrelim(leftSibling!!) + getSpacing(graph, leftSibling, node)
82 | nodeData.modifier = nodeData.prelim - midPoint
83 | } else {
84 | nodeData.prelim = midPoint
85 | }
86 | }
87 | }
88 |
89 | private fun secondWalk(graph: Graph, node: Node, modifier: Double) {
90 | val nodeData = getNodeData(node)
91 | val depth = nodeData.depth
92 |
93 | val vertical = isVertical
94 | node.setPosition(
95 | VectorF(
96 | (nodeData.prelim + modifier).toFloat(),
97 | (depth * (if (vertical) minNodeHeight else minNodeWidth) + depth * configuration.levelSeparation).toFloat()
98 | )
99 | )
100 |
101 | graph.successorsOf(node).forEach { w ->
102 | secondWalk(graph, w, modifier + nodeData.modifier)
103 | }
104 |
105 | }
106 |
107 | private fun calculateGraphSize(graph: Graph): Size {
108 | var left = Integer.MAX_VALUE
109 | var top = Integer.MAX_VALUE
110 | var right = Integer.MIN_VALUE
111 | var bottom = Integer.MIN_VALUE
112 | graph.nodes.forEach { node ->
113 | left = min(left.toFloat(), node.x).toInt()
114 | top = min(top.toFloat(), node.y).toInt()
115 | right = max(right.toFloat(), node.x + node.width).toInt()
116 | bottom = max(bottom.toFloat(), node.y + node.height).toInt()
117 | }
118 |
119 | return Size(right - left, bottom - top)
120 | }
121 |
122 |
123 | private fun executeShifts(graph: Graph, node: Node) {
124 | var shift = 0.0
125 | var change = 0.0
126 | var w = getRightMostChild(graph, node)
127 | while (w != null) {
128 | val nodeData = getNodeData(w)
129 |
130 | nodeData.prelim = nodeData.prelim + shift
131 | nodeData.modifier = nodeData.modifier + shift
132 | change += nodeData.change
133 | shift += nodeData.shift + change
134 |
135 | w = getLeftSibling(graph, w)
136 | }
137 | }
138 |
139 | private fun apportion(graph: Graph, node: Node, defaultAncestor: Node): Node {
140 | var ancestor = defaultAncestor
141 | if (hasLeftSibling(graph, node)) {
142 | val leftSibling = getLeftSibling(graph, node)
143 |
144 | var vip = node
145 | var vop: Node? = node
146 | var vim = leftSibling
147 | var vom: Node? = getLeftMostChild(graph, graph.predecessorsOf(vip)[0])
148 |
149 | var sip = getModifier(vip)
150 | var sop = getModifier(vop!!)
151 | var sim = getModifier(vim!!)
152 | var som = getModifier(vom!!)
153 |
154 | var nextRight = nextRight(graph, vim)
155 | var nextLeft = nextLeft(graph, vip)
156 |
157 | while (nextRight != null && nextLeft != null) {
158 | vim = nextRight
159 | vip = nextLeft
160 | vom = nextLeft(graph, vom)
161 | vop = nextRight(graph, vop)
162 |
163 | setAncestor(vop!!, node)
164 |
165 | val shift =
166 | getPrelim(vim) + sim - (getPrelim(vip) + sip) + getSpacing(graph, vim, node)
167 | if (shift > 0) {
168 | moveSubtree(ancestor(graph, vim, node, ancestor), node, shift)
169 | sip += shift
170 | sop += shift
171 | }
172 |
173 | sim += getModifier(vim)
174 | sip += getModifier(vip)
175 | som += getModifier(vom!!)
176 | sop += getModifier(vop)
177 |
178 | nextRight = nextRight(graph, vim)
179 | nextLeft = nextLeft(graph, vip)
180 | }
181 |
182 | if (nextRight != null && nextRight(graph, vop) == null) {
183 | setThread(vop!!, nextRight)
184 | setModifier(vop, getModifier(vop) + sim - sop)
185 | }
186 |
187 | if (nextLeft != null && nextLeft(graph, vom) == null) {
188 | setThread(vom!!, nextLeft)
189 | setModifier(vom, getModifier(vom) + sip - som)
190 | ancestor = node
191 | }
192 | }
193 |
194 | return ancestor
195 | }
196 |
197 | private fun setAncestor(v: Node, ancestor: Node) {
198 | getNodeData(v).ancestor = ancestor
199 | }
200 |
201 | private fun setModifier(v: Node, modifier: Double) {
202 | getNodeData(v).modifier = modifier
203 | }
204 |
205 | private fun setThread(v: Node, thread: Node?) {
206 | getNodeData(v).thread = thread
207 | }
208 |
209 | private fun getPrelim(v: Node): Double {
210 | return getNodeData(v).prelim
211 | }
212 |
213 | private fun getModifier(vip: Node): Double {
214 | return getNodeData(vip).modifier
215 | }
216 |
217 | private fun moveSubtree(wm: Node, wp: Node, shift: Double) {
218 | val wpNodeData = getNodeData(wp)
219 | val wmNodeData = getNodeData(wm)
220 |
221 | val subtrees = wpNodeData.number - wmNodeData.number
222 | wpNodeData.change = wpNodeData.change - shift / subtrees
223 | wpNodeData.shift = wpNodeData.shift + shift
224 | wmNodeData.change = wmNodeData.change + shift / subtrees
225 | wpNodeData.prelim = wpNodeData.prelim + shift
226 | wpNodeData.modifier = wpNodeData.modifier + shift
227 | }
228 |
229 | private fun ancestor(graph: Graph, vim: Node, node: Node, defaultAncestor: Node): Node {
230 | val vipNodeData = getNodeData(vim)
231 |
232 | return if (graph.predecessorsOf(vipNodeData.ancestor)[0] === graph.predecessorsOf(node)[0]) {
233 | vipNodeData.ancestor
234 | } else defaultAncestor
235 | }
236 |
237 | private fun nextRight(graph: Graph, node: Node?): Node? {
238 | return if (graph.hasSuccessor(node!!)) {
239 | getRightMostChild(graph, node)
240 | } else getNodeData(node).thread
241 | }
242 |
243 | private fun nextLeft(graph: Graph, node: Node?): Node? {
244 | return if (graph.hasSuccessor(node!!)) {
245 | getLeftMostChild(graph, node)
246 | } else getNodeData(node).thread
247 | }
248 |
249 | private fun getSpacing(graph: Graph, leftNode: Node?, rightNode: Node): Int {
250 | var separation = configuration.subtreeSeparation
251 |
252 | if (isSibling(graph, leftNode, rightNode)) {
253 | separation = configuration.siblingSeparation
254 | }
255 |
256 | val vertical = isVertical
257 | return separation + if (vertical) leftNode!!.width else leftNode!!.height
258 | }
259 |
260 | private fun isSibling(graph: Graph, leftNode: Node?, rightNode: Node): Boolean {
261 | val leftParent = graph.predecessorsOf(leftNode!!)[0]
262 | return graph.successorsOf(leftParent).contains(rightNode)
263 | }
264 |
265 | private fun isLeaf(graph: Graph, node: Node): Boolean {
266 | return graph.successorsOf(node).isEmpty()
267 | }
268 |
269 | private fun getLeftSibling(graph: Graph, node: Node): Node? {
270 | if (!hasLeftSibling(graph, node)) {
271 | return null
272 | }
273 |
274 | val parent = graph.predecessorsOf(node)[0]
275 | val children = graph.successorsOf(parent)
276 | val nodeIndex = children.indexOf(node)
277 | return children[nodeIndex - 1]
278 | }
279 |
280 | private fun hasLeftSibling(graph: Graph, node: Node): Boolean {
281 | val parents = graph.predecessorsOf(node)
282 | if (parents.isEmpty()) {
283 | return false
284 | }
285 |
286 | val parent = parents[0]
287 | val nodeIndex = graph.successorsOf(parent).indexOf(node)
288 | return nodeIndex > 0
289 | }
290 |
291 | private fun getRightSibling(graph: Graph, node: Node): Node? {
292 | if (!hasRightSibling(graph, node)) {
293 | return null
294 | }
295 |
296 | val parent = graph.predecessorsOf(node)[0]
297 | val children = graph.successorsOf(parent)
298 | val nodeIndex = children.indexOf(node)
299 | return children[nodeIndex + 1]
300 | }
301 |
302 | private fun hasRightSibling(graph: Graph, node: Node): Boolean {
303 | val parents = graph.predecessorsOf(node)
304 | if (parents.isEmpty()) {
305 | return false
306 | }
307 | val parent = parents[0]
308 | val children = graph.successorsOf(parent)
309 | val nodeIndex = children.indexOf(node)
310 | return nodeIndex < children.size - 1
311 | }
312 |
313 | private fun getLeftMostChild(graph: Graph, node: Node): Node {
314 | return graph.successorsOf(node)[0]
315 | }
316 |
317 | private fun getRightMostChild(graph: Graph, node: Node): Node? {
318 | val children = graph.successorsOf(node)
319 | return if (children.isEmpty()) {
320 | null
321 | } else children[children.size - 1]
322 | }
323 |
324 | override fun run(graph: Graph, shiftX: Float, shiftY: Float): Size {
325 | // TODO check for cycles and multiple parents
326 | mNodeData.clear()
327 | graph.setAsTree(true)
328 |
329 | val firstNode = graph.getNodeAtPosition(0)
330 | firstWalk(graph, firstNode, 0, 0)
331 |
332 | secondWalk(graph, firstNode, 0.0)
333 |
334 | positionNodes(graph)
335 |
336 | shiftCoordinates(graph, shiftX, shiftY)
337 |
338 | return calculateGraphSize(graph)
339 | }
340 |
341 | private fun positionNodes(graph: Graph) {
342 | var globalPadding = 0
343 | var localPadding = 0
344 | val offset = getOffset(graph)
345 |
346 | val orientation = configuration.orientation
347 | val needReverseOrder =
348 | orientation == BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP || orientation == BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT
349 | val nodes = sortByLevel(graph, needReverseOrder)
350 |
351 | val firstLevel = getNodeData(nodes[0]).depth
352 | var localMaxSize = findMaxSize(filterByLevel(nodes, firstLevel))
353 | var currentLevel = if (needReverseOrder) firstLevel else 0
354 |
355 | nodes.forEach { node ->
356 | val depth = getNodeData(node).depth
357 | if (depth != currentLevel) {
358 | when (configuration.orientation) {
359 | BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM, BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> globalPadding += localPadding
360 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP, BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> globalPadding -= localPadding
361 | }
362 | localPadding = 0
363 | currentLevel = depth
364 |
365 | localMaxSize = findMaxSize(filterByLevel(nodes, currentLevel))
366 | }
367 |
368 | val height = node.height
369 | val width = node.width
370 | when (configuration.orientation) {
371 | BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM -> if (height > minNodeHeight) {
372 | val diff = height - minNodeHeight
373 | localPadding = max(localPadding, diff)
374 | }
375 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP -> if (height < localMaxSize.height) {
376 | val diff = localMaxSize.height - height
377 | node.setPosition(node.position.subtract(VectorF(0f, diff.toFloat())))
378 | localPadding = max(localPadding, diff)
379 | }
380 | BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> if (width > minNodeWidth) {
381 | val diff = width - minNodeWidth
382 | localPadding = max(localPadding, diff)
383 | }
384 | BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> if (width < localMaxSize.width) {
385 | val diff = localMaxSize.width - width
386 | node.setPosition(node.position.subtract(VectorF(0f, diff.toFloat())))
387 | localPadding = max(localPadding, diff)
388 | }
389 | }
390 |
391 | node.setPosition(getPosition(node, globalPadding, offset))
392 | }
393 | }
394 |
395 | private fun shiftCoordinates(graph: Graph, shiftX: Float, shiftY: Float) {
396 | graph.nodes.forEach { node ->
397 | node.setPosition(VectorF(node.x + shiftX, node.y + shiftY))
398 | }
399 | }
400 |
401 | private fun findMaxSize(nodes: List): Size {
402 | var width = Integer.MIN_VALUE
403 | var height = Integer.MIN_VALUE
404 |
405 | nodes.forEach { node ->
406 | width = max(width, node.width)
407 | height = max(height, node.height)
408 | }
409 |
410 | return Size(width, height)
411 | }
412 |
413 | private fun getOffset(graph: Graph): VectorF {
414 | var offsetX = java.lang.Float.MAX_VALUE
415 | var offsetY = java.lang.Float.MAX_VALUE
416 | when (configuration.orientation) {
417 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP, BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> offsetY =
418 | java.lang.Float.MIN_VALUE
419 | }
420 |
421 | val orientation = configuration.orientation
422 | graph.nodes.forEach { node ->
423 | when (orientation) {
424 | BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM, BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> {
425 | offsetX = min(offsetX, node.x)
426 | offsetY = min(offsetY, node.y)
427 | }
428 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP, BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> {
429 | offsetX = min(offsetX, node.x)
430 | offsetY = max(offsetY, node.y)
431 | }
432 | }
433 | }
434 | return VectorF(offsetX, offsetY)
435 | }
436 |
437 | private fun getPosition(node: Node, globalPadding: Int, offset: VectorF): VectorF =
438 | when (configuration.orientation) {
439 | BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM -> VectorF(
440 | node.x - offset.x,
441 | node.y + globalPadding
442 | )
443 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP -> VectorF(
444 | node.x - offset.x,
445 | offset.y - node.y - globalPadding.toFloat()
446 | )
447 | BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> VectorF(
448 | node.y + globalPadding,
449 | node.x - offset.x
450 | )
451 | BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> VectorF(
452 | offset.y - node.y - globalPadding.toFloat(),
453 | node.x - offset.x
454 | )
455 | else -> {
456 | throw IllegalStateException("Unknown Orientation! ${configuration.orientation}")
457 | }
458 | }
459 |
460 | private fun sortByLevel(graph: Graph, descending: Boolean): List {
461 | val nodes = ArrayList(graph.nodes)
462 | var comparator = Comparator { o1, o2 ->
463 | val data1 = getNodeData(o1)
464 | val data2 = getNodeData(o2)
465 | compare(data1.depth, data2.depth)
466 | }
467 |
468 | if (descending) {
469 | comparator = Collections.reverseOrder(comparator)
470 | }
471 |
472 | Collections.sort(nodes, comparator)
473 |
474 | return nodes
475 | }
476 |
477 | private fun filterByLevel(nodes: List, level: Int): List {
478 | val nodeList = ArrayList(nodes)
479 |
480 | val iterator = nodeList.iterator()
481 | while (iterator.hasNext()) {
482 | val node = iterator.next()
483 | val depth = getNodeData(node).depth
484 | if (depth != level) {
485 | iterator.remove()
486 | }
487 | }
488 |
489 | return nodeList
490 | }
491 | }
492 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerNodeData.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.tree
2 |
3 | import dev.bandb.graphview.graph.Node
4 |
5 | internal class BuchheimWalkerNodeData {
6 | lateinit var ancestor: Node
7 | var thread: Node? = null
8 | var number: Int = 0
9 | var depth: Int = 0
10 | var prelim: Double = 0.toDouble()
11 | var modifier: Double = 0.toDouble()
12 | var shift: Double = 0.toDouble()
13 | var change: Double = 0.toDouble()
14 | }
15 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/layouts/tree/TreeEdgeDecoration.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.layouts.tree
2 |
3 | import android.graphics.*
4 | import androidx.recyclerview.widget.RecyclerView
5 | import dev.bandb.graphview.AbstractGraphAdapter
6 |
7 |
8 | open class TreeEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
9 | strokeWidth = 5f
10 | color = Color.BLACK
11 | style = Paint.Style.STROKE
12 | strokeJoin = Paint.Join.ROUND
13 | pathEffect = CornerPathEffect(10f)
14 | }) : RecyclerView.ItemDecoration() {
15 |
16 | private val linePath = Path()
17 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
18 | val adapter = parent.adapter
19 | if (parent.layoutManager == null || adapter == null) {
20 | return
21 | }
22 | if (adapter !is AbstractGraphAdapter) {
23 | throw RuntimeException(
24 | "TreeEdgeDecoration only works with ${AbstractGraphAdapter::class.simpleName}")
25 | }
26 | val layout = parent.layoutManager
27 | if (layout !is BuchheimWalkerLayoutManager) {
28 | throw RuntimeException(
29 | "TreeEdgeDecoration only works with ${BuchheimWalkerLayoutManager::class.simpleName}")
30 | }
31 |
32 | val configuration = layout.configuration
33 |
34 | val graph = adapter.graph
35 | if (graph != null && graph.hasNodes()) {
36 | val nodes = graph.nodes
37 |
38 | for (node in nodes) {
39 | val children = graph.successorsOf(node)
40 |
41 | for (child in children) {
42 | linePath.reset()
43 | when (configuration.orientation) {
44 | BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM -> {
45 | // position at the middle-top of the child
46 | linePath.moveTo(child.x + child.width / 2f, child.y)
47 | // draws a line from the child's middle-top halfway up to its parent
48 | linePath.lineTo(
49 | child.x + child.width / 2f,
50 | child.y - configuration.levelSeparation / 2f
51 | )
52 | // draws a line from the previous point to the middle of the parents width
53 | linePath.lineTo(
54 | node.x + node.width / 2f,
55 | child.y - configuration.levelSeparation / 2f
56 | )
57 |
58 | // position at the middle of the level separation under the parent
59 | linePath.moveTo(
60 | node.x + node.width / 2f,
61 | child.y - configuration.levelSeparation / 2f
62 | )
63 | // draws a line up to the parents middle-bottom
64 | linePath.lineTo(
65 | node.x + node.width / 2f,
66 | node.y + node.height
67 | )
68 | }
69 | BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP -> {
70 | linePath.moveTo(child.x + child.width / 2f, child.y + child.height)
71 | linePath.lineTo(
72 | child.x + child.width / 2f,
73 | child.y + child.height.toFloat() + configuration.levelSeparation / 2f
74 | )
75 | linePath.lineTo(
76 | node.x + node.width / 2f,
77 | child.y + child.height.toFloat() + configuration.levelSeparation / 2f
78 | )
79 |
80 | linePath.moveTo(
81 | node.x + node.width / 2f,
82 | child.y + child.height.toFloat() + configuration.levelSeparation / 2f
83 | )
84 | linePath.lineTo(
85 | node.x + node.width / 2f,
86 | node.y + node.height
87 | )
88 | }
89 | BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> {
90 | linePath.moveTo(child.x, child.y + child.height / 2f)
91 | linePath.lineTo(
92 | child.x - configuration.levelSeparation / 2f,
93 | child.y + child.height / 2f
94 | )
95 | linePath.lineTo(
96 | child.x - configuration.levelSeparation / 2f,
97 | node.y + node.height / 2f
98 | )
99 |
100 | linePath.moveTo(
101 | child.x - configuration.levelSeparation / 2f,
102 | node.y + node.height / 2f
103 | )
104 | linePath.lineTo(
105 | node.x + node.width,
106 | node.y + node.height / 2f
107 | )
108 | }
109 | BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> {
110 | linePath.moveTo(child.x + child.width, child.y + child.height / 2f)
111 | linePath.lineTo(
112 | child.x + child.width.toFloat() + configuration.levelSeparation / 2f,
113 | child.y + child.height / 2f
114 | )
115 | linePath.lineTo(
116 | child.x + child.width.toFloat() + configuration.levelSeparation / 2f,
117 | node.y + node.height / 2f
118 | )
119 |
120 | linePath.moveTo(
121 | child.x + child.width.toFloat() + configuration.levelSeparation / 2f,
122 | node.y + node.height / 2f
123 | )
124 | linePath.lineTo(
125 | node.x + node.width,
126 | node.y + node.height / 2f
127 | )
128 | }
129 | }
130 |
131 | c.drawPath(linePath, linePaint)
132 | }
133 | }
134 | }
135 | super.onDraw(c, parent, state)
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/util/Size.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.util
2 |
3 | data class Size(var width: Int = 0, var height: Int = 0)
4 |
--------------------------------------------------------------------------------
/graphview/src/main/java/dev/bandb/graphview/util/VectorF.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.util
2 |
3 | import kotlin.math.sqrt
4 |
5 | data class VectorF constructor(var x: Float = 0f, var y: Float = 0f) {
6 |
7 | fun add(operand: VectorF): VectorF {
8 | return VectorF(operand.x + x, operand.y + y)
9 | }
10 |
11 | fun add(x: Float, y: Float): VectorF {
12 | return VectorF(this.x + x, this.y + y)
13 | }
14 |
15 | fun subtract(operand: VectorF): VectorF {
16 | return VectorF(x - operand.x, y - operand.y)
17 | }
18 |
19 | fun subtract(x: Float, y: Float): VectorF {
20 | return VectorF(this.x - x, this.y - y)
21 | }
22 |
23 | fun multiply(operand: VectorF): VectorF {
24 | return VectorF(x * operand.x, y * operand.y)
25 | }
26 |
27 | fun multiply(operand: Float): VectorF {
28 | return VectorF(x * operand, y * operand)
29 | }
30 |
31 | fun divide(operand: VectorF): VectorF {
32 | return VectorF(x / operand.x, y / operand.y)
33 | }
34 |
35 | fun divide(operand: Float): VectorF {
36 | return VectorF(x / operand, y / operand)
37 | }
38 |
39 | fun length(): Float {
40 | return sqrt((x * x + y * y).toDouble()).toFloat()
41 | }
42 | }
--------------------------------------------------------------------------------
/image/Graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/image/Graph.png
--------------------------------------------------------------------------------
/image/GraphView_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/image/GraphView_logo.jpg
--------------------------------------------------------------------------------
/image/LayeredGraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/image/LayeredGraph.png
--------------------------------------------------------------------------------
/image/Tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/image/Tree.png
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion 30
6 | buildToolsVersion "30.0.5"
7 |
8 | defaultConfig {
9 | applicationId "dev.bandb.graphview.sample"
10 | minSdkVersion 18
11 | targetSdkVersion 30
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 |
23 | compileOptions {
24 | sourceCompatibility 1.8
25 | targetCompatibility 1.8
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation fileTree(dir: 'libs', include: ['*.jar'])
31 | testImplementation 'junit:junit:4.13.2'
32 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
34 | implementation "androidx.recyclerview:recyclerview:1.2.0"
35 | implementation 'androidx.appcompat:appcompat:1.2.0'
36 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0-beta01'
37 | implementation 'com.otaliastudios:zoomlayout:1.8.0'
38 | implementation 'com.google.android.material:material:1.3.0'
39 | implementation project(path: ':graphview')
40 | }
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\DennisBlock\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/GraphActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.TextView
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.appcompat.widget.Toolbar
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.google.android.material.floatingactionbutton.FloatingActionButton
12 | import com.google.android.material.snackbar.Snackbar
13 | import dev.bandb.graphview.AbstractGraphAdapter
14 | import dev.bandb.graphview.graph.Graph
15 | import dev.bandb.graphview.graph.Node
16 | import java.util.*
17 |
18 | abstract class GraphActivity : AppCompatActivity() {
19 | protected lateinit var recyclerView: RecyclerView
20 | protected lateinit var adapter: AbstractGraphAdapter
21 | private lateinit var fab: FloatingActionButton
22 | private var currentNode: Node? = null
23 | private var nodeCount = 1
24 |
25 | protected abstract fun createGraph(): Graph
26 | protected abstract fun setLayoutManager()
27 | protected abstract fun setEdgeDecoration()
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(R.layout.activity_graph)
32 |
33 | val graph = createGraph()
34 | recyclerView = findViewById(R.id.recycler)
35 | setLayoutManager()
36 | setEdgeDecoration()
37 | setupGraphView(graph)
38 |
39 | setupFab(graph)
40 | setupToolbar()
41 | }
42 |
43 | private fun setupGraphView(graph: Graph) {
44 | adapter = object : AbstractGraphAdapter() {
45 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
46 | val view = LayoutInflater.from(parent.context)
47 | .inflate(R.layout.node, parent, false)
48 | return NodeViewHolder(view)
49 | }
50 |
51 | override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
52 | holder.textView.text = Objects.requireNonNull(getNodeData(position)).toString()
53 | }
54 | }.apply {
55 | this.submitGraph(graph)
56 | recyclerView.adapter = this
57 | }
58 | }
59 |
60 | private fun setupFab(graph: Graph) {
61 | fab = findViewById(R.id.addNode)
62 | fab.setOnClickListener {
63 | val newNode = Node(nodeText)
64 | if (currentNode != null) {
65 | graph.addEdge(currentNode!!, newNode)
66 | } else {
67 | graph.addNode(newNode)
68 | }
69 | adapter.notifyDataSetChanged()
70 | }
71 | fab.setOnLongClickListener {
72 | if (currentNode != null) {
73 | graph.removeNode(currentNode!!)
74 | currentNode = null
75 | adapter.notifyDataSetChanged()
76 | fab.hide()
77 | }
78 | true
79 | }
80 | }
81 |
82 | private fun setupToolbar() {
83 | val toolbar = findViewById(R.id.toolbar)
84 | setSupportActionBar(toolbar)
85 | val ab = supportActionBar
86 | if (ab != null) {
87 | ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back)
88 | ab.setDisplayHomeAsUpEnabled(true)
89 | }
90 | }
91 |
92 | override fun onSupportNavigateUp(): Boolean {
93 | onBackPressed()
94 | return true
95 | }
96 |
97 | protected inner class NodeViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
98 | var textView: TextView = itemView.findViewById(R.id.textView)
99 |
100 | init {
101 | itemView.setOnClickListener {
102 | if (!fab.isShown) {
103 | fab.show()
104 | }
105 | currentNode = adapter.getNode(bindingAdapterPosition)
106 | Snackbar.make(itemView, "Clicked on " + adapter.getNodeData(bindingAdapterPosition)?.toString(),
107 | Snackbar.LENGTH_SHORT).show()
108 | }
109 | }
110 | }
111 |
112 | protected val nodeText: String
113 | get() = "Node " + nodeCount++
114 | }
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.TextView
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.appcompat.widget.Toolbar
11 | import androidx.recyclerview.widget.DividerItemDecoration
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import androidx.recyclerview.widget.RecyclerView
14 |
15 | class MainActivity : AppCompatActivity() {
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | setContentView(R.layout.activity_main)
19 | setupToolbar()
20 | setupRecyclerView()
21 | }
22 |
23 | private fun setupRecyclerView() {
24 | findViewById(R.id.graphs).apply {
25 | layoutManager = LinearLayoutManager(this@MainActivity)
26 | adapter = GraphListAdapter()
27 | val decoration = DividerItemDecoration(applicationContext, DividerItemDecoration.VERTICAL)
28 | addItemDecoration(decoration)
29 | }
30 | }
31 |
32 | private fun setupToolbar() {
33 | val toolbar = findViewById(R.id.toolbar)
34 | setSupportActionBar(toolbar)
35 | val ab = supportActionBar
36 | ab?.setDisplayHomeAsUpEnabled(false)
37 | }
38 |
39 | private inner class GraphListAdapter : RecyclerView.Adapter() {
40 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphViewHolder {
41 | val view = LayoutInflater.from(parent.context)
42 | .inflate(R.layout.main_item, parent, false)
43 | return GraphViewHolder(view)
44 | }
45 |
46 | override fun onBindViewHolder(holder: GraphViewHolder, position: Int) {
47 | val graphItem = MainContent.ITEMS[position]
48 | holder.title.text = graphItem.title
49 | holder.description.text = graphItem.description
50 | holder.itemView.setOnClickListener { startActivity(Intent(this@MainActivity, graphItem.clazz)) }
51 | }
52 |
53 | override fun getItemCount(): Int {
54 | return MainContent.ITEMS.size
55 | }
56 | }
57 |
58 | private inner class GraphViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
59 | val title: TextView = itemView.findViewById(R.id.title)
60 | val description: TextView = itemView.findViewById(R.id.description)
61 | }
62 | }
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/MainContent.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample
2 |
3 | import dev.bandb.graphview.sample.algorithms.BuchheimWalkerActivity
4 | import dev.bandb.graphview.sample.algorithms.FruchtermanReingoldActivity
5 | import dev.bandb.graphview.sample.algorithms.SugiyamaActivity
6 | import java.util.*
7 |
8 | object MainContent {
9 | val ITEMS: MutableList = ArrayList()
10 |
11 | class GraphItem(val title: String, val description: String, val clazz: Class<*>) {
12 | override fun toString(): String {
13 | return title
14 | }
15 | }
16 |
17 | init {
18 | ITEMS.add(GraphItem("BuchheimWalker", "Algorithm for drawing tree structures", BuchheimWalkerActivity::class.java))
19 | ITEMS.add(GraphItem("FruchtermanReingold", "Directed graph drawing by simulating attraction/repulsion forces", FruchtermanReingoldActivity::class.java))
20 | ITEMS.add(GraphItem("Sugiyama et al.", "Algorithm for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph.", SugiyamaActivity::class.java))
21 | }
22 | }
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/algorithms/BuchheimWalkerActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample.algorithms
2 |
3 | import android.view.Menu
4 | import android.view.MenuItem
5 | import dev.bandb.graphview.graph.Graph
6 | import dev.bandb.graphview.graph.Node
7 | import dev.bandb.graphview.layouts.tree.BuchheimWalkerConfiguration
8 | import dev.bandb.graphview.layouts.tree.BuchheimWalkerLayoutManager
9 | import dev.bandb.graphview.layouts.tree.TreeEdgeDecoration
10 | import dev.bandb.graphview.sample.GraphActivity
11 | import dev.bandb.graphview.sample.R
12 |
13 | class BuchheimWalkerActivity : GraphActivity() {
14 |
15 | public override fun setLayoutManager() {
16 | val configuration = BuchheimWalkerConfiguration.Builder()
17 | .setSiblingSeparation(100)
18 | .setLevelSeparation(100)
19 | .setSubtreeSeparation(100)
20 | .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM)
21 | .build()
22 | recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, configuration)
23 | }
24 |
25 | public override fun setEdgeDecoration() {
26 | recyclerView.addItemDecoration(TreeEdgeDecoration())
27 | }
28 |
29 | public override fun createGraph(): Graph {
30 | val graph = Graph()
31 | val node1 = Node(nodeText)
32 | val node2 = Node(nodeText)
33 | val node3 = Node(nodeText)
34 | val node4 = Node(nodeText)
35 | val node5 = Node(nodeText)
36 | val node6 = Node(nodeText)
37 | val node8 = Node(nodeText)
38 | val node7 = Node(nodeText)
39 | val node9 = Node(nodeText)
40 | val node10 = Node(nodeText)
41 | val node11 = Node(nodeText)
42 | val node12 = Node(nodeText)
43 | graph.addEdge(node1, node2)
44 | graph.addEdge(node1, node3)
45 | graph.addEdge(node1, node4)
46 | graph.addEdge(node2, node5)
47 | graph.addEdge(node2, node6)
48 | graph.addEdge(node6, node7)
49 | graph.addEdge(node6, node8)
50 | graph.addEdge(node4, node9)
51 | graph.addEdge(node4, node10)
52 | graph.addEdge(node4, node11)
53 | graph.addEdge(node11, node12)
54 | return graph
55 | }
56 |
57 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
58 | val inflater = menuInflater
59 | inflater.inflate(R.menu.menu_buchheim_walker_orientations, menu)
60 | return true
61 | }
62 |
63 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
64 | val builder = BuchheimWalkerConfiguration.Builder()
65 | .setSiblingSeparation(100)
66 | .setLevelSeparation(300)
67 | .setSubtreeSeparation(300)
68 | val itemId = item.itemId
69 | if (itemId == R.id.topToBottom) {
70 | builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM)
71 | } else if (itemId == R.id.bottomToTop) {
72 | builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP)
73 | } else if (itemId == R.id.leftToRight) {
74 | builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT)
75 | } else if (itemId == R.id.rightToLeft) {
76 | builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT)
77 | } else {
78 | return super.onOptionsItemSelected(item)
79 | }
80 | recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, builder.build())
81 | recyclerView.adapter = adapter
82 | return true
83 | }
84 | }
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/algorithms/FruchtermanReingoldActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample.algorithms
2 |
3 | import dev.bandb.graphview.decoration.edge.ArrowEdgeDecoration
4 | import dev.bandb.graphview.graph.Graph
5 | import dev.bandb.graphview.graph.Node
6 | import dev.bandb.graphview.layouts.energy.FruchtermanReingoldLayoutManager
7 | import dev.bandb.graphview.sample.GraphActivity
8 |
9 | class FruchtermanReingoldActivity : GraphActivity() {
10 |
11 | public override fun setLayoutManager() {
12 | recyclerView.layoutManager = FruchtermanReingoldLayoutManager(this, 1000)
13 | }
14 |
15 | public override fun setEdgeDecoration() {
16 | recyclerView.addItemDecoration(ArrowEdgeDecoration())
17 | }
18 |
19 | public override fun createGraph(): Graph {
20 | val graph = Graph()
21 | val a = Node(nodeText)
22 | val b = Node(nodeText)
23 | val c = Node(nodeText)
24 | val d = Node(nodeText)
25 | val e = Node(nodeText)
26 | val f = Node(nodeText)
27 | val g = Node(nodeText)
28 | val h = Node(nodeText)
29 | graph.addEdge(a, b)
30 | graph.addEdge(a, c)
31 | graph.addEdge(a, d)
32 | graph.addEdge(c, e)
33 | graph.addEdge(d, f)
34 | graph.addEdge(f, c)
35 | graph.addEdge(g, c)
36 | graph.addEdge(h, g)
37 | return graph
38 | }
39 | }
--------------------------------------------------------------------------------
/sample/src/main/java/dev/bandb/graphview/sample/algorithms/SugiyamaActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.bandb.graphview.sample.algorithms
2 |
3 | import dev.bandb.graphview.graph.Graph
4 | import dev.bandb.graphview.graph.Node
5 | import dev.bandb.graphview.layouts.layered.SugiyamaArrowEdgeDecoration
6 | import dev.bandb.graphview.layouts.layered.SugiyamaConfiguration
7 | import dev.bandb.graphview.layouts.layered.SugiyamaLayoutManager
8 | import dev.bandb.graphview.sample.GraphActivity
9 |
10 | class SugiyamaActivity : GraphActivity() {
11 |
12 | public override fun setLayoutManager() {
13 | recyclerView.layoutManager = SugiyamaLayoutManager(this, SugiyamaConfiguration.Builder().build())
14 | }
15 |
16 | public override fun setEdgeDecoration() {
17 | recyclerView.addItemDecoration(SugiyamaArrowEdgeDecoration())
18 | }
19 |
20 | public override fun createGraph(): Graph {
21 | val graph = Graph()
22 | val node1 = Node(nodeText)
23 | val node2 = Node(nodeText)
24 | val node3 = Node(nodeText)
25 | val node4 = Node(nodeText)
26 | val node5 = Node(nodeText)
27 | val node6 = Node(nodeText)
28 | val node8 = Node(nodeText)
29 | val node7 = Node(nodeText)
30 | val node9 = Node(nodeText)
31 | val node10 = Node(nodeText)
32 | val node11 = Node(nodeText)
33 | val node12 = Node(nodeText)
34 | val node13 = Node(nodeText)
35 | val node14 = Node(nodeText)
36 | val node15 = Node(nodeText)
37 | val node16 = Node(nodeText)
38 | val node17 = Node(nodeText)
39 | val node18 = Node(nodeText)
40 | val node19 = Node(nodeText)
41 | val node20 = Node(nodeText)
42 | val node21 = Node(nodeText)
43 | val node22 = Node(nodeText)
44 | val node23 = Node(nodeText)
45 | graph.addEdge(node1, node13)
46 | graph.addEdge(node1, node21)
47 | graph.addEdge(node1, node4)
48 | graph.addEdge(node1, node3)
49 | graph.addEdge(node2, node3)
50 | graph.addEdge(node2, node20)
51 | graph.addEdge(node3, node4)
52 | graph.addEdge(node3, node5)
53 | graph.addEdge(node3, node23)
54 | graph.addEdge(node4, node6)
55 | graph.addEdge(node5, node7)
56 | graph.addEdge(node6, node8)
57 | graph.addEdge(node6, node16)
58 | graph.addEdge(node6, node23)
59 | graph.addEdge(node7, node9)
60 | graph.addEdge(node8, node10)
61 | graph.addEdge(node8, node11)
62 | graph.addEdge(node9, node12)
63 | graph.addEdge(node10, node13)
64 | graph.addEdge(node10, node14)
65 | graph.addEdge(node10, node15)
66 | graph.addEdge(node11, node15)
67 | graph.addEdge(node11, node16)
68 | graph.addEdge(node12, node20)
69 | graph.addEdge(node13, node17)
70 | graph.addEdge(node14, node17)
71 | graph.addEdge(node14, node18)
72 | graph.addEdge(node16, node18)
73 | graph.addEdge(node16, node19)
74 | graph.addEdge(node16, node20)
75 | graph.addEdge(node18, node21)
76 | graph.addEdge(node19, node22)
77 | graph.addEdge(node21, node23)
78 | graph.addEdge(node22, node23)
79 | return graph
80 | }
81 | }
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_add_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
32 |
33 |
38 |
39 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/main_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/node.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/sample/src/main/res/menu/menu_buchheim_walker_orientations.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oss-bandb/GraphView/53db97a3cdf95df4ef46d2ffcd0547b178166830/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2196F3
4 | #1976D2
5 | #1565C0
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 | 16dp
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GraphView Sample
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':graphview', ':sample'
--------------------------------------------------------------------------------