diff --git a/build.gradle b/build.gradle
index a5d11fb..365b670 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.2.0-beta1'
+ classpath 'com.android.tools.build:gradle:2.2.0-rc1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.9"
// NOTE: Do not place your application dependencies here; they belong
diff --git a/circlelayout/build.gradle b/circlelayout/build.gradle
index ab2923c..51d0b5f 100644
--- a/circlelayout/build.gradle
+++ b/circlelayout/build.gradle
@@ -12,7 +12,7 @@ def projectFriendlyName = 'CircleLayout'
def projectDescription = 'An Android layout for arranging children along a circle'
group 'io.github.francoiscampbell'
-version '0.2.0'
+version '0.3.0'
repositories {
maven {
diff --git a/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt
index 93e8a8a..e5de68e 100644
--- a/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt
+++ b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt
@@ -23,45 +23,72 @@ class CircleLayout @JvmOverloads constructor(
defStyleAttr,
defStyleRes
) {
- var angle: Float
- var angleOffset: Float
- var fixedRadius: Int
- var radiusPreset = FITS_LARGEST_CHILD
- set(newRadiusPreset: Int) = when (newRadiusPreset) {
- FITS_LARGEST_CHILD, FITS_SMALLEST_CHILD -> field = newRadiusPreset
- else -> throw IllegalArgumentException("radiusPreset must be either FITS_LARGEST_CHILD or FITS_SMALLEST_CHILD")
+ /**
+ * (Optional) A fixed angle between views.
+ */
+ var angle: Float = 0f
+ set (value) {
+ field = value % 360f
+ requestLayout()
+ }
+
+ /**
+ * The initial angle of the layout pass. A value of 0 will start laying out from the horizontal axis. Defaults to 0.
+ */
+ var angleOffset: Float = 0f
+ set (value) {
+ field = value % 360f
+ requestLayout()
}
+
+ /**
+ * The radius of the circle. Use a dimension, FITS_SMALLEST_CHILD
, or FITS_LARGEST_CHILD
. Defaults to FITS_LARGEST_CHILD
.
+ */
+ var radius = FITS_LARGEST_CHILD
+ set(value) {
+ field = value
+ requestLayout()
+ }
+
+ /**
+ * The layout direction. Takes the sign (+/-) of the value only. Defaults to COUNTER_CLOCKWISE
.
+ */
var direction = COUNTER_CLOCKWISE
- set(newDirection: Int) = when {
- newDirection > 0 -> field = 1
- newDirection < 0 -> field = -1
- else -> throw IllegalArgumentException("direction must be either positive or negative")
+ set(value) {
+ field = Math.signum(value.toFloat()).toInt()
+ requestLayout()
}
- val layoutHasCenterView: Boolean
- get() = centerViewId != View.NO_ID
+ /**
+ * Whether this layout currently has a visible view in the center
+ */
+ val hasCenterView: Boolean
+ get() = centerView != null && centerView?.visibility != GONE
private var centerViewId: Int
+
+ /**
+ * The view shown in the center of the circle
+ */
var centerView: View? = null
- set(newCenterView: View?) = when {
- newCenterView != null && indexOfChild(newCenterView) == -1 -> {
+ set(newCenterView) {
+ if (newCenterView != null && indexOfChild(newCenterView) == -1) {
throw IllegalArgumentException("View with ID ${newCenterView.id} is not a child of this layout")
}
- else -> {
- field = newCenterView
- centerViewId = newCenterView?.id ?: NO_ID
- }
+ field = newCenterView
+ centerViewId = newCenterView?.id ?: NO_ID
+ requestLayout()
}
+ // Pre-allocate to avoid object allocation in onLayout
private val childrenToLayout = LinkedList()
init {
- val attributes = context.obtainStyledAttributes(attrs, R.styleable.CircleLayout, defStyleAttr, 0)
+ val attributes = context.obtainStyledAttributes(attrs, R.styleable.CircleLayout, defStyleAttr, defStyleRes)
centerViewId = attributes.getResourceId(R.styleable.CircleLayout_cl_centerView, NO_ID)
- angle = Math.toRadians(attributes.getFloat(R.styleable.CircleLayout_cl_angle, 0f).toDouble()).toFloat()
- angleOffset = Math.toRadians(attributes.getFloat(R.styleable.CircleLayout_cl_angleOffset, 0f).toDouble()).toFloat()
- fixedRadius = attributes.getDimensionPixelSize(R.styleable.CircleLayout_cl_radius, 0)
- radiusPreset = attributes.getInt(R.styleable.CircleLayout_cl_radiusPreset, FITS_LARGEST_CHILD)
+ angle = attributes.getFloat(R.styleable.CircleLayout_cl_angle, 0f)
+ angleOffset = attributes.getFloat(R.styleable.CircleLayout_cl_angleOffset, 0f)
+ radius = attributes.getInt(R.styleable.CircleLayout_cl_radius, FITS_LARGEST_CHILD)
direction = attributes.getInt(R.styleable.CircleLayout_cl_direction, COUNTER_CLOCKWISE)
attributes.recycle()
}
@@ -94,19 +121,20 @@ class CircleLayout @JvmOverloads constructor(
var minChildRadius = outerRadius
var maxChildRadius = 0
childrenToLayout.clear()
- forEachChild {
- if (layoutHasCenterView && id == centerViewId || visibility == GONE) {
- return@forEachChild
+ for (i in 0..childCount - 1) {
+ val child = getChildAt(i)
+ if ((hasCenterView && child.id == centerViewId) || child.visibility == GONE) {
+ continue
}
- childrenToLayout.add(this)
- maxChildRadius = Math.max(maxChildRadius, radius)
- minChildRadius = Math.min(minChildRadius, radius)
+ childrenToLayout.add(child)
+ maxChildRadius = Math.max(maxChildRadius, child.radius)
+ minChildRadius = Math.min(minChildRadius, child.radius)
}
//choose angle increment
val angleIncrement = if (angle != 0f) angle else getEqualAngle(childrenToLayout.size)
//choose radius
- val layoutRadius = if (fixedRadius != 0) fixedRadius else getLayoutRadius(outerRadius, maxChildRadius, minChildRadius)
+ val layoutRadius = getLayoutRadius(outerRadius, maxChildRadius, minChildRadius)
layoutChildrenAtAngle(centerX, centerY, angleIncrement, angleOffset, layoutRadius, childrenToLayout)
}
@@ -117,70 +145,78 @@ class CircleLayout @JvmOverloads constructor(
* @param minChildRadius The radius of the smallest child
* @return The radius of the layout path along which the children will be placed
*/
- private fun getLayoutRadius(outerRadius: Int, maxChildRadius: Int, minChildRadius: Int): Int {
- return when (radiusPreset) {
+ fun getLayoutRadius(outerRadius: Int, maxChildRadius: Int, minChildRadius: Int): Int {
+ return when (radius) {
FITS_LARGEST_CHILD -> outerRadius - maxChildRadius
FITS_SMALLEST_CHILD -> outerRadius - minChildRadius
- else -> outerRadius - maxChildRadius
+ else -> Math.abs(radius)
}
}
/**
- * Splits a circle into `n` equal slices
+ * Splits a circle into n
equal slices
* @param numSlices The number of slices in which to divide the circle
- * @return The angle between two adjacent slices, or 2*pi if `n` is zero
+ * @return The angle between two adjacent slices in degrees, or 360 if n
is zero
*/
- private fun getEqualAngle(numSlices: Int): Float = 2 * Math.PI.toFloat() / if (numSlices != 0) numSlices else 1
+ fun getEqualAngle(numSlices: Int): Float = 360f / if (numSlices != 0) numSlices else 1
/**
* Lays out the child views along a circle
* @param cx The X coordinate of the center of the circle
* @param cy The Y coordinate of the center of the circle
- * @param angleIncrement The angle increment between two adjacent children
- * @param angleOffset The starting offset angle from the horizontal axis
+ * @param angleIncrement The angle increment between two adjacent children, in degrees
+ * @param angleOffset The starting offset angle from the horizontal axis, in degrees
* @param radius The radius of the circle along which the centers of the children will be placed
* @param childrenToLayout The views to layout
*/
private fun layoutChildrenAtAngle(cx: Int, cy: Int, angleIncrement: Float, angleOffset: Float, radius: Int, childrenToLayout: List) {
- var currentAngle = angleOffset
- childrenToLayout.forEach {
- val childCenterX = polarToX(radius.toFloat(), currentAngle)
- val childCenterY = polarToY(radius.toFloat(), currentAngle)
- it.layoutFromCenter(cx + childCenterX, cy - childCenterY)
-
- currentAngle += angleIncrement * direction
+ val angleIncrementRad = Math.toRadians(angleIncrement.toDouble())
+ var currentAngleRad = Math.toRadians(angleOffset.toDouble())
+ for (i in 0..childrenToLayout.size - 1) {
+ val child = childrenToLayout[i]
+ val childCenterX = polarToX(radius.toDouble(), currentAngleRad)
+ val childCenterY = polarToY(radius.toDouble(), currentAngleRad)
+ child.layoutFromCenter((cx + childCenterX).toInt(), (cy - childCenterY).toInt())
+
+ currentAngleRad += angleIncrementRad * direction
}
}
/**
* Gets the X coordinate from a set of polar coordinates
* @param radius The polar radius
- * @param angle The polar angle
+ * @param angle The polar angle, in radians
* @return The equivalent X coordinate
*/
- fun polarToX(radius: Float, angle: Float): Int = (radius * Math.cos(angle.toDouble())).toInt()
+ fun polarToX(radius: Double, angle: Double) = radius * Math.cos(angle)
/**
* Gets the Y coordinate from a set of polar coordinates
* @param radius The polar radius
- * @param angle The polar angle
+ * @param angle The polar angle, in radians
* @return The equivalent Y coordinate
*/
- fun polarToY(radius: Float, angle: Float): Int = (radius * Math.sin(angle.toDouble())).toInt()
+ fun polarToY(radius: Double, angle: Double) = radius * Math.sin(angle)
companion object {
/**
- * The type of override for the radius of the circle
+ * Will adjust the radius to make the smallest child fit in the layout and larger children will bleed outside the radius.
+ */
+ const val FITS_SMALLEST_CHILD = -1
+ /**
+ * Will adjust the radius to make the largest child fit in the layout.
*/
- private val FITS_SMALLEST_CHILD = 0
- private val FITS_LARGEST_CHILD = 1
+ const val FITS_LARGEST_CHILD = -2
/**
- * The direction of rotation, 1 for counter-clockwise, -1 for clockwise
+ * For use with setDirection
+ */
+ const val COUNTER_CLOCKWISE = 1
+ /**
+ * For use with setDirection
*/
- private val COUNTER_CLOCKWISE = 1
- private val CLOCKWISE = -1
+ const val CLOCKWISE = -1
}
}
diff --git a/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/ViewExtensions.kt b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/ViewExtensions.kt
index ba1f12e..2a6f64f 100644
--- a/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/ViewExtensions.kt
+++ b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/ViewExtensions.kt
@@ -1,7 +1,6 @@
package io.github.francoiscampbell.circlelayout
import android.view.View
-import android.view.ViewGroup
/**
* Created by francois on 2016-01-12.
@@ -23,10 +22,4 @@ fun View.layoutFromCenter(cx: Int, cy: Int) {
val right = left + measuredWidth
val bottom = top + measuredHeight
layout(left, top, right, bottom)
-}
-
-inline fun ViewGroup.forEachChild(action: View.() -> Unit): Unit {
- repeat(childCount, { index ->
- getChildAt(index).action()
- })
}
\ No newline at end of file
diff --git a/circlelayout/src/main/res/values/attrs.xml b/circlelayout/src/main/res/values/attrs.xml
index 8a1a4f9..55d5e6e 100644
--- a/circlelayout/src/main/res/values/attrs.xml
+++ b/circlelayout/src/main/res/values/attrs.xml
@@ -1,16 +1,25 @@
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/testapp/build.gradle b/testapp/build.gradle
index 4ae2a5d..ffb8bdf 100644
--- a/testapp/build.gradle
+++ b/testapp/build.gradle
@@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 23
diff --git a/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt b/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt
index 5d0b581..cfce1e5 100644
--- a/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt
+++ b/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt
@@ -1,12 +1,35 @@
package io.github.francoiscampbell.testapp
+import android.animation.ObjectAnimator
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
+import android.util.Log
+import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
+ private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+
+ ObjectAnimator.ofInt(circleLayout, "radius", 0, 500)
+ .setDuration(1000)
+ .apply {
+ repeatMode = ObjectAnimator.REVERSE
+ repeatCount = ObjectAnimator.INFINITE
+ addUpdateListener { Log.i(TAG, "animatedValue: ${it.animatedValue}"); }
+ }
+ .start()
+
+ ObjectAnimator.ofFloat(circleLayout, "angleOffset", 0f, 360f)
+ .setDuration(10000)
+ .apply {
+ repeatMode = ObjectAnimator.RESTART
+ repeatCount = ObjectAnimator.INFINITE
+ interpolator = null
+ addUpdateListener { Log.i(TAG, "animatedValue: ${it.animatedValue}"); }
+ }
+ .start()
}
}
diff --git a/testapp/src/main/res/layout/activity_main.xml b/testapp/src/main/res/layout/activity_main.xml
index 21aa8d3..3e7379a 100644
--- a/testapp/src/main/res/layout/activity_main.xml
+++ b/testapp/src/main/res/layout/activity_main.xml
@@ -1,6 +1,8 @@
+
+ cl:cl_centerView="@+id/centerView"
+ cl:cl_direction="clockwise">