()
12 |
13 | override fun instantiateItem(container: ViewGroup, position: Int): Any {
14 | val view = views[position]
15 | container.addView(view)
16 | return view
17 | }
18 |
19 | override fun destroyItem(container: ViewGroup, position: Int, ob: Any) {
20 | container.removeView(views[position])
21 | }
22 |
23 | override fun getCount(): Int {
24 | return views.size
25 | }
26 |
27 | override fun isViewFromObject(view: View, ob: Any): Boolean {
28 | return view === ob
29 | }
30 |
31 | override fun getItemPosition(ob: Any): Int {
32 | val index = views.indexOf(ob)
33 | if (index == -1)
34 | return PagerAdapter.POSITION_NONE
35 | else
36 | return index
37 | }
38 |
39 | fun addView(v: View): Int {
40 | return addView(v, views.size)
41 | }
42 |
43 | fun addView(v: View, position: Int): Int {
44 | views.add(position, v)
45 | return position
46 | }
47 |
48 | fun removeView(pager: ViewPager, v: View): Int {
49 | return removeView(pager, views.indexOf(v))
50 | }
51 |
52 | fun removeView(pager: ViewPager, position: Int): Int {
53 | pager.adapter = null
54 | views.removeAt(position)
55 | pager.adapter = this
56 | return position
57 | }
58 |
59 | fun getView(position: Int): View {
60 | return views[position]
61 | }
62 | }
--------------------------------------------------------------------------------
/circular-picker/src/main/java/com/agilie/circularpicker/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.volumecontrol
2 |
3 | import android.graphics.PointF
4 | import java.lang.Math.*
5 |
6 | fun getPointOnBorderLineOfCircle(center: PointF, radius: Float, alfa: Int = 0) =
7 | PointF().apply {
8 | x = (radius * cos(toRadians(alfa - 90.0)) + center.x).toFloat()
9 | y = (radius * sin(toRadians(alfa - 90.0)) + center.y).toFloat()
10 | }
11 |
12 | fun calculateAngleWithTwoVectors(touch: PointF?, center: PointF?): Float {
13 | var angle = 0.0
14 | if (touch != null && center != null) {
15 | val x2 = touch.x - center.x
16 | val y2 = touch.y - center.y
17 | val d1 = sqrt((center.y * center.y).toDouble())
18 | val d2 = sqrt((x2 * x2 + y2 * y2).toDouble())
19 | if (touch.x >= center.x) {
20 | angle = toDegrees(acos((-center.y * y2) / (d1 * d2)))
21 | } else
22 | angle = 360 - toDegrees(acos((-center.y * y2) / (d1 * d2)))
23 | }
24 | return angle.toFloat()
25 | }
26 |
27 | fun distance(point1: PointF, point2: PointF): Float {
28 | return sqrt(((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)).toDouble()).toFloat()
29 | }
30 |
31 | fun pointInCircle(point: PointF, pointCenter: PointF, radius: Float) =
32 | pow((point.x - pointCenter.x).toDouble(), 2.0) +
33 | pow((point.y - pointCenter.y).toDouble(), 2.0) <= radius * radius
34 |
35 |
36 | fun closestValue(value: Int, step: Int): Int {
37 | var j = (Math.round(value.toDouble())).toInt()
38 | while (true) {
39 | if (j > 0 && step > 0) {
40 | if (j % step == 0)
41 | return j
42 | else
43 | ++j
44 | } else
45 | return j
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/circular-picker/src/main/java/com/agilie/circularpicker/ui/view/CircularPickerViewPager.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.circularpicker.ui.view
2 |
3 | import android.content.Context
4 | import android.graphics.PointF
5 | import android.support.v4.view.ViewPager
6 | import android.util.AttributeSet
7 | import android.view.MotionEvent
8 | import com.agilie.circularpicker.ui.PickerPagerAdapter
9 | import com.agilie.volumecontrol.pointInCircle
10 |
11 |
12 | class CircularPickerViewPager : ViewPager {
13 |
14 | private var pagerAdapter = PickerPagerAdapter()
15 |
16 | constructor(context: Context?) : super(context)
17 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
18 |
19 | var swipeEnable = false
20 |
21 | override fun onTouchEvent(ev: MotionEvent?): Boolean {
22 | if (swipeEnable) return super.onTouchEvent(ev)
23 | else return false
24 | }
25 |
26 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
27 | if (swipeEnable) return super.onInterceptTouchEvent(ev)
28 | else return false
29 | }
30 |
31 | fun onAddView(view: CircularPickerView) {
32 | addTouchListener(view)
33 | this@CircularPickerViewPager.addView(view)
34 | pagerAdapter.addView(view)
35 | pagerAdapter.notifyDataSetChanged()
36 | adapter = pagerAdapter
37 | }
38 |
39 | fun getView(position: Int) = pagerAdapter.views[position] as CircularPickerView
40 |
41 | private fun addTouchListener(view: CircularPickerView) {
42 | view.apply {
43 | touchListener = object : CircularPickerView.TouchListener {
44 | override fun onViewTouched(pointF: PointF, event: MotionEvent?) {
45 | val pickerPoint = pointInCircle(pointF, center, radius + maxPullUp) &&
46 | !pointInCircle(pointF, center, (radius * swipeRadiusFactor))
47 |
48 | picker = pickerPoint
49 | this@CircularPickerViewPager.swipeEnable = !pickerPoint
50 | this@CircularPickerViewPager.onInterceptTouchEvent(event)
51 | }
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/TimePickerExample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
24 |
25 |
35 |
36 |
46 |
47 |
48 |
55 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/circular-picker/src/main/java/com/agilie/circularpicker/ui/animation/PickerPath.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.circularpicker.ui.animation
2 |
3 | import android.graphics.*
4 |
5 |
6 | class PickerPath(val pickerPaint: Paint,
7 | val pointerPaint: Paint) {
8 |
9 | private val path = Path()
10 | private val pointerPath = Path()
11 | private val pointerRadius = 10f
12 | var lockMove: Boolean = true
13 | var center = PointF()
14 | var radius = 0f
15 |
16 | fun onDraw(canvas: Canvas) {
17 | canvas.drawPath(path, pickerPaint)
18 | canvas.drawPath(pointerPath, pointerPaint)
19 | }
20 |
21 | fun onActionDown(angle: Int, pullUp: Float) {
22 | // Draw egg
23 | updatePickerPath(pullUp)
24 | rotatePicker(angle)
25 | }
26 |
27 | fun onActionMove(angle: Int, pullUp: Float) {
28 | if (lockMove)
29 | return
30 |
31 | updatePickerPath(pullUp)
32 | updatePointerPath(pullUp)
33 | rotatePicker(angle)
34 | }
35 |
36 | fun onActionUp() {
37 | updatePickerPath(0f)
38 | updatePointerPath(0f)
39 | }
40 |
41 | fun createPickerPath() {
42 | updatePickerPath(0f)
43 | updatePointerPath(0f)
44 | }
45 |
46 | private fun rotatePicker(angle: Int) {
47 | // Rotate egg
48 | val matrix = Matrix()
49 | matrix.setRotate(angle.toFloat(), center.x, center.y)
50 | path.transform(matrix)
51 | pointerPath.transform(matrix)
52 | }
53 |
54 | private fun updatePickerPath(pullUp: Float) {
55 | path.reset()
56 |
57 | val controlDelta = radius * 0.552f
58 | // Draw egg or circle
59 | val offset = pullUp // radius + pullUp
60 |
61 | val startPoint = PointF(center.x, center.y - radius - offset)
62 | path.moveTo(startPoint.x, startPoint.y)
63 |
64 | var controlPoint1 = PointF(center.x + controlDelta, center.y - radius - offset)
65 | var controlPoint2 = PointF(center.x + radius, center.y - controlDelta)
66 |
67 | val point2 = PointF(center.x + radius, center.y)
68 | path.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, point2.x, point2.y)
69 |
70 | controlPoint1 = PointF(center.x + radius, center.y + controlDelta)
71 | controlPoint2 = PointF(center.x + controlDelta, center.y + radius)
72 |
73 | val point3 = PointF(center.x, center.y + radius)
74 | path.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, point3.x, point3.y)
75 |
76 | controlPoint1 = PointF(center.x - controlDelta, center.y + radius)
77 | controlPoint2 = PointF(center.x - radius, center.y + controlDelta)
78 | val point4 = PointF(center.x - radius, center.y)
79 | path.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, point4.x, point4.y)
80 |
81 | controlPoint1 = PointF(center.x - radius, center.y - controlDelta)
82 | controlPoint2 = PointF(center.x - controlDelta, center.y - radius - offset)
83 | path.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, startPoint.x, startPoint.y)
84 |
85 | path.close()
86 | }
87 |
88 | private fun updatePointerPath(pullUp: Float) {
89 | pointerPath.reset()
90 |
91 | val x = center.x
92 | val y = center.y - radius - pullUp + 30f
93 |
94 | pointerPath.addCircle(x, y, pointerRadius, Path.Direction.CW)
95 | pointerPath.close()
96 |
97 | }
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/TimePickerExample/src/main/java/com/agilie/timepickerexample/SampleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.timepickerexample
2 |
3 | import android.graphics.Color
4 | import android.graphics.Typeface
5 | import android.os.Bundle
6 | import android.support.v4.view.ViewPager
7 | import android.support.v7.app.AppCompatActivity
8 | import com.agilie.circularpicker.presenter.CircularPickerContract
9 | import com.agilie.circularpicker.ui.view.CircularPickerView
10 | import com.agilie.circularpicker.ui.view.PickerPagerTransformer
11 | import kotlinx.android.synthetic.main.activity_main.*
12 |
13 | class SampleActivity : AppCompatActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.activity_main)
18 |
19 | view_pager.apply {
20 | clipChildren = false
21 | setPageTransformer(false, PickerPagerTransformer(context, 300))
22 | }
23 | val typeface = Typeface.createFromAsset(this.assets, "OpenSans-ExtraBold.ttf")
24 |
25 | view_pager.onAddView(CircularPickerView(this@SampleActivity).apply {
26 | colors = (intArrayOf(
27 | Color.parseColor("#00EDE9"),
28 | Color.parseColor("#0087D9"),
29 | Color.parseColor("#8A1CC3")))
30 | gradientAngle = 220
31 | maxLapCount = 1
32 | centeredTypeFace = typeface
33 | currentValue = 13
34 | maxValue = 100
35 | centeredTextSize = 60f
36 | centeredText = "Volume"
37 | valueChangedListener = (object : CircularPickerContract.Behavior.ValueChangedListener {
38 | override fun onValueChanged(value: Int) {
39 | when (value) {
40 | 0 -> hoursTextView.text = value.toString() + "0"
41 | else -> hoursTextView.text = value.toString()
42 | }
43 | }
44 | })
45 | colorChangedListener = (object : CircularPickerContract.Behavior.ColorChangedListener {
46 | override fun onColorChanged(r: Int, g: Int, b: Int) {
47 | hoursTextView.setTextColor(Color.rgb(r, g, b))
48 | }
49 | })
50 | })
51 |
52 | view_pager.onAddView(CircularPickerView(this@SampleActivity).apply {
53 | colors = (intArrayOf(
54 | Color.parseColor("#FF8D00"),
55 | Color.parseColor("#FF0058"),
56 | Color.parseColor("#920084")))
57 | gradientAngle = 150
58 | maxValue = 60
59 | currentValue = 24
60 | centeredTypeFace = typeface
61 | maxLapCount = 1
62 | centeredTextSize = 60f
63 | centeredText = "Minutes"
64 | valueChangedListener = object : CircularPickerContract.Behavior.ValueChangedListener {
65 | override fun onValueChanged(value: Int) {
66 | when (value) {
67 | 0 -> minutesTextView.text = value.toString() + "0"
68 | else -> minutesTextView.text = value.toString()
69 | }
70 | }
71 | }
72 | colorChangedListener = (object : CircularPickerContract.Behavior.ColorChangedListener {
73 | override fun onColorChanged(r: Int, g: Int, b: Int) {
74 | minutesTextView.setTextColor(Color.rgb(r, g, b))
75 | }
76 | })
77 | })
78 | hoursTextView.apply {
79 | this.typeface = typeface
80 | text = "13"
81 | }
82 |
83 | minutesTextView.apply {
84 | this.typeface = typeface
85 | text = "24"
86 |
87 | }
88 | setupScale()
89 | addPageListener()
90 |
91 | }
92 |
93 | private fun addPageListener() {
94 | view_pager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
95 | override fun onPageScrollStateChanged(state: Int) {
96 | // empty
97 | }
98 |
99 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
100 | val scaleFactor = 1 - positionOffset * 0.3f
101 | val scrollX = -positionOffset * (hoursTextView.width + colonTextView.width)
102 | if (positionOffset > 0) {
103 | // Scale
104 | scaleView(scaleFactor)
105 | // Translation
106 | translationView(scrollX)
107 | // ColorZ
108 | hoursTextView.setTextColor(blendColors(resources.getColor(R.color.colorFirstCounter), resources.getColor((R.color.colorCounterBehind)), positionOffset))
109 |
110 | minutesTextView.apply {
111 | scaleX = 0.7f + positionOffset * 0.3f
112 | scaleY = 0.7f + positionOffset * 0.3f
113 | setTextColor(blendColors(resources.getColor(R.color.colorCounterBehind), resources.getColor(R.color.colorSecondCounter), positionOffset))
114 | }
115 | }
116 | }
117 |
118 | override fun onPageSelected(position: Int) {
119 | // empty
120 | }
121 |
122 | })
123 | }
124 |
125 | private fun setupScale() {
126 | minutesTextView.apply {
127 | scaleX = 0.7f
128 | scaleY = 0.7f
129 | }
130 |
131 | colonTextView.apply {
132 | scaleX = 0.7f
133 | scaleY = 0.7f
134 | }
135 | }
136 |
137 | private fun translationView(scrollX: Float) {
138 | hoursTextView.translationX = scrollX
139 | colonTextView.translationX = scrollX
140 | minutesTextView.translationX = scrollX
141 | }
142 |
143 | private fun scaleView(scaleFactor: Float) {
144 | hoursTextView.apply {
145 | scaleX = scaleFactor
146 | scaleY = scaleFactor
147 | }
148 | }
149 |
150 | private fun blendColors(from: Int, to: Int, ratio: Float): Int {
151 | val inverseRatio = 1f - ratio
152 |
153 | val r = Color.red(to) * ratio + Color.red(from) * inverseRatio
154 | val g = Color.green(to) * ratio + Color.green(from) * inverseRatio
155 | val b = Color.blue(to) * ratio + Color.blue(from) * inverseRatio
156 |
157 | return Color.rgb(r.toInt(), g.toInt(), b.toInt())
158 | }
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | We are pleased to offer you our new free lightweight plugin named CircularPicker.
25 |
26 | CircularPicker is helpful for creating a controller aimed to manage any calculated parameter. For example, it can be used as a countdown timer or for keeping the score in the game interface.
27 |
28 | CircularPicker can be customized to meet your individual requirements. The developer can set the number of the controllers and their design by selecting a color, gradient and other similar parameters. In addition, it’s possible to specify the transition type for showing controllers on the screen.
29 | ### Demo
30 |
31 |
32 | ## Link to iOS repo
33 |
34 | Check out our iOS [CircularPicker](https://github.com/agilie/AGCircularPicker/) also!
35 |
36 | ## Example
37 | To run the example project, clone the repo and run [sample](TimePickerExample/).
38 |
39 | ### How does it work?
40 |
41 | Just add CircularPickerPagerContainer which contains CircularPickerViewPager to your layout file.
42 | ````xml
43 |
47 |
48 |
54 |
55 |
56 |
57 | ````
58 | Also you can use only CircularPickerView
59 | ````xml
60 |
63 | ````
64 |
65 | The library contains three key elements:
66 | 1. CircularPickerPagerContainer - a custom container to show more than one page at a time.
67 | 2. CircularPickerViewPager - a custom ViewPager in which we define the region in the view element for swipe action.
68 | 3. CircularPickerView - a controller aimed to manage any calculated parameter.
69 | CircularPickerView has the following settings:
70 | ````kotlin
71 | var colors : intArrayOf
72 | var gradientAngle : Int
73 | var maxLapCount : Int // number of laps (required)
74 | var maxValue : Int // total values (required)
75 | var currentValue : Int
76 | var centeredTextSize : Float
77 | var centeredText : String
78 | var centeredTextColor: Int
79 | var centeredTypeFace : TypeFace
80 | var valueChangedListener : object
81 | var colorChangedListener : object
82 | ````
83 | ````xml
84 |
85 |
86 |
87 |
88 | ````
89 |
90 | ### Our example of using CircularPicker
91 | Let's see how we can use it in practice.
92 | In our *layout.xml* we added CircularPickerPagerContainer which contains CircularPickerViewPager, then created CircularPickerView in the Activity and set up parameters:
93 |
94 | ````gradle
95 | СircularPickerView(context).apply {
96 | colors = (intArrayOf(
97 | Color.parseColor("#00EDE9"),
98 | Color.parseColor("#0087D9"),
99 | Color.parseColor("#8A1CC3")))
100 | gradientAngle = 220
101 | maxLapCount = 2
102 | currentValue = 13
103 | maxValue = 24
104 | centeredTextSize = 60f
105 | centeredText = "Hours"
106 | ````
107 | Here are also two callback interfaces provided with _CircularPickerView_. Use them to handle changes made during the interaction with the component:
108 |
109 | ````kotlin
110 | interface ValueChangedListener {
111 | fun onValueChanged(value: Int)
112 | }
113 |
114 | interface ColorChangedListener {
115 | fun onColorChanged(r: Int, g: Int, b: Int)
116 | }
117 | ````
118 |
119 | ## Usage
120 |
121 | ### Gradle
122 |
123 | Add dependency in your `build.gradle` file:
124 | ````gradle
125 | compile 'com.agilie:circular-picker:1.0'
126 | ````
127 |
128 | ### Maven
129 | Add rependency in your `.pom` file:
130 | ````xml
131 |
132 | com.agilie
133 | circular-picker
134 | 1.0
135 | pom
136 |
137 | ````
138 |
139 | ## Requirements
140 |
141 | CircularPicker works on Android API 16+
142 |
143 | ## Troubleshooting
144 |
145 | Problems? Check the [Issues](https://github.com/agilie/AGMobileGift/issues) block
146 | to find the solution or create an new issue that we will fix asap.
147 |
148 |
149 | ## Author
150 |
151 | This library is open-sourced by [Agilie Team](https://www.agilie.com?utm_source=github&utm_medium=referral&utm_campaign=Git_Android_Kotlin&utm_term=CircularPicker)
152 |
153 | ## Contributors
154 |
155 | - [Eugene Surkov](https://github.com/ukevgen)
156 | - [Roman Kapshuk](https://github.com/RomanKapshuk)
157 |
158 | ## Contact us
159 | If you have any questions, suggestions or just need a help with web or mobile development, please email us at
160 | You can ask us anything from basic to complex questions.
161 | We will continue publishing new open-source projects. Stay with us, more updates will follow!
162 |
163 | ## License
164 |
165 | The [MIT](LICENSE.md) License (MIT) Copyright © 2017 [Agilie Team](https://www.agilie.com?utm_source=github&utm_medium=referral&utm_campaign=Git_Android_Kotlin&utm_term=CircularPicker)
166 |
--------------------------------------------------------------------------------
/circular-picker/src/main/java/com/agilie/circularpicker/ui/view/CircularPickerView.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.circularpicker.ui.view
2 |
3 | import android.content.Context
4 | import android.graphics.*
5 | import android.util.AttributeSet
6 | import android.view.MotionEvent
7 | import android.view.View
8 | import com.agilie.circularpicker.R
9 | import com.agilie.circularpicker.R.styleable.CircularPickerView_circularPickerSpace
10 | import com.agilie.circularpicker.R.styleable.CircularPickerView_pullUp
11 | import com.agilie.circularpicker.presenter.BaseBehavior
12 | import com.agilie.circularpicker.presenter.CircularPickerContract
13 | import com.agilie.circularpicker.ui.animation.PickerPath
14 | import com.agilie.volumecontrol.closestValue
15 |
16 | class CircularPickerView : View, View.OnTouchListener, CircularPickerContract.View {
17 | var behavior: BaseBehavior = PickerBehavior()
18 |
19 | var swipeRadiusFactor: Float
20 | get() = behavior.swipeRadiusFactor
21 | set(value) {
22 | behavior.swipeRadiusFactor = value
23 | }
24 |
25 | var centeredTextSize: Float
26 | get() = behavior.centeredTextSize
27 | set(value) {
28 | behavior.centeredTextSize = value
29 | }
30 | var maxPullUp: Float
31 | get() = behavior.maxPullUp
32 | set(value) {
33 | behavior.maxPullUp
34 | }
35 |
36 | var viewSpace: Float
37 | get() = behavior.viewSpace
38 | set(value) {
39 | behavior.viewSpace
40 | }
41 |
42 | var centeredTextColor: Int
43 | get() = behavior.centeredTextColor
44 | set(value) {
45 | behavior.centeredTextColor = value
46 | }
47 |
48 | var centeredTypeFace: Typeface
49 | get() = behavior.centeredTypeface
50 | set(value) {
51 | behavior.centeredTypeface = value
52 | }
53 |
54 | var maxValue: Int
55 | get() = behavior.countOfValues
56 | set(value) {
57 | behavior.countOfValues = value
58 | behavior.build()
59 | }
60 |
61 | var maxLapCount: Int
62 | get() = behavior.maxLapCount
63 | set(value) {
64 | behavior.maxLapCount = value
65 | behavior.build()
66 | }
67 |
68 | var currentValue: Int = 1
69 | set(value) {
70 | behavior.currentValue = value
71 | }
72 |
73 | var color: Int
74 | get() = behavior.colors[0]
75 | set(value) {
76 | behavior.colors = intArrayOf(value, value)
77 | behavior.updatePaint(center, radius)
78 | }
79 |
80 | var colors: IntArray
81 | get() = behavior.colors
82 | set(value) {
83 | behavior.colors = value
84 | behavior.updatePaint(center, radius)
85 | }
86 |
87 | var gradientAngle: Int
88 | get() = behavior.gradientAngle
89 | set(value) {
90 | behavior.gradientAngle = value
91 | behavior.updatePaint(center, radius)
92 | }
93 |
94 | var centeredText: String
95 | get() = behavior.centeredText
96 | set(value) {
97 | behavior.centeredText = value
98 | }
99 |
100 | var valueChangedListener: CircularPickerContract.Behavior.ValueChangedListener?
101 | get() = behavior.valueChangedListener
102 | set(value) {
103 | behavior.valueChangedListener = value
104 | }
105 |
106 | var colorChangedListener: CircularPickerContract.Behavior.ColorChangedListener?
107 | get() = behavior.colorChangedListener
108 | set(value) {
109 | behavior.colorChangedListener = value
110 | }
111 |
112 | private var w = 0
113 | private var h = 0
114 |
115 | var picker: Boolean
116 | set(value) {
117 | behavior.picker = value
118 | }
119 | get() = behavior.picker
120 |
121 | val center: PointF
122 | get() = behavior.pointCenter
123 | val radius: Float
124 | get() = behavior.radius
125 |
126 | var touchListener: TouchListener? = null
127 |
128 | constructor(context: Context?) : super(context) {
129 | init(null)
130 | }
131 |
132 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
133 | init(attrs)
134 | }
135 |
136 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
137 | init(attrs)
138 | }
139 |
140 | override fun onDraw(canvas: Canvas) {
141 | super.onDraw(canvas)
142 | behavior.onDraw(canvas)
143 | }
144 |
145 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
146 | super.onSizeChanged(w, h, oldw, oldh)
147 | this.w = w
148 | this.h = h
149 | behavior.onSizeChanged(w, h)
150 | }
151 |
152 | override fun onTouch(v: View, event: MotionEvent): Boolean {
153 | when (event.action) {
154 | MotionEvent.ACTION_DOWN -> touchListener?.onViewTouched(PointF(event.x, event.y), event)
155 |
156 | }
157 | return behavior.onTouchEvent(event)
158 | }
159 |
160 | fun onInvalidate() {
161 | invalidate()
162 | }
163 |
164 | private fun init(attrs: AttributeSet?) {
165 | setOnTouchListener(this)
166 | this.isDrawingCacheEnabled = true
167 |
168 | val attributes = context
169 | .obtainStyledAttributes(attrs, R.styleable.CircularPickerView)
170 | viewSpace = attributes.getFloat(CircularPickerView_circularPickerSpace, behavior.viewSpace)
171 | maxPullUp = attributes.getFloat(CircularPickerView_pullUp, behavior.maxPullUp)
172 | }
173 |
174 | interface TouchListener {
175 | fun onViewTouched(pointF: PointF, event: MotionEvent?)
176 | }
177 |
178 | private fun setTrianglePaint() = Paint().apply {
179 | color = Color.WHITE
180 | isAntiAlias = true
181 | style = Paint.Style.FILL
182 | pathEffect = CornerPathEffect(10f)
183 | strokeWidth = 2f
184 | }
185 |
186 | private fun setPickerPaint() = Paint().apply {
187 | color = Color.WHITE
188 | isAntiAlias = true
189 | style = Paint.Style.FILL
190 | strokeWidth = 4f
191 | }
192 |
193 | inner class PickerBehavior : BaseBehavior(this@CircularPickerView, PickerPath(setPickerPaint(), setTrianglePaint())) {
194 | private var valuesPerLap = 1
195 | private var anglesPerValue = 1
196 |
197 | override fun build() {
198 | valuesPerLap = countOfValues / maxLapCount
199 | anglesPerValue = 360 / valuesPerLap
200 | }
201 |
202 | var prevValue = 0
203 | var angle = 0
204 | override fun calculateValue(angle: Int): Int {
205 | this.angle = angle
206 |
207 | val closestAngle = closestValue(angle, anglesPerValue)
208 |
209 | val value = (countOfValues * closestAngle) / (360 * maxLapCount) - 1
210 | return value
211 | }
212 |
213 | override fun value(value: Int) {
214 | if (prevValue == value) return
215 | if (value < 0) valueChangedListener?.onValueChanged(prevValue)
216 | else {
217 | prevValue = value
218 | valueChangedListener?.onValueChanged(prevValue)
219 | }
220 | }
221 |
222 | }
223 | }
--------------------------------------------------------------------------------
/circular-picker/src/main/java/com/agilie/circularpicker/presenter/BaseBehavior.kt:
--------------------------------------------------------------------------------
1 | package com.agilie.circularpicker.presenter
2 |
3 | import android.graphics.*
4 | import android.util.Log
5 | import android.view.MotionEvent
6 | import com.agilie.circularpicker.ui.animation.PickerPath
7 | import com.agilie.circularpicker.ui.view.CircularPickerView
8 | import com.agilie.volumecontrol.calculateAngleWithTwoVectors
9 | import com.agilie.volumecontrol.distance
10 | import com.agilie.volumecontrol.getPointOnBorderLineOfCircle
11 | import java.lang.Math.*
12 |
13 | abstract class BaseBehavior : CircularPickerContract.Behavior {
14 |
15 | val view: CircularPickerView
16 | val pickerPath: PickerPath
17 | var countOfValues = 24
18 | var currentValue = 0
19 | var maxLapCount = 2
20 | var colors: IntArray = intArrayOf(
21 | Color.parseColor("#0080ff"),
22 | Color.parseColor("#53FFFF"))
23 |
24 | constructor(view: CircularPickerView, pickerPath: PickerPath) {
25 | this.view = view
26 | this.pickerPath = pickerPath
27 | }
28 |
29 | private companion object {
30 | val MIN_ANGLE = 0
31 | val MAX_ANGLE = 360
32 | }
33 |
34 | var valueChangedListener: CircularPickerContract.Behavior.ValueChangedListener? = null
35 | var colorChangedListener: CircularPickerContract.Behavior.ColorChangedListener? = null
36 |
37 | var centeredText = ""
38 | var swipeRadiusFactor = 0.6f
39 | var viewSpace = 3f
40 | var maxPullUp = 35f
41 |
42 | var picker = true
43 |
44 | val pointCenter: PointF
45 | get() = pickerPath.center
46 | val radius: Float
47 | get() = pickerPath.radius
48 |
49 | var centeredTextSize = 50f
50 | var centeredStrokeWidth = 40f
51 | var centeredTypeface = Typeface.DEFAULT
52 | var centeredTextColor = Color.WHITE
53 |
54 | var textPaint = Paint().apply {
55 | color = centeredTextColor
56 | textSize = centeredTextSize
57 | typeface = centeredTypeface
58 | strokeWidth = centeredStrokeWidth
59 | }
60 |
61 | override fun onDraw(canvas: Canvas) {
62 | pickerPath.onDraw(canvas)
63 | drawText(canvas)
64 | }
65 |
66 | fun drawText(canvas: Canvas) {
67 | textPaint.color = centeredTextColor
68 | textPaint.textSize = centeredTextSize
69 | textPaint.typeface = centeredTypeface
70 | textPaint.strokeWidth = centeredStrokeWidth
71 | canvas.drawText(centeredText, (pointCenter.x - (textPaint.measureText(centeredText) / 2)),
72 | (pointCenter.y - ((textPaint.descent() + textPaint.ascent())) / 2), textPaint)
73 | }
74 |
75 | override fun onSizeChanged(width: Int, height: Int) {
76 | val center = PointF(width / 2f, height / 2f)
77 | val radius = max(min(width, height), 0) / viewSpace
78 | updatePaint(center, radius)
79 | drawShapes(center, radius)
80 | }
81 |
82 | var gradientAngle = 0
83 |
84 | fun updatePaint(center: PointF, radius: Float) {
85 | val startPoint = getPointOnBorderLineOfCircle(center, radius, 180 + gradientAngle)
86 | val endPoint = getPointOnBorderLineOfCircle(center, radius, 0 + gradientAngle)
87 | pickerPath.pickerPaint.apply {
88 | shader = LinearGradient(startPoint.x, startPoint.y, endPoint.x, endPoint.y, colors,
89 | null,
90 | Shader.TileMode.CLAMP)
91 | }
92 | }
93 |
94 | override fun onTouchEvent(event: MotionEvent): Boolean {
95 | when (event.action) {
96 | MotionEvent.ACTION_DOWN -> {
97 | onActionDown(PointF(event.x, event.y))
98 | previousPoint = PointF(event.x, event.y)
99 | }
100 | MotionEvent.ACTION_MOVE -> {
101 | onActionMove(PointF(event.x, event.y))
102 | view.onInvalidate()
103 | }
104 | MotionEvent.ACTION_UP -> {
105 |
106 | onActionUp(PointF(event.x, event.y))
107 | view.onInvalidate()
108 | }
109 | }
110 | return true
111 | }
112 |
113 | private fun drawShapes(center: PointF, radius: Float) {
114 | pickerPath.center = center
115 | pickerPath.radius = radius
116 | pickerPath.createPickerPath()
117 | // rotate to current value
118 | val angle = calculateClosestAngle(currentValue)
119 | pickerPath.onActionDown(angle, maxPullUp)
120 | previousAngle = angle
121 |
122 | }
123 |
124 | private fun calculateClosestAngle(currentValue: Int): Int {
125 | if (currentValue >= countOfValues) {
126 | totalAngle = maxLapCount * MAX_ANGLE
127 | return MAX_ANGLE
128 | } else if (currentValue <= 0) {
129 | return 0
130 | } else {
131 | val valPerAngle = (maxLapCount * MAX_ANGLE) / countOfValues
132 | totalAngle = valPerAngle * currentValue
133 | return totalAngle
134 | }
135 |
136 | }
137 |
138 | private fun onActionDown(pointF: PointF) {
139 | calculateAngleValue(pointF)
140 | }
141 |
142 | private fun changeColors(angle: Int, pullUp: Float) {
143 |
144 | val colorPoint = getPointOnBorderLineOfCircle(pointCenter, radius - pullUp, angle)
145 |
146 | view.buildDrawingCache()
147 | var pixel = view.getDrawingCache(true).getPixel(colorPoint.x.toInt(), colorPoint.y.toInt())
148 | colorChangedListener?.onColorChanged(Color.red(pixel), Color.green(pixel), Color.blue(pixel))
149 | }
150 |
151 | var previousPoint = PointF()
152 | private fun onActionMove(pointF: PointF) {
153 |
154 | if (picker) {
155 | val currentAngle = calculateAngleWithTwoVectors(pointF, pickerPath.center).toInt()
156 | val angleChanged = previousAngle != currentAngle
157 |
158 | if (angleChanged) {
159 | if (abs(currentAngle - previousAngle) < 180) {
160 | totalAngle += (currentAngle - previousAngle)
161 | if (totalAngle > MAX_ANGLE * maxLapCount) {
162 | totalAngle = MIN_ANGLE
163 | }
164 | if (totalAngle < MIN_ANGLE) {
165 | totalAngle = MAX_ANGLE * maxLapCount
166 | }
167 | }
168 |
169 | Log.d("TAG", "totalAngle $totalAngle ")
170 | previousAngle = currentAngle
171 |
172 | val distance = distance(pointF, pickerPath.center) - pickerPath.radius
173 | val pullUp = Math.min(maxPullUp, Math.max(distance, 0f))
174 | pickerPath.onActionMove(totalAngle, pullUp)
175 | value(calculateValue(totalAngle))
176 |
177 | changeColors(totalAngle, pullUp)
178 | }
179 | }
180 | }
181 |
182 | private var actionDownAngle = 0
183 | private var totalAngle = 0
184 | private var previousAngle = 0
185 |
186 | private fun onActionUp(pointF: PointF) {
187 | previousAngle = 0
188 | pickerPath.lockMove = true
189 | }
190 |
191 | private fun calculateAngleValue(pointF: PointF) {
192 | if (picker) {
193 | pickerPath.lockMove = !picker
194 | val distance = distance(pointF, pickerPath.center) - pickerPath.radius
195 | val pullUp = Math.min(maxPullUp, Math.max(distance, 0f))
196 | actionDownAngle = calculateAngleWithTwoVectors(pointF, pickerPath.center).toInt()
197 |
198 | if (totalAngle > MAX_ANGLE) {
199 | calculateAngleDiff(actionDownAngle)
200 | } else {
201 | totalAngle = actionDownAngle
202 | }
203 |
204 | previousAngle = actionDownAngle
205 | pickerPath.onActionMove(actionDownAngle, pullUp)
206 | value(calculateValue(totalAngle))
207 |
208 | changeColors(totalAngle, 0f)
209 |
210 | view.onInvalidate()
211 | }
212 | }
213 |
214 | private fun calculateAngleDiff(touchAngle: Int) {
215 | val angleInLap = (totalAngle / MAX_ANGLE) * MAX_ANGLE
216 | val diff = totalAngle - angleInLap
217 | if (touchAngle > diff) {
218 | totalAngle += abs(touchAngle - diff)
219 | } else {
220 | totalAngle -= abs(touchAngle - diff)
221 | }
222 |
223 | }
224 |
225 | abstract fun build()
226 | }
227 |
--------------------------------------------------------------------------------