Skip to content

Commit

Permalink
Merged branch 'animation'
Browse files Browse the repository at this point in the history
  • Loading branch information
francoiscampbell committed Sep 4, 2016
2 parents f0409ea + c2b51d5 commit f421730
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 71 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion circlelayout/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, <code>FITS_SMALLEST_CHILD</code>, or <code>FITS_LARGEST_CHILD</code>. Defaults to <code>FITS_LARGEST_CHILD</code>.
*/
var radius = FITS_LARGEST_CHILD
set(value) {
field = value
requestLayout()
}

/**
* The layout direction. Takes the sign (+/-) of the value only. Defaults to <code>COUNTER_CLOCKWISE</code>.
*/
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<View>()

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()
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 <code>n</code> 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 <code>n</code> 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<View>) {
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 <code>setDirection</code>
*/
const val COUNTER_CLOCKWISE = 1
/**
* For use with <code>setDirection</code>
*/
private val COUNTER_CLOCKWISE = 1
private val CLOCKWISE = -1
const val CLOCKWISE = -1
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.francoiscampbell.circlelayout

import android.view.View
import android.view.ViewGroup

/**
* Created by francois on 2016-01-12.
Expand All @@ -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()
})
}
17 changes: 13 additions & 4 deletions circlelayout/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleLayout">
<!-- (Optional) The ID of the child to show at the center of the layout. -->
<attr name="cl_centerView" format="reference" />

<!-- (Optional) A fixed angle between views. -->
<attr name="cl_angle" format="float" />

<!-- The initial angle of the layout pass. A value of 0 will start laying out from the horizontal axis. Defaults to 0. -->
<attr name="cl_angleOffset" format="float" />

<attr name="cl_radius" format="dimension" />
<attr name="cl_radiusPreset">
<enum name="fitsSmallestChild" value="0" />
<enum name="fitsLargestChild" value="1" />
<!-- The radius of the circle. Use a dimension, {@code fitsSmallestChild}, or {@code fitsLargestChild}. Defaults to {@code fitsLargestChild}. -->
<attr name="cl_radius" format="dimension">
<!-- Will adjust the radius to make the smallest child fit in the layout and larger children will bleed outside the radius. -->
<enum name="fitsSmallestChild" value="-1" />

<!-- Will adjust the radius to make the largest child fit in the layout. -->
<enum name="fitsLargestChild" value="-2" />
</attr>

<!-- The layout direction. Defaults to {@code counterClockwise}. -->
<attr name="cl_direction">
<enum name="clockwise" value="-1" />
<enum name="counterClockwise" value="1" />
Expand Down
1 change: 1 addition & 0 deletions testapp/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 23
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
6 changes: 4 additions & 2 deletions testapp/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>

<io.github.francoiscampbell.circlelayout.CircleLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cl="http://schemas.android.com/apk/res-auto"
android:id="@+id/circleLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
cl:cl_angleOffset="90"
cl:cl_direction="clockwise"
cl:cl_centerView="@+id/centerView">
cl:cl_centerView="@+id/centerView"
cl:cl_direction="clockwise">

<Switch
android:id="@+id/centerView"
Expand Down

0 comments on commit f421730

Please sign in to comment.