From 679fbc8fcb3d813491c83d89fbf0942c757db9e6 Mon Sep 17 00:00:00 2001 From: Francois Campbell Date: Sat, 27 Aug 2016 11:29:30 -0400 Subject: [PATCH 1/2] Basic animation support --- .../circlelayout/CircleLayout.kt | 49 ++++++++++--------- circlelayout/src/main/res/values/attrs.xml | 8 +-- testapp/build.gradle | 1 + .../francoiscampbell/testapp/MainActivity.kt | 13 +++++ testapp/src/main/res/layout/activity_main.xml | 5 +- 5 files changed, 48 insertions(+), 28 deletions(-) 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..c3029d3 100644 --- a/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt +++ b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt @@ -23,22 +23,28 @@ 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") + var angle: Float = 0f + set (value) { + field = value + requestLayout() + } + var angleOffset: Float = 0f + set (value) { + field = value + requestLayout() + } + var radius = FITS_LARGEST_CHILD + set(value) { + field = value + requestLayout() } 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 + val hasCenterView: Boolean get() = centerViewId != View.NO_ID private var centerViewId: Int @@ -60,8 +66,7 @@ class CircleLayout @JvmOverloads constructor( 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) + radius = attributes.getInt(R.styleable.CircleLayout_cl_radius, FITS_LARGEST_CHILD) direction = attributes.getInt(R.styleable.CircleLayout_cl_direction, COUNTER_CLOCKWISE) attributes.recycle() } @@ -95,7 +100,7 @@ class CircleLayout @JvmOverloads constructor( var maxChildRadius = 0 childrenToLayout.clear() forEachChild { - if (layoutHasCenterView && id == centerViewId || visibility == GONE) { + if (hasCenterView && id == centerViewId || visibility == GONE) { return@forEachChild } childrenToLayout.add(this) @@ -106,7 +111,7 @@ class CircleLayout @JvmOverloads constructor( 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) } @@ -118,10 +123,10 @@ class CircleLayout @JvmOverloads constructor( * @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) { + return when (radius) { FITS_LARGEST_CHILD -> outerRadius - maxChildRadius FITS_SMALLEST_CHILD -> outerRadius - minChildRadius - else -> outerRadius - maxChildRadius + else -> Math.abs(radius) } } @@ -173,14 +178,14 @@ class CircleLayout @JvmOverloads constructor( /** * The type of override for the radius of the circle */ - private val FITS_SMALLEST_CHILD = 0 - private val FITS_LARGEST_CHILD = 1 + val FITS_SMALLEST_CHILD = -1 + val FITS_LARGEST_CHILD = -2 /** * The direction of rotation, 1 for counter-clockwise, -1 for clockwise */ - private val COUNTER_CLOCKWISE = 1 - private val CLOCKWISE = -1 + val COUNTER_CLOCKWISE = 1 + val CLOCKWISE = -1 } } diff --git a/circlelayout/src/main/res/values/attrs.xml b/circlelayout/src/main/res/values/attrs.xml index 8a1a4f9..25fbfcb 100644 --- a/circlelayout/src/main/res/values/attrs.xml +++ b/circlelayout/src/main/res/values/attrs.xml @@ -6,11 +6,11 @@ - - - - + + + + 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..b0def3b 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,25 @@ 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(300) + .apply { + repeatMode = ObjectAnimator.REVERSE + repeatCount = ObjectAnimator.INFINITE + 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..cbc1073 100644 --- a/testapp/src/main/res/layout/activity_main.xml +++ b/testapp/src/main/res/layout/activity_main.xml @@ -1,6 +1,7 @@ + cl:cl_centerView="@+id/centerView" + cl:cl_direction="clockwise"> Date: Sun, 4 Sep 2016 11:32:31 -0400 Subject: [PATCH 2/2] Finished animation support --- build.gradle | 2 +- .../circlelayout/CircleLayout.kt | 113 +++++++++++------- .../circlelayout/ViewExtensions.kt | 7 -- circlelayout/src/main/res/values/attrs.xml | 9 ++ .../francoiscampbell/testapp/MainActivity.kt | 12 +- testapp/src/main/res/layout/activity_main.xml | 1 + 6 files changed, 94 insertions(+), 50 deletions(-) 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/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt b/circlelayout/src/main/kotlin/io/github/francoiscampbell/circlelayout/CircleLayout.kt index c3029d3..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,49 +23,71 @@ class CircleLayout @JvmOverloads constructor( defStyleAttr, defStyleRes ) { + /** + * (Optional) A fixed angle between views. + */ var angle: Float = 0f set (value) { - field = 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 + 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(value) { field = Math.signum(value.toFloat()).toInt() requestLayout() } + /** + * Whether this layout currently has a visible view in the center + */ val hasCenterView: Boolean - get() = centerViewId != View.NO_ID + 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() + 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() @@ -99,13 +121,14 @@ class CircleLayout @JvmOverloads constructor( var minChildRadius = outerRadius var maxChildRadius = 0 childrenToLayout.clear() - forEachChild { - if (hasCenterView && 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) @@ -122,7 +145,7 @@ 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 { + fun getLayoutRadius(outerRadius: Int, maxChildRadius: Int, minChildRadius: Int): Int { return when (radius) { FITS_LARGEST_CHILD -> outerRadius - maxChildRadius FITS_SMALLEST_CHILD -> outerRadius - minChildRadius @@ -131,61 +154,69 @@ class CircleLayout @JvmOverloads constructor( } /** - * 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. */ - val FITS_SMALLEST_CHILD = -1 - val FITS_LARGEST_CHILD = -2 + 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 */ - val COUNTER_CLOCKWISE = 1 - 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 25fbfcb..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/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt b/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt index b0def3b..cfce1e5 100644 --- a/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt +++ b/testapp/src/main/kotlin/io/github/francoiscampbell/testapp/MainActivity.kt @@ -14,12 +14,22 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) ObjectAnimator.ofInt(circleLayout, "radius", 0, 500) - .setDuration(300) + .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 cbc1073..3e7379a 100644 --- a/testapp/src/main/res/layout/activity_main.xml +++ b/testapp/src/main/res/layout/activity_main.xml @@ -1,4 +1,5 @@ +