├── .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 | ![alt Logo](image/GraphView_logo.jpg "Graph Logo") 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 | ![alt Example](image/Tree.png "Tree Example") 138 | 139 | #### Directed Graph 140 | ![alt Example](image/Graph.png "Graph Example") 141 | 142 | #### Layered Graph 143 | ![alt Example](image/LayeredGraph.png "Layered Graph Example") 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 | 3 | 6 | 9 | 12 | 15 | -------------------------------------------------------------------------------- /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 |