diff --git a/README.md b/README.md index 0c7005f..6139f98 100644 --- a/README.md +++ b/README.md @@ -3,137 +3,133 @@ GraphView Android GraphView is used to display data in graph structures. -![alt Logo](image/GraphView_logo.png "Graph Logo") +![alt Logo](image/GraphView_logo.jpg "Graph Logo") Overview ======== -The library is designed to support different graph layouts and currently works with small graphs only. +The library can be used within `RecyclerView` and currently works with small graphs only. **This project is currently experimental and the API subject to breaking changes without notice.** - Download ======== +The library is only available on MavenCentral. Please add this code to your build.gradle file on project level: +```gradle +allprojects { + repositories { + ... + mavenCentral() + } +} +``` -```groovy +And add the dependency to the build.gradle file within the app module: +```gradle dependencies { - implementation 'de.blox:graphview:0.7.1' + implementation 'dev.bandb.graphview:graphview:0.8.1' } ``` Layouts ====== ### Tree -Uses Walker's algorithm with Buchheim's runtime improvements (`BuchheimWalkerAlgorithm` class). 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 +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 `ORIENTATION_BOTTOM_TOP` (default). Furthermore parameters like sibling-, level-, subtree separation can be set. ### Directed graph -Directed graph drawing by simulating attraction/repulsion forces. For this the algorithm by Fruchterman and Reingold (`FruchtermanReingoldAlgorithm` class) was implemented. +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`. ### Layered graph -Algorithm from Sugiyama et al. for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph (`SugiyamaAlgorithm` class). You can also set the parameters for node and level separation using the `SugiyamaConfiguration.Builder`. +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`. Usage ====== -Using GraphView is not much different than using RecyclerView. -Add GraphView to your layout file. +GraphView must be integrated with `RecyclerView`. +For this you’ll need to add a `RecyclerView` to your layout and create an item layout like usually when working with `RecyclerView`. + ```xml - + + ``` 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. -Then define the node layout, e.g. ```node.xml```. You can make the node Layout as complex as you want. -```xml - -``` - -To create a graph, we need to instantiate the `Graph` class. Next bind your graph to GraphView, for that you must extend from the `GraphView.Adapter` class. - -```java -public class GraphActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - GraphView graphView = findViewById(R.id.graph); - - // example tree - final Graph graph = new Graph(); - final Node node1 = new Node("Parent"); - final Node node2 = new Node("Child 1"); - final Node node3 = new Node("Child 2"); - - graph.addEdge(node1, node2); - graph.addEdge(node1, node3); - - // you can set the graph via the constructor or use the adapter.setGraph(Graph) method - GraphAdapter adapter = new GraphAdapter(graph) { - - @NonNull - @Override - public GraphView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.node, parent, false); - return new SimpleViewHolder(view); - } - - @Override - public void onBindViewHolder(GraphView.ViewHolder viewHolder, Object data, int position) { - ((SimpleViewHolder) viewHolder).textView.setText(data.toString()); - } - }; - graphView.setAdapter(adapter); - - // set the algorithm here - final BuchheimWalkerConfiguration configuration = new BuchheimWalkerConfiguration.Builder() - .setSiblingSeparation(100) - .setLevelSeparation(300) - .setSubtreeSeparation(300) - .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) - .build(); - graphView.setLayout(new BuchheimWalkerAlgorithm(configuration)); +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. + +```kotlin +private void setupGraphView { + val recycler = findViewById(R.id.recycler) + + // 1. Set a layout manager of the ones described above that the RecyclerView will use. + val configuration = BuchheimWalkerConfiguration.Builder() + .setSiblingSeparation(100) + .setLevelSeparation(100) + .setSubtreeSeparation(100) + .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) + .build() + recycler.layoutManager = BuchheimWalkerLayoutManager(context, configuration) + + // 2. Attach item decorations to draw edges + recycler.addItemDecoration(TreeEdgeDecoration()) + + // 3. Build your graph + val graph = Graph() + val node1 = Node("Parent") + val node2 = Node("Child 1") + val node3 = Node("Child 2") + + graph.addEdge(node1, node2) + graph.addEdge(node1, node3) + + // 4. You will need a simple Adapter/ViewHolder. + // 4.1 Your Adapter class should extend from `AbstractGraphAdapter` + adapter = object : AbstractGraphAdapter() { + + // 4.2 ViewHolder should extend from `RecyclerView.ViewHolder` + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.node, parent, false) + return NodeViewHolder(view) + } + + override fun onBindViewHolder(holder: NodeViewHolder, position: Int) { + holder.textView.text = getNodeData(position).toString() + } + }.apply { + // 4.3 Submit the graph + this.submitGraph(graph) + recycler.adapter = this } } ``` -Your ViewHolder class should extend from `GraphView.ViewHolder`: -```java -class SimpleViewHolder extends GraphView.ViewHolder { - TextView textView; - - SimpleViewHolder(View itemView) { - super(itemView); - textView = itemView.findViewById(R.id.text); +Customization +====== +You can change the edge design by supplying your custom paint object to your edge decorator. +```kotlin + val edgeStyle = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) } -} + + recyclerView.addItemDecoration(TreeEdgeDecoration(edgeStyle)) ``` -Customization -============= - -To use the custom attributes you have to add the namespace first: ``` - xmlns:app="http://schemas.android.com/apk/res-auto"``` - -| Attribute | Format | Example | Explanation| -|------------------|-----------|--------------------------------|------------| -| lineThickness | Dimension | 10dp | Set how thick the connection lines should be -| lineColor | Color | "@android:color/holo_red_dark" | Set the color of the connection lines -| useMaxSize | Boolean | true | Use the same size for each node - -Each of the attributes has a corresponding setter in the GraphView class, if you want to use it programmatically. +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. +```kotlin + recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, configuration).apply { + useMaxSize = true + } +``` Examples ======== @@ -149,7 +145,7 @@ Examples License ======= - Copyright 2020 Team-Blox + Copyright 2019 - 2021 Block & Block Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -161,4 +157,4 @@ License distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/build.gradle b/build.gradle index dca8ccc..c127d9a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,21 +1,25 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.4.31' repositories { google() + mavenCentral() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0' + classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.10.2' } } allprojects { repositories { google() + mavenCentral() jcenter() } } diff --git a/gradle.properties b/gradle.properties index 39fdbb9..010ed56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,21 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +GROUP=dev.bandb.graphview +POM_ARTIFACT_ID=graphview +VERSION_NAME=0.8.1 +POM_NAME=graphview +POM_PACKAGING=aar +POM_DESCRIPTION=Android GraphView is used to display data in graph structures. +POM_INCEPTION_YEAR=2021 +POM_URL=https://github.com/oss-bandb/GraphView +POM_SCM_URL=https://github.com/oss-bandb/GraphView +POM_SCM_CONNECTION=scm:git@github.com:oss-bandb/GraphView.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:oss-bandb/GraphView.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=bandb +POM_DEVELOPER_NAME=Block & Block +POM_DEVELOPER_URL=https://github.com/oss-bandb \ No newline at end of file diff --git a/gradle/bintray.gradle b/gradle/bintray.gradle deleted file mode 100644 index 100df3f..0000000 --- a/gradle/bintray.gradle +++ /dev/null @@ -1,52 +0,0 @@ -apply plugin: 'com.jfrog.bintray' - -version = libraryVersion - -task sourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - classifier = 'sources' -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} -artifacts { - archives javadocJar - archives sourcesJar -} - -// Bintray -Properties properties = new Properties() -properties.load(project.rootProject.file('local.properties').newDataInputStream()) - -bintray { - user = properties.getProperty("bintray.user") - key = properties.getProperty("bintray.apikey") - - configurations = ['archives'] - pkg { - repo = bintrayRepo - name = bintrayName - desc = libraryDescription - - websiteUrl = siteUrl - vcsUrl = gitUrl - licenses = allLicenses - publish = true - publicDownloadNumbers = true - version { - desc = libraryDescription - gpg { - sign = true //Determines whether to GPG sign the files. The default is false - passphrase = properties.getProperty("bintray.gpg.password") - //Optional. The passphrase for GPG signing' - } - } - } -} \ No newline at end of file diff --git a/gradle/install.gradle b/gradle/install.gradle deleted file mode 100644 index 6cd4762..0000000 --- a/gradle/install.gradle +++ /dev/null @@ -1,41 +0,0 @@ -apply plugin: 'com.github.dcendents.android-maven' - -group = publishedGroupId // Maven Group ID for the artifact - -install { - repositories.mavenInstaller { - // This generates POM.xml with proper parameters - pom { - project { - packaging 'aar' - groupId publishedGroupId - artifactId artifact - - // Add your description here - name libraryName - description libraryDescription - url siteUrl - - // Set your license - licenses { - license { - name licenseName - url licenseUrl - } - } - developers { - developer { - id developerId - name developerName - email developerEmail - } - } - scm { - connection gitUrl - developerConnection gitUrl - url siteUrl - } - } - } - } -} \ No newline at end of file diff --git a/gradle/publish-mavencentral.gradle b/gradle/publish-mavencentral.gradle new file mode 100644 index 0000000..b04bcbb --- /dev/null +++ b/gradle/publish-mavencentral.gradle @@ -0,0 +1,52 @@ +apply plugin: 'maven-publish' + +configure(subprojects) { + apply() + + configure { + withJavadocJar() + withSourcesJar() + } + + configure { + publications { + val main by creating(MavenPublication::class) { + from(components["java"]) + + pom { + name.set("…") + description.set("…") + url.set("…") + licenses { + license { + name.set("…") + url.set("…") + } + } + developers { + developer { + id.set("…") + name.set("…") + email.set("…") + } + } + scm { + connection.set("…") + developerConnection.set("…") + url.set("…") + } + } + } + } + repositories { + maven { + name = "OSSRH" + setUrl("https://oss.sonatype.org/service/local/staging/deploy/maven2") + credentials { + username = System.getenv("OSSRH_USER") + password = System.getenv("OSSRH_PASSWORD") + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b30c706..e471218 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 01 14:28:22 CEST 2020 +#Sun Mar 14 15:44:09 CET 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/graphview/build.gradle b/graphview/build.gradle index 082b1fe..d9e4b98 100644 --- a/graphview/build.gradle +++ b/graphview/build.gradle @@ -1,12 +1,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply plugin: 'com.vanniktech.maven.publish' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 16 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -28,34 +29,8 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.annotation:annotation:1.1.0" + implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.annotation:annotation:1.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" -} - -ext { - bintrayRepo = 'maven' - bintrayName = 'de.blox.graphview' - - publishedGroupId = 'de.blox' - libraryName = 'GraphView' - artifact = 'graphview' - - libraryDescription = 'Android GraphView is used to display data in graph structures.' - - siteUrl = "https://github.com/Team-Blox/GraphView" - gitUrl = "https://github.com/Team-Blox/GraphView.git" - - libraryVersion = '0.7.1' - - developerId = 'Team-Blox' - developerName = 'Blox' - developerEmail = 'dennis.block@gmx.de' - - licenseName = 'The Apache Software License, Version 2.0' - licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - allLicenses = ["Apache-2.0"] -} - -apply from: rootProject.file('gradle/install.gradle') -apply from: rootProject.file('gradle/bintray.gradle') \ No newline at end of file + implementation "androidx.recyclerview:recyclerview:1.2.0" +} \ No newline at end of file diff --git a/graphview/src/main/AndroidManifest.xml b/graphview/src/main/AndroidManifest.xml index d92bb31..d6bc4a8 100644 --- a/graphview/src/main/AndroidManifest.xml +++ b/graphview/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + - diff --git a/graphview/src/main/java/de/blox/graphview/GraphAdapter.kt b/graphview/src/main/java/de/blox/graphview/GraphAdapter.kt deleted file mode 100644 index aac69e8..0000000 --- a/graphview/src/main/java/de/blox/graphview/GraphAdapter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package de.blox.graphview - -import android.database.DataSetObservable -import android.database.DataSetObserver -import android.view.View -import android.view.ViewGroup -import android.widget.Adapter -import androidx.annotation.NonNull - -abstract class GraphAdapter(var graph: Graph) : Adapter { - - private val dataSetObservable = DataSetObservable() - private var graphViewObserver: DataSetObserver? = null - - fun notifyDataSetChanged() { - synchronized(this) { - graphViewObserver?.onChanged() - } - dataSetObservable.notifyChanged() - } - - override fun registerDataSetObserver(observer: DataSetObserver) { - dataSetObservable.registerObserver(observer) - } - - override fun unregisterDataSetObserver(observer: DataSetObserver) { - dataSetObservable.unregisterObserver(observer) - } - - open fun setGraphViewObserver(observer: DataSetObserver?) { - synchronized(this) { graphViewObserver = observer } - } - - @NonNull - abstract fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): VH - - abstract fun onBindViewHolder(@NonNull viewHolder: VH, data: Any, position: Int) - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view: View - val viewHolder: VH - - if (convertView == null) { - viewHolder = onCreateViewHolder(parent, getItemViewType(position)) - view = viewHolder.itemView - view.tag = viewHolder - } else { - viewHolder = convertView.tag as VH - view = viewHolder.itemView - } - - onBindViewHolder(viewHolder, getItem(position), position) - - return view - } - - override fun getItemId(position: Int): Long = NO_ID - - override fun hasStableIds(): Boolean = false - - override fun getItemViewType(position: Int): Int = 0 - - override fun getViewTypeCount(): Int = 0 - - companion object { - const val NO_ID: Long = -1 - } -} diff --git a/graphview/src/main/java/de/blox/graphview/GraphObserver.kt b/graphview/src/main/java/de/blox/graphview/GraphObserver.kt deleted file mode 100644 index 6882b01..0000000 --- a/graphview/src/main/java/de/blox/graphview/GraphObserver.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.blox.graphview - -interface GraphObserver { - fun notifyGraphInvalidated() -} \ No newline at end of file diff --git a/graphview/src/main/java/de/blox/graphview/GraphView.kt b/graphview/src/main/java/de/blox/graphview/GraphView.kt deleted file mode 100644 index 876b807..0000000 --- a/graphview/src/main/java/de/blox/graphview/GraphView.kt +++ /dev/null @@ -1,355 +0,0 @@ -package de.blox.graphview - -import android.content.Context -import android.database.DataSetObserver -import android.graphics.* -import android.util.AttributeSet -import android.util.Log -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.View -import android.widget.AdapterView -import androidx.annotation.ColorInt -import androidx.annotation.Px -import kotlin.math.max -import kotlin.math.min - - -class GraphView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AdapterView>(context, attrs, defStyleAttr) { - private var linePaint: Paint - - init { - linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - strokeWidth = lineThickness.toFloat() - color = lineColor - style = Paint.Style.STROKE - strokeJoin = Paint.Join.ROUND // set the join to round you want - pathEffect = CornerPathEffect(10f) // set the path effect when they join. - } - - attrs?.let { initAttrs(context, it) } - } - - var lineThickness: Int = DEFAULT_LINE_THICKNESS - set(@Px value) { - linePaint.strokeWidth = value.toFloat() - field = value - invalidate() - } - - var lineColor: Int = DEFAULT_LINE_COLOR - @ColorInt get - set(@ColorInt value) { - linePaint.color = value - field = value - invalidate() - } - - var isUsingMaxSize: Boolean = DEFAULT_USE_MAX_SIZE - set(value) { - field = value - invalidate() - requestLayout() - } - - private var adapter: GraphAdapter? = null - - private var layout: Layout? = null - - private var maxChildWidth: Int = 0 - private var maxChildHeight: Int = 0 - private val rect: Rect by lazy { - Rect() - } - - private var observer: DataSetObserver? = null - private val gestureDetector: GestureDetector = GestureDetector(getContext(), GestureListener()) - - private fun initAttrs(context: Context, attrs: AttributeSet) { - val a = context.theme.obtainStyledAttributes(attrs, R.styleable.GraphView, 0, 0) - - lineThickness = a.getDimensionPixelSize(R.styleable.GraphView_lineThickness, DEFAULT_LINE_THICKNESS) - lineColor = a.getColor(R.styleable.GraphView_lineColor, DEFAULT_LINE_COLOR) - isUsingMaxSize = a.getBoolean(R.styleable.GraphView_useMaxSize, DEFAULT_USE_MAX_SIZE) - - a.recycle() - } - - private fun positionItems() { - var maxLeft = Integer.MAX_VALUE - var maxRight = Integer.MIN_VALUE - var maxTop = Integer.MAX_VALUE - var maxBottom = Integer.MIN_VALUE - - for (index in 0 until adapter!!.count) { - val child = adapter!!.getView(index, null, this) - addAndMeasureChild(child) - - val width = child.measuredWidth - val height = child.measuredHeight - val node = adapter!!.getItem(index) as Node - - val (x, y) = node.position - - // calculate the size and position of this child - val left = x.toInt() - val top = y.toInt() - val right = left + width - val bottom = top + height - - child.layout(left, top, right, bottom) - - maxRight = max(maxRight, right) - maxLeft = min(maxLeft, left) - maxBottom = max(maxBottom, bottom) - maxTop = min(maxTop, top) - } - } - - private fun addAndMeasureChild(child: View) { - var params: LayoutParams? = child.layoutParams - if (params == null) { - params = LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT - ) - } - - addViewInLayout(child, -1, params, false) - var widthSpec = makeMeasureSpec(params.width) - var heightSpec = makeMeasureSpec(params.height) - - if (isUsingMaxSize) { - widthSpec = MeasureSpec.makeMeasureSpec( - maxChildWidth, MeasureSpec.EXACTLY - ) - heightSpec = MeasureSpec.makeMeasureSpec( - maxChildHeight, MeasureSpec.EXACTLY - ) - } - - child.measure(widthSpec, heightSpec) - } - - - private fun getContainingChildIndex(x: Int, y: Int): Int { - for (index in 0 until childCount) { - getChildAt(index).getHitRect(rect) - if (rect.contains(x, y)) { - return index - } - } - return INVALID_INDEX - } - - private fun clickChildAt(x: Int, y: Int) { - val index = getContainingChildIndex(x, y) - // no child found at this position - if (index == INVALID_INDEX) { - return - } - - val itemView = getChildAt(index) - val id = adapter!!.getItemId(index) - performItemClick(itemView, index, id) - } - - private fun longClickChildAt(x: Int, y: Int) { - val index = getContainingChildIndex(x, y) - // no child found at this position - if (index == INVALID_INDEX) { - return - } - - val itemView = getChildAt(index) - val id = adapter!!.getItemId(index) - val listener = onItemLongClickListener - listener?.onItemLongClick(this, itemView, index, id) - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) - } - - override fun onLayout( - changed: Boolean, left: Int, top: Int, right: Int, - bottom: Int - ) { - super.onLayout(changed, left, top, right, bottom) - - if (adapter == null) { - Log.e("GraphView", "No adapter attached; skipping layout") - return - } - - removeAllViewsInLayout() //TODO: recycle views - positionItems() - invalidate() - } - - - fun setLayout(layout: Layout?) { - if (layout === this.layout) { - return - } - this.layout = layout - requestLayout() - } - - override fun getSelectedView(): View? { - return null - } - - override fun setSelection(position: Int) {} - - override fun dispatchDraw(canvas: Canvas) { - val adapter = getAdapter() - adapter?.run { - if (graph.hasNodes()) { - layout?.drawEdges(canvas, graph, linePaint) - } - } - super.dispatchDraw(canvas) - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val adapter = this.adapter ?: return - if (!adapter.graph.hasNodes()) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)) - return - } - - var maxWidth = 0 - var maxHeight = 0 - var minHeight = Integer.MAX_VALUE - - for (i in 0 until adapter.count) { - val child = adapter.getView(i, null, this) - - var params: MarginLayoutParams? = child.layoutParams as? MarginLayoutParams - if (params == null) { - params = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - } - addViewInLayout(child, -1, params, true) - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0) - val node = adapter.getItem(i) as Node - val measuredWidth = child.measuredWidth - val measuredHeight = child.measuredHeight - node.size.apply { - width = child.measuredWidth - height = child.measuredHeight - } - - maxWidth = max(maxWidth, measuredWidth) - maxHeight = max(maxHeight, measuredHeight) - minHeight = min(minHeight, measuredHeight) - } - - maxChildWidth = maxWidth - maxChildHeight = maxHeight - - if (isUsingMaxSize) { - removeAllViewsInLayout() - for (i in 0 until adapter.count) { - val child = adapter.getView(i, null, this) - - var params: LayoutParams? = child.layoutParams - if (params == null) { - params = LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT - ) - } - addViewInLayout(child, -1, params, true) - - val widthSpec = - MeasureSpec.makeMeasureSpec(maxChildWidth, MeasureSpec.EXACTLY) - val heightSpec = - MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY) - child.measure(widthSpec, heightSpec) - - val node = adapter.getItem(i) as Node - node.size.apply { - width = child.measuredWidth - height = child.measuredHeight - } - } - } - - adapter.notifyDataSetChanged() - layout?.run { - val size = run(adapter.graph, paddingLeft.toFloat(), paddingTop.toFloat()) - setMeasuredDimension(size.width + paddingRight + paddingLeft, size.height + paddingBottom + paddingTop) - } - } - - override fun setAdapter(adapter: GraphAdapter?) { - val oldAdapter = this.adapter - - oldAdapter?.setGraphViewObserver(null) - removeAllViewsInLayout() - - this.adapter = adapter - - this.adapter?.let { - if (observer == null) { - observer = GraphViewObserver() - } - it.setGraphViewObserver(observer) - requestLayout() - } - } - - override fun getAdapter(): GraphAdapter? = adapter - - private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent): Boolean { - return true - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - clickChildAt(e.x.toInt(), e.y.toInt()) - return true - } - - override fun onLongPress(e: MotionEvent) { - longClickChildAt(e.x.toInt(), e.y.toInt()) - } - } - - private inner class GraphViewObserver : DataSetObserver() { - override fun onChanged() { - refresh() - } - - override fun onInvalidated() { - refresh() - } - - private fun refresh() { - requestLayout() - invalidate() - } - } - - abstract class ViewHolder(val itemView: View) - - companion object { - const val DEFAULT_USE_MAX_SIZE = false - const val DEFAULT_LINE_THICKNESS = 5 - const val DEFAULT_LINE_COLOR = Color.BLACK - const val INVALID_INDEX = -1 - - private fun makeMeasureSpec(dimension: Int): Int { - return if (dimension > 0) { - MeasureSpec.makeMeasureSpec(dimension, MeasureSpec.EXACTLY) - } else { - MeasureSpec.UNSPECIFIED - } - } - } -} diff --git a/graphview/src/main/java/de/blox/graphview/Layout.kt b/graphview/src/main/java/de/blox/graphview/Layout.kt deleted file mode 100644 index c6d7368..0000000 --- a/graphview/src/main/java/de/blox/graphview/Layout.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.blox.graphview - -import android.graphics.Canvas -import android.graphics.Paint - -import de.blox.graphview.edgerenderer.EdgeRenderer -import de.blox.graphview.util.Size - -interface Layout { - /** - * Executes the algorithm. - * @param shiftY Shifts the y-coordinate origin - * @param shiftX Shifts the x-coordinate origin - * @return The size of the graph - */ - fun run(graph: Graph, shiftX: Float, shiftY: Float): Size - fun drawEdges(canvas: Canvas, graph: Graph, linePaint: Paint) - fun setEdgeRenderer(renderer: EdgeRenderer) -} diff --git a/graphview/src/main/java/de/blox/graphview/edgerenderer/EdgeRenderer.kt b/graphview/src/main/java/de/blox/graphview/edgerenderer/EdgeRenderer.kt deleted file mode 100644 index b5090cd..0000000 --- a/graphview/src/main/java/de/blox/graphview/edgerenderer/EdgeRenderer.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.blox.graphview.edgerenderer - -import android.graphics.Canvas -import android.graphics.Paint - -import de.blox.graphview.Graph - -interface EdgeRenderer { - fun render(canvas: Canvas, graph: Graph, paint: Paint) -} diff --git a/graphview/src/main/java/de/blox/graphview/edgerenderer/StraightEdgeRenderer.kt b/graphview/src/main/java/de/blox/graphview/edgerenderer/StraightEdgeRenderer.kt deleted file mode 100644 index c4f54a2..0000000 --- a/graphview/src/main/java/de/blox/graphview/edgerenderer/StraightEdgeRenderer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package de.blox.graphview.edgerenderer - -import android.graphics.Canvas -import android.graphics.Paint -import de.blox.graphview.Graph - -class StraightEdgeRenderer : EdgeRenderer { - override fun render(canvas: Canvas, graph: Graph, paint: Paint) { - graph.edges.forEach { (source, destination) -> - val (x1, y1) = source.position - val (x2, y2) = destination.position - - canvas.drawLine( - x1 + source.width / 2f, - y1 + source.height / 2f, - x2 + destination.width / 2f, - y2 + destination.height / 2f, paint - ) - } - } -} diff --git a/graphview/src/main/java/de/blox/graphview/tree/TreeEdgeRenderer.kt b/graphview/src/main/java/de/blox/graphview/tree/TreeEdgeRenderer.kt deleted file mode 100644 index 500bbd6..0000000 --- a/graphview/src/main/java/de/blox/graphview/tree/TreeEdgeRenderer.kt +++ /dev/null @@ -1,117 +0,0 @@ -package de.blox.graphview.tree - -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import de.blox.graphview.Graph -import de.blox.graphview.edgerenderer.EdgeRenderer -import de.blox.graphview.tree.BuchheimWalkerConfiguration.Companion.ORIENTATION_BOTTOM_TOP -import de.blox.graphview.tree.BuchheimWalkerConfiguration.Companion.ORIENTATION_LEFT_RIGHT -import de.blox.graphview.tree.BuchheimWalkerConfiguration.Companion.ORIENTATION_RIGHT_LEFT -import de.blox.graphview.tree.BuchheimWalkerConfiguration.Companion.ORIENTATION_TOP_BOTTOM - -class TreeEdgeRenderer(private val configuration: BuchheimWalkerConfiguration) : EdgeRenderer { - private val linePath = Path() - - override fun render(canvas: Canvas, graph: Graph, paint: Paint) { - val nodes = graph.nodes - - for (node in nodes) { - val children = graph.successorsOf(node) - - for (child in children) { - linePath.reset() - when (configuration.orientation) { - ORIENTATION_TOP_BOTTOM -> { - // position at the middle-top of the child - linePath.moveTo(child.x + child.width / 2f, child.y) - // draws a line from the child's middle-top halfway up to its parent - linePath.lineTo( - child.x + child.width / 2f, - child.y - configuration.levelSeparation / 2f - ) - // draws a line from the previous point to the middle of the parents width - linePath.lineTo( - node.x + node.width / 2f, - child.y - configuration.levelSeparation / 2f - ) - - - // position at the middle of the level separation under the parent - linePath.moveTo( - node.x + node.width / 2f, - child.y - configuration.levelSeparation / 2f - ) - // draws a line up to the parents middle-bottom - linePath.lineTo( - node.x + node.width / 2f, - node.y + node.height - ) - } - ORIENTATION_BOTTOM_TOP -> { - linePath.moveTo(child.x + child.width / 2f, child.y + child.height) - linePath.lineTo( - child.x + child.width / 2f, - child.y + child.height.toFloat() + configuration.levelSeparation / 2f - ) - linePath.lineTo( - node.x + node.width / 2f, - child.y + child.height.toFloat() + configuration.levelSeparation / 2f - ) - - linePath.moveTo( - node.x + node.width / 2f, - child.y + child.height.toFloat() + configuration.levelSeparation / 2f - ) - linePath.lineTo( - node.x + node.width / 2f, - node.y + node.height - ) - } - ORIENTATION_LEFT_RIGHT -> { - linePath.moveTo(child.x, child.y + child.height / 2f) - linePath.lineTo( - child.x - configuration.levelSeparation / 2f, - child.y + child.height / 2f - ) - linePath.lineTo( - child.x - configuration.levelSeparation / 2f, - node.y + node.height / 2f - ) - - linePath.moveTo( - child.x - configuration.levelSeparation / 2f, - node.y + node.height / 2f - ) - linePath.lineTo( - node.x + node.width, - node.y + node.height / 2f - ) - } - ORIENTATION_RIGHT_LEFT -> { - linePath.moveTo(child.x + child.width, child.y + child.height / 2f) - linePath.lineTo( - child.x + child.width.toFloat() + configuration.levelSeparation / 2f, - child.y + child.height / 2f - ) - linePath.lineTo( - child.x + child.width.toFloat() + configuration.levelSeparation / 2f, - node.y + node.height / 2f - ) - - linePath.moveTo( - child.x + child.width.toFloat() + configuration.levelSeparation / 2f, - node.y + node.height / 2f - ) - linePath.lineTo( - node.x + node.width, - node.y + node.height / 2f - ) - } - } - - canvas.drawPath(linePath, paint) - } - } - } -} diff --git a/graphview/src/main/java/dev/bandb/graphview/AbstractGraphAdapter.kt b/graphview/src/main/java/dev/bandb/graphview/AbstractGraphAdapter.kt new file mode 100644 index 0000000..d8f6896 --- /dev/null +++ b/graphview/src/main/java/dev/bandb/graphview/AbstractGraphAdapter.kt @@ -0,0 +1,26 @@ +package dev.bandb.graphview + +import androidx.annotation.Nullable +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node + +abstract class AbstractGraphAdapter : RecyclerView.Adapter() { + var graph: Graph? = null + override fun getItemCount(): Int = graph?.nodeCount ?: 0 + + open fun getNode(position: Int): Node? = graph?.getNodeAtPosition(position) + open fun getNodeData(position: Int): Any? = graph?.getNodeAtPosition(position)?.data + + /** + * Submits a new graph to be displayed. + * + * + * If a graph is already being displayed, you need to dispatch Adapter.notifyItem. + * + * @param graph The new graph to be displayed. + */ + open fun submitGraph(@Nullable graph: Graph?) { + this.graph = graph + } +} \ No newline at end of file diff --git a/graphview/src/main/java/de/blox/graphview/edgerenderer/ArrowEdgeRenderer.kt b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowDecoration.kt similarity index 70% rename from graphview/src/main/java/de/blox/graphview/edgerenderer/ArrowEdgeRenderer.kt rename to graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowDecoration.kt index 9e81bbf..087c41f 100644 --- a/graphview/src/main/java/de/blox/graphview/edgerenderer/ArrowEdgeRenderer.kt +++ b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowDecoration.kt @@ -1,19 +1,35 @@ -package de.blox.graphview.edgerenderer +package dev.bandb.graphview.decoration.edge -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import de.blox.graphview.Graph -import de.blox.graphview.Node +import android.graphics.* +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter +import dev.bandb.graphview.graph.Node + +open class ArrowDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) +}) : RecyclerView.ItemDecoration() { -open class ArrowEdgeRenderer : EdgeRenderer { private val trianglePath = Path() - override fun render(canvas: Canvas, graph: Graph, paint: Paint) { - val trianglePaint = Paint(paint) - trianglePaint.style = Paint.Style.FILL + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.layoutManager == null) { + return + } + val adapter = parent.adapter + if (adapter !is AbstractGraphAdapter) { + throw RuntimeException( + "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}") + } - graph.edges.forEach { (source, destination) -> + val graph = adapter.graph + val trianglePaint = Paint(this.linePaint).apply { + style = Paint.Style.FILL + } + graph?.edges?.forEach { (source, destination) -> val (x1, y1) = source.position val (x2, y2) = destination.position @@ -24,21 +40,16 @@ open class ArrowEdgeRenderer : EdgeRenderer { val clippedLine = clipLine(startX, startY, stopX, stopY, destination) - val triangleCentroid: FloatArray = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) - - canvas.drawLine(clippedLine[0], - clippedLine[1], - triangleCentroid[0], - triangleCentroid[1], paint) + drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) } } protected fun clipLine( - startX: Float, - startY: Float, - stopX: Float, - stopY: Float, - destination: Node + startX: Float, + startY: Float, + stopX: Float, + stopY: Float, + destination: Node ): FloatArray { val resultLine = FloatArray(4) resultLine[0] = startX @@ -110,6 +121,7 @@ open class ArrowEdgeRenderer : EdgeRenderer { } companion object { + //TODO: expose private const val ARROW_DEGREES = 0.5f private const val ARROW_LENGTH = 50f } diff --git a/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowEdgeDecoration.kt b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowEdgeDecoration.kt new file mode 100644 index 0000000..3947ec3 --- /dev/null +++ b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/ArrowEdgeDecoration.kt @@ -0,0 +1,52 @@ +package dev.bandb.graphview.decoration.edge + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.CornerPathEffect +import android.graphics.Paint +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter + +open class ArrowEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f // TODO: move default values res xml + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) +}) : ArrowDecoration(linePaint) { + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.layoutManager == null) { + return + } + val adapter = parent.adapter + if (adapter !is AbstractGraphAdapter) { + throw RuntimeException( + "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}") + } + + val graph = adapter.graph + val trianglePaint = Paint(linePaint).apply { + style = Paint.Style.FILL + } + graph?.edges?.forEach { (source, destination) -> + val (x1, y1) = source.position + val (x2, y2) = destination.position + + val startX = x1 + source.width / 2f + val startY = y1 + source.height / 2f + val stopX = x2 + destination.width / 2f + val stopY = y2 + destination.height / 2f + + val clippedLine = clipLine(startX, startY, stopX, stopY, destination) + + //TODO: modularization + val triangleCentroid: FloatArray = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) + + c.drawLine(clippedLine[0], + clippedLine[1], + triangleCentroid[0], + triangleCentroid[1], linePaint) + } + } +} diff --git a/graphview/src/main/java/dev/bandb/graphview/decoration/edge/StraightEdgeDecoration.kt b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/StraightEdgeDecoration.kt new file mode 100644 index 0000000..4525c63 --- /dev/null +++ b/graphview/src/main/java/dev/bandb/graphview/decoration/edge/StraightEdgeDecoration.kt @@ -0,0 +1,42 @@ +package dev.bandb.graphview.decoration.edge + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.CornerPathEffect +import android.graphics.Paint +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter + +open class StraightEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) +}) : RecyclerView.ItemDecoration() { + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.layoutManager == null) { + return + } + val adapter = parent.adapter + if (adapter !is AbstractGraphAdapter) { + throw RuntimeException( + "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}") + } + + val graph = adapter.graph + + graph?.edges?.forEach { (source, destination) -> + val (x1, y1) = source.position + val (x2, y2) = destination.position + + c.drawLine( + x1 + source.width / 2f, + y1 + source.height / 2f, + x2 + destination.width / 2f, + y2 + destination.height / 2f, linePaint + ) + } + super.onDraw(c, parent, state) + } +} diff --git a/graphview/src/main/java/de/blox/graphview/Edge.kt b/graphview/src/main/java/dev/bandb/graphview/graph/Edge.kt similarity index 63% rename from graphview/src/main/java/de/blox/graphview/Edge.kt rename to graphview/src/main/java/dev/bandb/graphview/graph/Edge.kt index 02cbab1..1ba5bbe 100644 --- a/graphview/src/main/java/de/blox/graphview/Edge.kt +++ b/graphview/src/main/java/dev/bandb/graphview/graph/Edge.kt @@ -1,3 +1,3 @@ -package de.blox.graphview +package dev.bandb.graphview.graph data class Edge(val source: Node, val destination: Node) diff --git a/graphview/src/main/java/de/blox/graphview/Graph.kt b/graphview/src/main/java/dev/bandb/graphview/graph/Graph.kt similarity index 91% rename from graphview/src/main/java/de/blox/graphview/Graph.kt rename to graphview/src/main/java/dev/bandb/graphview/graph/Graph.kt index f0d904d..84bf892 100644 --- a/graphview/src/main/java/de/blox/graphview/Graph.kt +++ b/graphview/src/main/java/dev/bandb/graphview/graph/Graph.kt @@ -1,9 +1,8 @@ -package de.blox.graphview +package dev.bandb.graphview.graph class Graph { private val _nodes = arrayListOf() private val _edges = arrayListOf() - private val graphObserver = arrayListOf() val nodes: List = _nodes val edges: List = _edges @@ -15,7 +14,6 @@ class Graph { fun addNode(node: Node) { if (node !in _nodes) { _nodes.add(node) - notifyGraphObserver() } } @@ -41,8 +39,6 @@ class Graph { iterator.remove() } } - - notifyGraphObserver() } fun removeNodes(vararg nodes: Node) = nodes.forEach { removeNode(it) } @@ -60,7 +56,6 @@ class Graph { if (edge !in _edges) { _edges.add(edge) - notifyGraphObserver() } } @@ -120,11 +115,7 @@ class Graph { fun getInEdges(node: Node): List = _edges.filter { it.destination == node } // Todo this is a quick fix and should be removed later - fun setAsTree(isTree: Boolean) { + internal fun setAsTree(isTree: Boolean) { this.isTree = isTree } - - private fun notifyGraphObserver() = graphObserver.forEach { - it.notifyGraphInvalidated() - } } diff --git a/graphview/src/main/java/de/blox/graphview/Node.kt b/graphview/src/main/java/dev/bandb/graphview/graph/Node.kt similarity index 88% rename from graphview/src/main/java/de/blox/graphview/Node.kt rename to graphview/src/main/java/dev/bandb/graphview/graph/Node.kt index b6e5edd..51404ae 100644 --- a/graphview/src/main/java/de/blox/graphview/Node.kt +++ b/graphview/src/main/java/dev/bandb/graphview/graph/Node.kt @@ -1,7 +1,7 @@ -package de.blox.graphview +package dev.bandb.graphview.graph -import de.blox.graphview.util.Size -import de.blox.graphview.util.VectorF +import dev.bandb.graphview.util.Size +import dev.bandb.graphview.util.VectorF data class Node(var data: Any) { // TODO make private diff --git a/graphview/src/main/java/dev/bandb/graphview/layouts/GraphLayoutManager.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/GraphLayoutManager.kt new file mode 100644 index 0000000..b13f343 --- /dev/null +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/GraphLayoutManager.kt @@ -0,0 +1,170 @@ +package dev.bandb.graphview.layouts + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.util.Size +import kotlin.math.max + +abstract class GraphLayoutManager internal constructor(context: Context) + : RecyclerView.LayoutManager() { + + var useMaxSize: Boolean = DEFAULT_USE_MAX_SIZE + set(value) { + field = value + requestLayout() + } + + private var adapter: AbstractGraphAdapter<*>? = null + + override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.WRAP_CONTENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) + + override fun onAdapterChanged(oldAdapter: RecyclerView.Adapter<*>?, + newAdapter: RecyclerView.Adapter<*>?) { + super.onAdapterChanged(oldAdapter, newAdapter) + + if (newAdapter !is AbstractGraphAdapter) { + throw RuntimeException( + "GraphLayoutManager only works with ${AbstractGraphAdapter::class.simpleName}") + } + + adapter = newAdapter + } + + override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { + detachAndScrapAttachedViews(recycler) + positionItems(recycler, state.itemCount) + } + + override fun canScrollHorizontally(): Boolean { + return false + } + + override fun canScrollVertically(): Boolean { + return false + } + + override fun onMeasure(recycler: RecyclerView.Recycler, state: RecyclerView.State, + widthSpec: Int, heightSpec: Int) { + val adapter = adapter + if (adapter == null) { + Log.e("GraphLayoutManager", "No adapter attached; skipping layout") + super.onMeasure(recycler, state, widthSpec, heightSpec) + return + } + + val graph = adapter.graph + if (graph == null || !graph.hasNodes()) { + Log.e("GraphLayoutManager", "No graph set; skipping layout") + super.onMeasure(recycler, state, widthSpec, heightSpec) + return + } + + var maxWidth = 0 + var maxHeight = 0 + + for (i in 0 until state.itemCount) { + val child = recycler.getViewForPosition(i) + + var params: ViewGroup.MarginLayoutParams? = child.layoutParams as? ViewGroup.MarginLayoutParams + if (params == null) { + params = ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + addView(child) + + val childWidthSpec = makeMeasureSpec(params.width) + val childHeightSpec = makeMeasureSpec(params.height) + child.measure(childWidthSpec, childHeightSpec) + + val measuredWidth = child.measuredWidth + val measuredHeight = child.measuredHeight + + val node = adapter.getNode(i) + node?.size?.apply { + width = measuredWidth + height = measuredHeight + } + + maxWidth = max(maxWidth, measuredWidth) + maxHeight = max(maxHeight, measuredHeight) + } + + if (useMaxSize) { + detachAndScrapAttachedViews(recycler) + for (i in 0 until state.itemCount) { + val child = recycler.getViewForPosition(i) + + addView(child) + + val childWidthSpec = makeMeasureSpec(maxWidth) + val childHeightSpec = makeMeasureSpec(maxHeight) + child.measure(childWidthSpec, childHeightSpec) + + val node = adapter.getNode(i) + node?.size?.apply { + width = child.measuredWidth + height = child.measuredHeight + } + } + } + + val size = run(graph, paddingLeft.toFloat(), paddingTop.toFloat()) + setMeasuredDimension(size.width + paddingRight + paddingLeft, size.height + paddingBottom + paddingTop) + } + + private fun positionItems(recycler: RecyclerView.Recycler, + itemCount: Int) { + for (index in 0 until itemCount) { + val child = recycler.getViewForPosition(index) + + adapter?.getNode(index)?.let { + val width = it.width + val height = it.height + val (x, y) = it.position + + addView(child) + val childWidthSpec = makeMeasureSpec(it.width) + val childHeightSpec = makeMeasureSpec(it.height) + child.measure(childWidthSpec, childHeightSpec) + + // calculate the size and position of this child + val left = x.toInt() + val top = y.toInt() + val right = left + width + val bottom = top + height + + child.layout(left, top, right, bottom) + } + } + } + + /** + * Executes the algorithm. + * @param shiftY Shifts the y-coordinate origin + * @param shiftX Shifts the x-coordinate origin + * @return The size of the graph + */ + abstract fun run(graph: Graph, shiftX: Float, shiftY: Float): Size + + companion object { + const val DEFAULT_USE_MAX_SIZE = false + + private fun makeMeasureSpec(dimension: Int): Int { + return if (dimension > 0) { + View.MeasureSpec.makeMeasureSpec(dimension, View.MeasureSpec.EXACTLY) + } else { + View.MeasureSpec.UNSPECIFIED + } + } + } + +} \ No newline at end of file diff --git a/graphview/src/main/java/de/blox/graphview/energy/FruchtermanReingoldAlgorithm.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/energy/FruchtermanReingoldLayoutManager.kt similarity index 79% rename from graphview/src/main/java/de/blox/graphview/energy/FruchtermanReingoldAlgorithm.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/energy/FruchtermanReingoldLayoutManager.kt index 4c8e5ec..3bbc7a7 100644 --- a/graphview/src/main/java/de/blox/graphview/energy/FruchtermanReingoldAlgorithm.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/energy/FruchtermanReingoldLayoutManager.kt @@ -1,28 +1,23 @@ -package de.blox.graphview.energy +package dev.bandb.graphview.layouts.energy -import android.graphics.Canvas -import android.graphics.Paint +import android.content.Context import android.graphics.RectF -import de.blox.graphview.Edge -import de.blox.graphview.Graph -import de.blox.graphview.Layout -import de.blox.graphview.Node -import de.blox.graphview.edgerenderer.ArrowEdgeRenderer -import de.blox.graphview.edgerenderer.EdgeRenderer -import de.blox.graphview.util.Size -import de.blox.graphview.util.VectorF +import dev.bandb.graphview.graph.Edge +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.GraphLayoutManager +import dev.bandb.graphview.util.Size +import dev.bandb.graphview.util.VectorF import java.util.* import kotlin.math.max import kotlin.math.min import kotlin.math.sqrt -class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterations: Int = DEFAULT_ITERATIONS) : - Layout { - private var edgeRenderer: EdgeRenderer = ArrowEdgeRenderer() +class FruchtermanReingoldLayoutManager @JvmOverloads constructor(private val context: Context, private val iterations: Int = DEFAULT_ITERATIONS) : GraphLayoutManager(context) { private val disps: MutableMap = HashMap() private val rand = Random(SEED) - private var width: Int = 0 - private var height: Int = 0 + private var w: Int = 0 // width + private var h: Int = 0 // height private var k: Float = 0.toFloat() private var t: Float = 0.toFloat() private var attraction_k: Float = 0.toFloat() @@ -32,10 +27,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati nodes.forEach { node -> // create meta data for each node disps[node] = VectorF() - node.setPosition( - randInt(rand, 0, width / 2).toFloat(), - randInt(rand, 0, height / 2).toFloat() - ) + node.setPosition(randInt(rand, 0, w / 2).toFloat(), randInt(rand, 0, h / 2).toFloat()) } } @@ -46,16 +38,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati private fun limitMaximumDisplacement(nodes: List) { nodes.forEach { val dispLength = max(EPSILON, getDisp(it).length().toDouble()).toFloat() - it.setPosition( - it.position.add( - getDisp(it).divide(dispLength).multiply( - min( - dispLength, - t - ) - ) - ) - ) + it.setPosition(it.position.add(getDisp(it).divide(dispLength).multiply(min(dispLength, t)))) } } @@ -63,14 +46,8 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati edges.forEach { (v, u) -> val delta = v.position.subtract(u.position) val deltaLength = max(EPSILON, delta.length().toDouble()).toFloat() - setDisp( - v, - getDisp(v).subtract(delta.divide(deltaLength).multiply(forceAttraction(deltaLength))) - ) - setDisp( - u, - getDisp(u).add(delta.divide(deltaLength).multiply(forceAttraction(deltaLength))) - ) + setDisp(v, getDisp(v).subtract(delta.divide(deltaLength).multiply(forceAttraction(deltaLength)))) + setDisp(u, getDisp(u).add(delta.divide(deltaLength).multiply(forceAttraction(deltaLength)))) } } @@ -80,10 +57,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati if (u != v) { val delta = v.position.subtract(u.position) val deltaLength = max(EPSILON, delta.length().toDouble()).toFloat() - setDisp( - v, - getDisp(v).add(delta.divide(deltaLength).multiply(forceRepulsion(deltaLength))) - ) + setDisp(v, getDisp(v).add(delta.divide(deltaLength).multiply(forceRepulsion(deltaLength)))) } } } @@ -107,14 +81,14 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati override fun run(graph: Graph, shiftX: Float, shiftY: Float): Size { val size = findBiggestSize(graph) * graph.nodeCount - width = size - height = size + w = size + h = size val nodes = graph.nodes val edges = graph.edges - t = (0.1 * sqrt((width / 2f * height / 2f).toDouble())).toFloat() - k = (0.75 * sqrt((width * height / nodes.size.toFloat()).toDouble())).toFloat() + t = (0.1 * sqrt((w / 2f * h / 2f).toDouble())).toFloat() + k = (0.75 * sqrt((w * h / nodes.size.toFloat()).toDouble())).toFloat() attraction_k = 0.75f * k repulsion_k = 0.75f * k @@ -244,7 +218,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati private fun findBiggestSize(graph: Graph): Int { return graph.nodes .map { max(it.height, it.width) } - .max() + .maxOrNull() ?: 0 } @@ -259,15 +233,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati } private fun done(): Boolean { - return t < 1.0 / max(height, width) - } - - override fun drawEdges(canvas: Canvas, graph: Graph, linePaint: Paint) { - edgeRenderer.render(canvas, graph, linePaint) - } - - override fun setEdgeRenderer(renderer: EdgeRenderer) { - this.edgeRenderer = renderer + return t < 1.0 / max(h, w) } private fun calculateGraphSize(graph: Graph): Size { @@ -335,6 +301,7 @@ class FruchtermanReingoldAlgorithm @JvmOverloads constructor(private val iterati } companion object { + //TODO: builder? const val DEFAULT_ITERATIONS = 1000 const val CLUSTER_PADDING = 100 private const val EPSILON = 0.0001 diff --git a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeRenderer.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaArrowEdgeDecoration.kt similarity index 50% rename from graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeRenderer.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaArrowEdgeDecoration.kt index ddaee31..9261731 100644 --- a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeRenderer.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaArrowEdgeDecoration.kt @@ -1,25 +1,42 @@ -package de.blox.graphview.layered +package dev.bandb.graphview.layouts.layered -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import de.blox.graphview.Edge -import de.blox.graphview.Graph -import de.blox.graphview.Node -import de.blox.graphview.edgerenderer.ArrowEdgeRenderer +import android.graphics.* +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter +import dev.bandb.graphview.decoration.edge.ArrowDecoration +//TODO throw UnsupportedOperationException("SugiyamaAlgorithm currently not support custom edge renderer!") +class SugiyamaArrowEdgeDecoration @JvmOverloads constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) +}) : ArrowDecoration(linePaint) { -class SugiyamaEdgeRenderer internal constructor( - private val nodeData: Map, - private val edgeData: Map -) : ArrowEdgeRenderer() { + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.layoutManager == null) { + return + } + val adapter = parent.adapter + if (adapter !is AbstractGraphAdapter) { + throw RuntimeException( + "SugiyamaArrowEdgeDecoration only works with ${AbstractGraphAdapter::class.simpleName}") + } + val layout = parent.layoutManager + if (layout !is SugiyamaLayoutManager) { + throw RuntimeException( + "SugiyamaArrowEdgeDecoration only works with ${SugiyamaLayoutManager::class.simpleName}") + } - override fun render(canvas: Canvas, graph: Graph, paint: Paint) { - val trianglePaint = Paint(paint) - trianglePaint.style = Paint.Style.FILL + val graph = adapter.graph + val edgeData = layout.edgeData + val nodeData = layout.nodeData val path = Path() + val trianglePaint = Paint(linePaint) + trianglePaint.style = Paint.Style.FILL - graph.edges.forEach { edge -> + graph?.edges?.forEach { edge -> val source = edge.source val (x, y) = source.position val destination = edge.destination @@ -48,7 +65,7 @@ class SugiyamaEdgeRenderer internal constructor( destination ) } - val triangleCentroid = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) + val triangleCentroid = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) path.reset() path.moveTo(bendPoints[0], bendPoints[1]) @@ -56,7 +73,7 @@ class SugiyamaEdgeRenderer internal constructor( path.lineTo(bendPoints[i - 1], bendPoints[i]) } path.lineTo(triangleCentroid[0], triangleCentroid[1]) - canvas.drawPath(path, paint) + c.drawPath(path, linePaint) } else { val startX = x + source.width / 2f val startY = y + source.height / 2f @@ -65,12 +82,12 @@ class SugiyamaEdgeRenderer internal constructor( clippedLine = clipLine(startX, startY, stopX, stopY, destination) - val triangleCentroid = drawTriangle(canvas, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) + val triangleCentroid = drawTriangle(c, trianglePaint, clippedLine[0], clippedLine[1], clippedLine[2], clippedLine[3]) - canvas.drawLine(clippedLine[0], + c.drawLine(clippedLine[0], clippedLine[1], triangleCentroid[0], - triangleCentroid[1], paint) + triangleCentroid[1], linePaint) } } } diff --git a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaConfiguration.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaConfiguration.kt similarity index 94% rename from graphview/src/main/java/de/blox/graphview/layered/SugiyamaConfiguration.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaConfiguration.kt index 68382fb..abdb921 100644 --- a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaConfiguration.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaConfiguration.kt @@ -1,4 +1,4 @@ -package de.blox.graphview.layered +package dev.bandb.graphview.layouts.layered class SugiyamaConfiguration private constructor(builder: Builder) { val levelSeparation: Int = builder.levelSeparation diff --git a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeData.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaEdgeData.kt similarity index 64% rename from graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeData.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaEdgeData.kt index cb12f5b..0d78cd0 100644 --- a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaEdgeData.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaEdgeData.kt @@ -1,4 +1,4 @@ -package de.blox.graphview.layered +package dev.bandb.graphview.layouts.layered internal class SugiyamaEdgeData { var bendPoints = mutableListOf() diff --git a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaAlgorithm.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaLayoutManager.kt similarity index 96% rename from graphview/src/main/java/de/blox/graphview/layered/SugiyamaAlgorithm.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaLayoutManager.kt index 0cc3555..9b7553b 100644 --- a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaAlgorithm.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaLayoutManager.kt @@ -1,25 +1,22 @@ -package de.blox.graphview.layered - -import android.graphics.Canvas -import android.graphics.Paint -import de.blox.graphview.Edge -import de.blox.graphview.Graph -import de.blox.graphview.Layout -import de.blox.graphview.Node -import de.blox.graphview.edgerenderer.EdgeRenderer -import de.blox.graphview.util.Size +package dev.bandb.graphview.layouts.layered + +import android.content.Context +import dev.bandb.graphview.graph.Edge +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.GraphLayoutManager +import dev.bandb.graphview.util.Size import java.util.* import kotlin.collections.ArrayList import kotlin.math.* -class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: SugiyamaConfiguration = SugiyamaConfiguration.Builder().build()) : Layout { - private val nodeData: MutableMap = HashMap() - private val edgeData: MutableMap = HashMap() +class SugiyamaLayoutManager @JvmOverloads constructor(private val context: Context, val configuration: SugiyamaConfiguration = SugiyamaConfiguration.Builder().build()) : GraphLayoutManager(context) { + internal val nodeData: MutableMap = HashMap() + internal val edgeData: MutableMap = HashMap() private val stack: MutableSet = HashSet() private val visited: MutableSet = HashSet() private var layers: MutableList> = mutableListOf() private lateinit var graph: Graph - private val edgeRenderer: EdgeRenderer = SugiyamaEdgeRenderer(nodeData, edgeData) private var nodeCount = 1 @@ -248,11 +245,11 @@ class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: Sug } } } - currentLayer.sortWith(Comparator { n1, n2 -> + currentLayer.sortWith { n1, n2 -> val nodeData1 = nodeData.getValue(n1) val nodeData2 = nodeData.getValue(n2) nodeData1.median - nodeData2.median - }) + } } } else { for (l in 1 until layers.size) { @@ -272,11 +269,11 @@ class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: Sug } } } - currentLayer.sortWith(Comparator { n1, n2 -> + currentLayer.sortWith { n1, n2 -> val nodeData1 = nodeData.getValue(n1) val nodeData2 = nodeData.getValue(n2) nodeData1.median - nodeData2.median - }) + } } } } @@ -471,7 +468,7 @@ class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: Sug // get the minimum coordinate value var minValue = (0..3) .flatMap { x[it].values } - .min() + .minOrNull() ?: java.lang.Float.MAX_VALUE // set left border to 0 @@ -495,7 +492,7 @@ class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: Sug } // get the minimum coordinate value - minValue = coordinates.values.min() + minValue = coordinates.values.minOrNull() ?: Integer.MAX_VALUE.toFloat() // set left border to 0 @@ -890,12 +887,4 @@ class SugiyamaAlgorithm @JvmOverloads constructor(private val configuration: Sug } } } - - override fun drawEdges(canvas: Canvas, graph: Graph, linePaint: Paint) { - edgeRenderer.render(canvas, this.graph, linePaint) - } - - override fun setEdgeRenderer(renderer: EdgeRenderer) { - throw UnsupportedOperationException("SugiyamaAlgorithm currently not support custom edge renderer!") - } } diff --git a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaNodeData.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaNodeData.kt similarity index 85% rename from graphview/src/main/java/de/blox/graphview/layered/SugiyamaNodeData.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaNodeData.kt index aad516e..debac70 100644 --- a/graphview/src/main/java/de/blox/graphview/layered/SugiyamaNodeData.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/layered/SugiyamaNodeData.kt @@ -1,6 +1,6 @@ -package de.blox.graphview.layered +package dev.bandb.graphview.layouts.layered -import de.blox.graphview.Node +import dev.bandb.graphview.graph.Node internal class SugiyamaNodeData { val reversed = mutableSetOf() diff --git a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerConfiguration.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerConfiguration.kt similarity index 97% rename from graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerConfiguration.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerConfiguration.kt index 84521c6..e8e60ae 100644 --- a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerConfiguration.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerConfiguration.kt @@ -1,4 +1,4 @@ -package de.blox.graphview.tree +package dev.bandb.graphview.layouts.tree class BuchheimWalkerConfiguration private constructor(builder: Builder) { val siblingSeparation: Int diff --git a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerAlgorithm.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerLayoutManager.kt similarity index 95% rename from graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerAlgorithm.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerLayoutManager.kt index 3dac420..9db5242 100644 --- a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerAlgorithm.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerLayoutManager.kt @@ -1,21 +1,17 @@ -package de.blox.graphview.tree - -import android.graphics.Canvas -import android.graphics.Paint -import de.blox.graphview.Graph -import de.blox.graphview.Layout -import de.blox.graphview.Node -import de.blox.graphview.edgerenderer.EdgeRenderer -import de.blox.graphview.util.Size -import de.blox.graphview.util.VectorF +package dev.bandb.graphview.layouts.tree + +import android.content.Context +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.GraphLayoutManager +import dev.bandb.graphview.util.Size +import dev.bandb.graphview.util.VectorF import java.util.* import kotlin.math.max import kotlin.math.min -class BuchheimWalkerAlgorithm @JvmOverloads constructor(private val configuration: BuchheimWalkerConfiguration = BuchheimWalkerConfiguration.Builder().build()) : - Layout { +class BuchheimWalkerLayoutManager @JvmOverloads constructor(private val context: Context, val configuration: BuchheimWalkerConfiguration = BuchheimWalkerConfiguration.Builder().build()) : GraphLayoutManager(context) { private val mNodeData: MutableMap = HashMap() - private var edgeRenderer: EdgeRenderer = TreeEdgeRenderer(configuration) private var minNodeHeight = Integer.MAX_VALUE private var minNodeWidth = Integer.MAX_VALUE private var maxNodeWidth = Integer.MIN_VALUE @@ -328,6 +324,7 @@ class BuchheimWalkerAlgorithm @JvmOverloads constructor(private val configuratio override fun run(graph: Graph, shiftX: Float, shiftY: Float): Size { // TODO check for cycles and multiple parents mNodeData.clear() + graph.setAsTree(true) val firstNode = graph.getNodeAtPosition(0) firstWalk(graph, firstNode, 0, 0) @@ -491,12 +488,4 @@ class BuchheimWalkerAlgorithm @JvmOverloads constructor(private val configuratio return nodeList } - - override fun drawEdges(canvas: Canvas, graph: Graph, linePaint: Paint) { - edgeRenderer.render(canvas, graph, linePaint) - } - - override fun setEdgeRenderer(renderer: EdgeRenderer) { - this.edgeRenderer = renderer - } } diff --git a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerNodeData.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerNodeData.kt similarity index 79% rename from graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerNodeData.kt rename to graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerNodeData.kt index 2dcbe13..c2dea73 100644 --- a/graphview/src/main/java/de/blox/graphview/tree/BuchheimWalkerNodeData.kt +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/BuchheimWalkerNodeData.kt @@ -1,6 +1,6 @@ -package de.blox.graphview.tree +package dev.bandb.graphview.layouts.tree -import de.blox.graphview.Node +import dev.bandb.graphview.graph.Node internal class BuchheimWalkerNodeData { lateinit var ancestor: Node diff --git a/graphview/src/main/java/dev/bandb/graphview/layouts/tree/TreeEdgeDecoration.kt b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/TreeEdgeDecoration.kt new file mode 100644 index 0000000..2267135 --- /dev/null +++ b/graphview/src/main/java/dev/bandb/graphview/layouts/tree/TreeEdgeDecoration.kt @@ -0,0 +1,137 @@ +package dev.bandb.graphview.layouts.tree + +import android.graphics.* +import androidx.recyclerview.widget.RecyclerView +import dev.bandb.graphview.AbstractGraphAdapter + + +open class TreeEdgeDecoration constructor(private val linePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + strokeWidth = 5f + color = Color.BLACK + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + pathEffect = CornerPathEffect(10f) +}) : RecyclerView.ItemDecoration() { + + private val linePath = Path() + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val adapter = parent.adapter + if (parent.layoutManager == null || adapter == null) { + return + } + if (adapter !is AbstractGraphAdapter) { + throw RuntimeException( + "TreeEdgeDecoration only works with ${AbstractGraphAdapter::class.simpleName}") + } + val layout = parent.layoutManager + if (layout !is BuchheimWalkerLayoutManager) { + throw RuntimeException( + "TreeEdgeDecoration only works with ${BuchheimWalkerLayoutManager::class.simpleName}") + } + + val configuration = layout.configuration + + val graph = adapter.graph + if (graph != null && graph.hasNodes()) { + val nodes = graph.nodes + + for (node in nodes) { + val children = graph.successorsOf(node) + + for (child in children) { + linePath.reset() + when (configuration.orientation) { + BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM -> { + // position at the middle-top of the child + linePath.moveTo(child.x + child.width / 2f, child.y) + // draws a line from the child's middle-top halfway up to its parent + linePath.lineTo( + child.x + child.width / 2f, + child.y - configuration.levelSeparation / 2f + ) + // draws a line from the previous point to the middle of the parents width + linePath.lineTo( + node.x + node.width / 2f, + child.y - configuration.levelSeparation / 2f + ) + + // position at the middle of the level separation under the parent + linePath.moveTo( + node.x + node.width / 2f, + child.y - configuration.levelSeparation / 2f + ) + // draws a line up to the parents middle-bottom + linePath.lineTo( + node.x + node.width / 2f, + node.y + node.height + ) + } + BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP -> { + linePath.moveTo(child.x + child.width / 2f, child.y + child.height) + linePath.lineTo( + child.x + child.width / 2f, + child.y + child.height.toFloat() + configuration.levelSeparation / 2f + ) + linePath.lineTo( + node.x + node.width / 2f, + child.y + child.height.toFloat() + configuration.levelSeparation / 2f + ) + + linePath.moveTo( + node.x + node.width / 2f, + child.y + child.height.toFloat() + configuration.levelSeparation / 2f + ) + linePath.lineTo( + node.x + node.width / 2f, + node.y + node.height + ) + } + BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT -> { + linePath.moveTo(child.x, child.y + child.height / 2f) + linePath.lineTo( + child.x - configuration.levelSeparation / 2f, + child.y + child.height / 2f + ) + linePath.lineTo( + child.x - configuration.levelSeparation / 2f, + node.y + node.height / 2f + ) + + linePath.moveTo( + child.x - configuration.levelSeparation / 2f, + node.y + node.height / 2f + ) + linePath.lineTo( + node.x + node.width, + node.y + node.height / 2f + ) + } + BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT -> { + linePath.moveTo(child.x + child.width, child.y + child.height / 2f) + linePath.lineTo( + child.x + child.width.toFloat() + configuration.levelSeparation / 2f, + child.y + child.height / 2f + ) + linePath.lineTo( + child.x + child.width.toFloat() + configuration.levelSeparation / 2f, + node.y + node.height / 2f + ) + + linePath.moveTo( + child.x + child.width.toFloat() + configuration.levelSeparation / 2f, + node.y + node.height / 2f + ) + linePath.lineTo( + node.x + node.width, + node.y + node.height / 2f + ) + } + } + + c.drawPath(linePath, linePaint) + } + } + } + super.onDraw(c, parent, state) + } +} diff --git a/graphview/src/main/java/de/blox/graphview/util/Size.kt b/graphview/src/main/java/dev/bandb/graphview/util/Size.kt similarity index 63% rename from graphview/src/main/java/de/blox/graphview/util/Size.kt rename to graphview/src/main/java/dev/bandb/graphview/util/Size.kt index 60f1c2c..3fe1c36 100644 --- a/graphview/src/main/java/de/blox/graphview/util/Size.kt +++ b/graphview/src/main/java/dev/bandb/graphview/util/Size.kt @@ -1,3 +1,3 @@ -package de.blox.graphview.util +package dev.bandb.graphview.util data class Size(var width: Int = 0, var height: Int = 0) diff --git a/graphview/src/main/java/de/blox/graphview/util/VectorF.kt b/graphview/src/main/java/dev/bandb/graphview/util/VectorF.kt similarity index 96% rename from graphview/src/main/java/de/blox/graphview/util/VectorF.kt rename to graphview/src/main/java/dev/bandb/graphview/util/VectorF.kt index d45b28f..5f76d3c 100644 --- a/graphview/src/main/java/de/blox/graphview/util/VectorF.kt +++ b/graphview/src/main/java/dev/bandb/graphview/util/VectorF.kt @@ -1,4 +1,4 @@ -package de.blox.graphview.util +package dev.bandb.graphview.util import kotlin.math.sqrt diff --git a/graphview/src/main/res/values/attrs.xml b/graphview/src/main/res/values/attrs.xml deleted file mode 100644 index 86483da..0000000 --- a/graphview/src/main/res/values/attrs.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/image/GraphView_logo.jpg b/image/GraphView_logo.jpg new file mode 100644 index 0000000..4f6d4d3 Binary files /dev/null and b/image/GraphView_logo.jpg differ diff --git a/image/GraphView_logo.png b/image/GraphView_logo.png deleted file mode 100644 index 488b0f7..0000000 Binary files a/image/GraphView_logo.png and /dev/null differ diff --git a/sample/build.gradle b/sample/build.gradle index 498e1f3..a48898e 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,13 +1,14 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 30 + buildToolsVersion "30.0.5" defaultConfig { - applicationId "de.blox.graphview.sample" + applicationId "dev.bandb.graphview.sample" minSdkVersion 18 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -27,13 +28,13 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation "androidx.recyclerview:recyclerview:1.2.0" + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.0-beta01' implementation 'com.otaliastudios:zoomlayout:1.8.0' - implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.3.0' implementation project(path: ':graphview') } \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index ce29362..b897def 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="dev.bandb.graphview.sample"> - + - + - + - - - - - - + + + \ No newline at end of file diff --git a/sample/src/main/java/de/blox/graphview/sample/Algorithms/BuchheimWalkerActivity.java b/sample/src/main/java/de/blox/graphview/sample/Algorithms/BuchheimWalkerActivity.java deleted file mode 100644 index a44ed2e..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/Algorithms/BuchheimWalkerActivity.java +++ /dev/null @@ -1,92 +0,0 @@ -package de.blox.graphview.sample.Algorithms; - -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import de.blox.graphview.Graph; -import de.blox.graphview.GraphView; -import de.blox.graphview.Node; -import de.blox.graphview.sample.GraphActivity; -import de.blox.graphview.sample.R; -import de.blox.graphview.tree.BuchheimWalkerAlgorithm; -import de.blox.graphview.tree.BuchheimWalkerConfiguration; - -public class BuchheimWalkerActivity extends GraphActivity { - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_buchheim_walker_orientations, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final BuchheimWalkerConfiguration.Builder builder = new BuchheimWalkerConfiguration.Builder() - .setSiblingSeparation(100) - .setLevelSeparation(300) - .setSubtreeSeparation(300); - - switch (item.getItemId()) { - case R.id.topToBottom: - builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); - break; - case R.id.bottomToTop: - builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP); - break; - case R.id.leftToRight: - builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT); - break; - case R.id.rightToLeft: - builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT); - break; - default: - return super.onOptionsItemSelected(item); - } - graphView.setLayout(new BuchheimWalkerAlgorithm(builder.build())); - return true; - } - - @Override - public Graph createGraph() { - final Graph graph = new Graph(); - final Node node1 = new Node(getNodeText()); - final Node node2 = new Node(getNodeText()); - final Node node3 = new Node(getNodeText()); - final Node node4 = new Node(getNodeText()); - final Node node5 = new Node(getNodeText()); - final Node node6 = new Node(getNodeText()); - final Node node8 = new Node(getNodeText()); - final Node node7 = new Node(getNodeText()); - final Node node9 = new Node(getNodeText()); - final Node node10 = new Node(getNodeText()); - final Node node11 = new Node(getNodeText()); - final Node node12 = new Node(getNodeText()); - - graph.addEdge(node1, node2); - graph.addEdge(node1, node3); - graph.addEdge(node1, node4); - graph.addEdge(node2, node5); - graph.addEdge(node2, node6); - graph.addEdge(node6, node7); - graph.addEdge(node6, node8); - graph.addEdge(node4, node9); - graph.addEdge(node4, node10); - graph.addEdge(node4, node11); - graph.addEdge(node11, node12); - - return graph; - } - - @Override - public void setLayout(GraphView view) { - final BuchheimWalkerConfiguration configuration = new BuchheimWalkerConfiguration.Builder() - .setSiblingSeparation(100) - .setLevelSeparation(300) - .setSubtreeSeparation(300) - .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) - .build(); - view.setLayout(new BuchheimWalkerAlgorithm(configuration)); - } -} diff --git a/sample/src/main/java/de/blox/graphview/sample/Algorithms/FruchtermanReingoldActivity.java b/sample/src/main/java/de/blox/graphview/sample/Algorithms/FruchtermanReingoldActivity.java deleted file mode 100644 index 6f7b7f2..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/Algorithms/FruchtermanReingoldActivity.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.blox.graphview.sample.Algorithms; - -import de.blox.graphview.Graph; -import de.blox.graphview.GraphView; -import de.blox.graphview.Node; -import de.blox.graphview.energy.FruchtermanReingoldAlgorithm; -import de.blox.graphview.sample.GraphActivity; - -public class FruchtermanReingoldActivity extends GraphActivity { - - @Override - public Graph createGraph() { - final Graph graph = new Graph(); - final Node a = new Node(getNodeText()); - final Node b = new Node(getNodeText()); - final Node c = new Node(getNodeText()); - final Node d = new Node(getNodeText()); - final Node e = new Node(getNodeText()); - final Node f = new Node(getNodeText()); - final Node g = new Node(getNodeText()); - final Node h = new Node(getNodeText()); - - graph.addEdge(a, b); - graph.addEdge(a, c); - graph.addEdge(a, d); - graph.addEdge(c, e); - graph.addEdge(d, f); - graph.addEdge(f, c); - graph.addEdge(g, c); - graph.addEdge(h, g); - - return graph; - } - - @Override - public void setLayout(GraphView view) { - view.setLayout(new FruchtermanReingoldAlgorithm(1000)); - } -} diff --git a/sample/src/main/java/de/blox/graphview/sample/Algorithms/SugiyamaActivity.java b/sample/src/main/java/de/blox/graphview/sample/Algorithms/SugiyamaActivity.java deleted file mode 100644 index da32108..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/Algorithms/SugiyamaActivity.java +++ /dev/null @@ -1,81 +0,0 @@ -package de.blox.graphview.sample.Algorithms; - -import de.blox.graphview.Graph; -import de.blox.graphview.GraphView; -import de.blox.graphview.Node; -import de.blox.graphview.layered.SugiyamaAlgorithm; -import de.blox.graphview.sample.GraphActivity; - -public class SugiyamaActivity extends GraphActivity { - - @Override - public Graph createGraph() { - final Graph graph = new Graph(); - - final Node node1 = new Node(getNodeText()); - final Node node2 = new Node(getNodeText()); - final Node node3 = new Node(getNodeText()); - final Node node4 = new Node(getNodeText()); - final Node node5 = new Node(getNodeText()); - final Node node6 = new Node(getNodeText()); - final Node node8 = new Node(getNodeText()); - final Node node7 = new Node(getNodeText()); - final Node node9 = new Node(getNodeText()); - final Node node10 = new Node(getNodeText()); - final Node node11 = new Node(getNodeText()); - final Node node12 = new Node(getNodeText()); - final Node node13 = new Node(getNodeText()); - final Node node14 = new Node(getNodeText()); - final Node node15 = new Node(getNodeText()); - final Node node16 = new Node(getNodeText()); - final Node node17 = new Node(getNodeText()); - final Node node18 = new Node(getNodeText()); - final Node node19 = new Node(getNodeText()); - final Node node20 = new Node(getNodeText()); - final Node node21 = new Node(getNodeText()); - final Node node22 = new Node(getNodeText()); - final Node node23 = new Node(getNodeText()); - - graph.addEdge(node1, node13); - graph.addEdge(node1, node21); - graph.addEdge(node1, node4); - graph.addEdge(node1, node3); - graph.addEdge(node2, node3); - graph.addEdge(node2, node20); - graph.addEdge(node3, node4); - graph.addEdge(node3, node5); - graph.addEdge(node3, node23); - graph.addEdge(node4, node6); - graph.addEdge(node5, node7); - graph.addEdge(node6, node8); - graph.addEdge(node6, node16); - graph.addEdge(node6, node23); - graph.addEdge(node7, node9); - graph.addEdge(node8, node10); - graph.addEdge(node8, node11); - graph.addEdge(node9, node12); - graph.addEdge(node10, node13); - graph.addEdge(node10, node14); - graph.addEdge(node10, node15); - graph.addEdge(node11, node15); - graph.addEdge(node11, node16); - graph.addEdge(node12, node20); - graph.addEdge(node13, node17); - graph.addEdge(node14, node17); - graph.addEdge(node14, node18); - graph.addEdge(node16, node18); - graph.addEdge(node16, node19); - graph.addEdge(node16, node20); - graph.addEdge(node18, node21); - graph.addEdge(node19, node22); - graph.addEdge(node21, node23); - graph.addEdge(node22, node23); - - return graph; - } - - @Override - public void setLayout(GraphView view) { - view.setLayout(new SugiyamaAlgorithm()); - } -} diff --git a/sample/src/main/java/de/blox/graphview/sample/GraphActivity.java b/sample/src/main/java/de/blox/graphview/sample/GraphActivity.java deleted file mode 100644 index df8059c..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/GraphActivity.java +++ /dev/null @@ -1,130 +0,0 @@ -package de.blox.graphview.sample; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import de.blox.graphview.Graph; -import de.blox.graphview.GraphAdapter; -import de.blox.graphview.GraphView; -import de.blox.graphview.Node; - -public abstract class GraphActivity extends AppCompatActivity { - private int nodeCount = 1; - private Node currentNode; - protected GraphView graphView; - protected GraphAdapter adapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_graph); - - final Graph graph = createGraph(); - setupToolbar(); - setupFab(graph); - setupAdapter(graph); - } - - private void setupAdapter(Graph graph) { - graphView = findViewById(R.id.graph); - setLayout(graphView); - adapter = new GraphAdapter(graph) { - - @Override - public int getCount() { - return graph.getNodeCount(); - } - - @Override - public Object getItem(int position) { - return graph.getNodeAtPosition(position); - } - - @Override - public boolean isEmpty() { - return graph.hasNodes(); - } - - @NonNull - @Override - public GraphView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.node, parent, false); - return new SimpleViewHolder(view); - } - - @Override - public void onBindViewHolder(GraphView.ViewHolder viewHolder, Object data, int position) { - ((SimpleViewHolder) viewHolder).textView.setText(data.toString()); - } - - class SimpleViewHolder extends GraphView.ViewHolder { - TextView textView; - - SimpleViewHolder(View itemView) { - super(itemView); - textView = itemView.findViewById(R.id.textView); - } - } - }; - graphView.setAdapter(adapter); - graphView.setOnItemClickListener((parent, view, position, id) -> { - currentNode = (Node) adapter.getItem(position); - Snackbar.make(graphView, "Clicked on " + currentNode.getData().toString(), Snackbar.LENGTH_SHORT).show(); - }); - } - - private void setupFab(final Graph graph) { - FloatingActionButton addButton = findViewById(R.id.addNode); - addButton.setOnClickListener(v -> { - final Node newNode = new Node(getNodeText()); - - if (currentNode != null) { - graph.addEdge(currentNode, newNode); - } else { - graph.addNode(newNode); - } - adapter.notifyDataSetChanged(); - }); - - addButton.setOnLongClickListener(v -> { - if (currentNode != null) { - graph.removeNode(currentNode); - currentNode = null; - } - return true; - }); - } - - private void setupToolbar() { - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back); - ab.setDisplayHomeAsUpEnabled(true); - } - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - public abstract Graph createGraph(); - - public abstract void setLayout(GraphView view); - protected String getNodeText() { - return "Node " + nodeCount++; - } -} diff --git a/sample/src/main/java/de/blox/graphview/sample/MainActivity.java b/sample/src/main/java/de/blox/graphview/sample/MainActivity.java deleted file mode 100644 index 1a7b1f7..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/MainActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -package de.blox.graphview.sample; - -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -public class MainActivity extends AppCompatActivity { - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - setupToolbar(); - setupRecyclerView(); - } - - private void setupRecyclerView() { - RecyclerView graphs = findViewById(R.id.graphs); - graphs.setLayoutManager(new LinearLayoutManager(this)); - graphs.setAdapter(new GraphListAdapter()); - DividerItemDecoration decoration = new DividerItemDecoration(getApplicationContext(), DividerItemDecoration.VERTICAL); - graphs.addItemDecoration(decoration); - } - - private void setupToolbar() { - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayHomeAsUpEnabled(false); - } - } - - private class GraphListAdapter extends RecyclerView.Adapter { - - @NonNull - @Override - public GraphViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.main_item, parent, false); - return new GraphViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull GraphViewHolder holder, final int position) { - final MainContent.GraphItem graphItem = MainContent.ITEMS.get(position); - holder.title.setText(graphItem.title); - holder.description.setText(graphItem.description); - - holder.itemView.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, graphItem.clazz))); - } - - @Override - public int getItemCount() { - return MainContent.ITEMS.size(); - } - } - - private class GraphViewHolder extends RecyclerView.ViewHolder { - private TextView title; - private TextView description; - - public GraphViewHolder(View itemView) { - super(itemView); - title = itemView.findViewById(R.id.title); - description = itemView.findViewById(R.id.description); - } - } -} diff --git a/sample/src/main/java/de/blox/graphview/sample/MainContent.java b/sample/src/main/java/de/blox/graphview/sample/MainContent.java deleted file mode 100644 index 62b649f..0000000 --- a/sample/src/main/java/de/blox/graphview/sample/MainContent.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.blox.graphview.sample; - -import java.util.ArrayList; -import java.util.List; - -import de.blox.graphview.sample.Algorithms.BuchheimWalkerActivity; -import de.blox.graphview.sample.Algorithms.FruchtermanReingoldActivity; -import de.blox.graphview.sample.Algorithms.SugiyamaActivity; - -public class MainContent { - - - public static final List ITEMS = new ArrayList<>(); - - static { - ITEMS.add(new GraphItem("BuchheimWalker", "Algorithm for drawing tree structures", BuchheimWalkerActivity.class)); - ITEMS.add(new GraphItem("FruchtermanReingold", "Directed graph drawing by simulating attraction/repulsion forces", FruchtermanReingoldActivity.class)); - ITEMS.add(new GraphItem("Sugiyama et al.", "Algorithm for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph.", SugiyamaActivity.class)); - } - - public static class GraphItem { - public final String title; - public final String description; - public final Class clazz; - - public GraphItem(String title, String description, Class clazz) { - this.clazz = clazz; - this.title = title; - this.description = description; - } - - @Override - public String toString() { - return title; - } - } -} diff --git a/sample/src/main/java/dev/bandb/graphview/sample/GraphActivity.kt b/sample/src/main/java/dev/bandb/graphview/sample/GraphActivity.kt new file mode 100644 index 0000000..301ab7a --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/GraphActivity.kt @@ -0,0 +1,114 @@ +package dev.bandb.graphview.sample + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar +import dev.bandb.graphview.AbstractGraphAdapter +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import java.util.* + +abstract class GraphActivity : AppCompatActivity() { + protected lateinit var recyclerView: RecyclerView + protected lateinit var adapter: AbstractGraphAdapter + private lateinit var fab: FloatingActionButton + private var currentNode: Node? = null + private var nodeCount = 1 + + protected abstract fun createGraph(): Graph + protected abstract fun setLayoutManager() + protected abstract fun setEdgeDecoration() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_graph) + + val graph = createGraph() + recyclerView = findViewById(R.id.recycler) + setLayoutManager() + setEdgeDecoration() + setupGraphView(graph) + + setupFab(graph) + setupToolbar() + } + + private fun setupGraphView(graph: Graph) { + adapter = object : AbstractGraphAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.node, parent, false) + return NodeViewHolder(view) + } + + override fun onBindViewHolder(holder: NodeViewHolder, position: Int) { + holder.textView.text = Objects.requireNonNull(getNodeData(position)).toString() + } + }.apply { + this.submitGraph(graph) + recyclerView.adapter = this + } + } + + private fun setupFab(graph: Graph) { + fab = findViewById(R.id.addNode) + fab.setOnClickListener { + val newNode = Node(nodeText) + if (currentNode != null) { + graph.addEdge(currentNode!!, newNode) + } else { + graph.addNode(newNode) + } + adapter.notifyDataSetChanged() + } + fab.setOnLongClickListener { + if (currentNode != null) { + graph.removeNode(currentNode!!) + currentNode = null + adapter.notifyDataSetChanged() + fab.hide() + } + true + } + } + + private fun setupToolbar() { + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + val ab = supportActionBar + if (ab != null) { + ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back) + ab.setDisplayHomeAsUpEnabled(true) + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + protected inner class NodeViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { + var textView: TextView = itemView.findViewById(R.id.textView) + + init { + itemView.setOnClickListener { + if (!fab.isShown) { + fab.show() + } + currentNode = adapter.getNode(bindingAdapterPosition) + Snackbar.make(itemView, "Clicked on " + adapter.getNodeData(bindingAdapterPosition)?.toString(), + Snackbar.LENGTH_SHORT).show() + } + } + } + + protected val nodeText: String + get() = "Node " + nodeCount++ +} \ No newline at end of file diff --git a/sample/src/main/java/dev/bandb/graphview/sample/MainActivity.kt b/sample/src/main/java/dev/bandb/graphview/sample/MainActivity.kt new file mode 100644 index 0000000..4d57410 --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/MainActivity.kt @@ -0,0 +1,62 @@ +package dev.bandb.graphview.sample + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + setupToolbar() + setupRecyclerView() + } + + private fun setupRecyclerView() { + findViewById(R.id.graphs).apply { + layoutManager = LinearLayoutManager(this@MainActivity) + adapter = GraphListAdapter() + val decoration = DividerItemDecoration(applicationContext, DividerItemDecoration.VERTICAL) + addItemDecoration(decoration) + } + } + + private fun setupToolbar() { + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + val ab = supportActionBar + ab?.setDisplayHomeAsUpEnabled(false) + } + + private inner class GraphListAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.main_item, parent, false) + return GraphViewHolder(view) + } + + override fun onBindViewHolder(holder: GraphViewHolder, position: Int) { + val graphItem = MainContent.ITEMS[position] + holder.title.text = graphItem.title + holder.description.text = graphItem.description + holder.itemView.setOnClickListener { startActivity(Intent(this@MainActivity, graphItem.clazz)) } + } + + override fun getItemCount(): Int { + return MainContent.ITEMS.size + } + } + + private inner class GraphViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val title: TextView = itemView.findViewById(R.id.title) + val description: TextView = itemView.findViewById(R.id.description) + } +} \ No newline at end of file diff --git a/sample/src/main/java/dev/bandb/graphview/sample/MainContent.kt b/sample/src/main/java/dev/bandb/graphview/sample/MainContent.kt new file mode 100644 index 0000000..c66aa19 --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/MainContent.kt @@ -0,0 +1,22 @@ +package dev.bandb.graphview.sample + +import dev.bandb.graphview.sample.algorithms.BuchheimWalkerActivity +import dev.bandb.graphview.sample.algorithms.FruchtermanReingoldActivity +import dev.bandb.graphview.sample.algorithms.SugiyamaActivity +import java.util.* + +object MainContent { + val ITEMS: MutableList = ArrayList() + + class GraphItem(val title: String, val description: String, val clazz: Class<*>) { + override fun toString(): String { + return title + } + } + + init { + ITEMS.add(GraphItem("BuchheimWalker", "Algorithm for drawing tree structures", BuchheimWalkerActivity::class.java)) + ITEMS.add(GraphItem("FruchtermanReingold", "Directed graph drawing by simulating attraction/repulsion forces", FruchtermanReingoldActivity::class.java)) + ITEMS.add(GraphItem("Sugiyama et al.", "Algorithm for drawing multilayer graphs, taking advantage of the hierarchical structure of the graph.", SugiyamaActivity::class.java)) + } +} \ No newline at end of file diff --git a/sample/src/main/java/dev/bandb/graphview/sample/algorithms/BuchheimWalkerActivity.kt b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/BuchheimWalkerActivity.kt new file mode 100644 index 0000000..307cd2e --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/BuchheimWalkerActivity.kt @@ -0,0 +1,84 @@ +package dev.bandb.graphview.sample.algorithms + +import android.view.Menu +import android.view.MenuItem +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.tree.BuchheimWalkerConfiguration +import dev.bandb.graphview.layouts.tree.BuchheimWalkerLayoutManager +import dev.bandb.graphview.layouts.tree.TreeEdgeDecoration +import dev.bandb.graphview.sample.GraphActivity +import dev.bandb.graphview.sample.R + +class BuchheimWalkerActivity : GraphActivity() { + + public override fun setLayoutManager() { + val configuration = BuchheimWalkerConfiguration.Builder() + .setSiblingSeparation(100) + .setLevelSeparation(100) + .setSubtreeSeparation(100) + .setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) + .build() + recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, configuration) + } + + public override fun setEdgeDecoration() { + recyclerView.addItemDecoration(TreeEdgeDecoration()) + } + + public override fun createGraph(): Graph { + val graph = Graph() + val node1 = Node(nodeText) + val node2 = Node(nodeText) + val node3 = Node(nodeText) + val node4 = Node(nodeText) + val node5 = Node(nodeText) + val node6 = Node(nodeText) + val node8 = Node(nodeText) + val node7 = Node(nodeText) + val node9 = Node(nodeText) + val node10 = Node(nodeText) + val node11 = Node(nodeText) + val node12 = Node(nodeText) + graph.addEdge(node1, node2) + graph.addEdge(node1, node3) + graph.addEdge(node1, node4) + graph.addEdge(node2, node5) + graph.addEdge(node2, node6) + graph.addEdge(node6, node7) + graph.addEdge(node6, node8) + graph.addEdge(node4, node9) + graph.addEdge(node4, node10) + graph.addEdge(node4, node11) + graph.addEdge(node11, node12) + return graph + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.menu_buchheim_walker_orientations, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val builder = BuchheimWalkerConfiguration.Builder() + .setSiblingSeparation(100) + .setLevelSeparation(300) + .setSubtreeSeparation(300) + val itemId = item.itemId + if (itemId == R.id.topToBottom) { + builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM) + } else if (itemId == R.id.bottomToTop) { + builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_BOTTOM_TOP) + } else if (itemId == R.id.leftToRight) { + builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_LEFT_RIGHT) + } else if (itemId == R.id.rightToLeft) { + builder.setOrientation(BuchheimWalkerConfiguration.ORIENTATION_RIGHT_LEFT) + } else { + return super.onOptionsItemSelected(item) + } + recyclerView.layoutManager = BuchheimWalkerLayoutManager(this, builder.build()) + recyclerView.adapter = adapter + return true + } +} \ No newline at end of file diff --git a/sample/src/main/java/dev/bandb/graphview/sample/algorithms/FruchtermanReingoldActivity.kt b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/FruchtermanReingoldActivity.kt new file mode 100644 index 0000000..3e3cc92 --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/FruchtermanReingoldActivity.kt @@ -0,0 +1,39 @@ +package dev.bandb.graphview.sample.algorithms + +import dev.bandb.graphview.decoration.edge.ArrowEdgeDecoration +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.energy.FruchtermanReingoldLayoutManager +import dev.bandb.graphview.sample.GraphActivity + +class FruchtermanReingoldActivity : GraphActivity() { + + public override fun setLayoutManager() { + recyclerView.layoutManager = FruchtermanReingoldLayoutManager(this, 1000) + } + + public override fun setEdgeDecoration() { + recyclerView.addItemDecoration(ArrowEdgeDecoration()) + } + + public override fun createGraph(): Graph { + val graph = Graph() + val a = Node(nodeText) + val b = Node(nodeText) + val c = Node(nodeText) + val d = Node(nodeText) + val e = Node(nodeText) + val f = Node(nodeText) + val g = Node(nodeText) + val h = Node(nodeText) + graph.addEdge(a, b) + graph.addEdge(a, c) + graph.addEdge(a, d) + graph.addEdge(c, e) + graph.addEdge(d, f) + graph.addEdge(f, c) + graph.addEdge(g, c) + graph.addEdge(h, g) + return graph + } +} \ No newline at end of file diff --git a/sample/src/main/java/dev/bandb/graphview/sample/algorithms/SugiyamaActivity.kt b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/SugiyamaActivity.kt new file mode 100644 index 0000000..fcc0ac5 --- /dev/null +++ b/sample/src/main/java/dev/bandb/graphview/sample/algorithms/SugiyamaActivity.kt @@ -0,0 +1,81 @@ +package dev.bandb.graphview.sample.algorithms + +import dev.bandb.graphview.graph.Graph +import dev.bandb.graphview.graph.Node +import dev.bandb.graphview.layouts.layered.SugiyamaArrowEdgeDecoration +import dev.bandb.graphview.layouts.layered.SugiyamaConfiguration +import dev.bandb.graphview.layouts.layered.SugiyamaLayoutManager +import dev.bandb.graphview.sample.GraphActivity + +class SugiyamaActivity : GraphActivity() { + + public override fun setLayoutManager() { + recyclerView.layoutManager = SugiyamaLayoutManager(this, SugiyamaConfiguration.Builder().build()) + } + + public override fun setEdgeDecoration() { + recyclerView.addItemDecoration(SugiyamaArrowEdgeDecoration()) + } + + public override fun createGraph(): Graph { + val graph = Graph() + val node1 = Node(nodeText) + val node2 = Node(nodeText) + val node3 = Node(nodeText) + val node4 = Node(nodeText) + val node5 = Node(nodeText) + val node6 = Node(nodeText) + val node8 = Node(nodeText) + val node7 = Node(nodeText) + val node9 = Node(nodeText) + val node10 = Node(nodeText) + val node11 = Node(nodeText) + val node12 = Node(nodeText) + val node13 = Node(nodeText) + val node14 = Node(nodeText) + val node15 = Node(nodeText) + val node16 = Node(nodeText) + val node17 = Node(nodeText) + val node18 = Node(nodeText) + val node19 = Node(nodeText) + val node20 = Node(nodeText) + val node21 = Node(nodeText) + val node22 = Node(nodeText) + val node23 = Node(nodeText) + graph.addEdge(node1, node13) + graph.addEdge(node1, node21) + graph.addEdge(node1, node4) + graph.addEdge(node1, node3) + graph.addEdge(node2, node3) + graph.addEdge(node2, node20) + graph.addEdge(node3, node4) + graph.addEdge(node3, node5) + graph.addEdge(node3, node23) + graph.addEdge(node4, node6) + graph.addEdge(node5, node7) + graph.addEdge(node6, node8) + graph.addEdge(node6, node16) + graph.addEdge(node6, node23) + graph.addEdge(node7, node9) + graph.addEdge(node8, node10) + graph.addEdge(node8, node11) + graph.addEdge(node9, node12) + graph.addEdge(node10, node13) + graph.addEdge(node10, node14) + graph.addEdge(node10, node15) + graph.addEdge(node11, node15) + graph.addEdge(node11, node16) + graph.addEdge(node12, node20) + graph.addEdge(node13, node17) + graph.addEdge(node14, node17) + graph.addEdge(node14, node18) + graph.addEdge(node16, node18) + graph.addEdge(node16, node19) + graph.addEdge(node16, node20) + graph.addEdge(node18, node21) + graph.addEdge(node19, node22) + graph.addEdge(node21, node23) + graph.addEdge(node22, node23) + return graph + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_graph.xml b/sample/src/main/res/layout/activity_graph.xml index ffdafe8..4d10f08 100644 --- a/sample/src/main/res/layout/activity_graph.xml +++ b/sample/src/main/res/layout/activity_graph.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="de.blox.graphview.sample.GraphActivity"> + tools:context=".GraphActivity"> + + - + android:layout_marginTop="?attr/actionBarSize" /> - - diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index f789b48..0034020 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -1,11 +1,10 @@ - + tools:context=".MainActivity">