@@ -5,11 +5,13 @@ import android.annotation.SuppressLint
55import android.content.Context
66import android.graphics.*
77import android.util.AttributeSet
8+ import android.view.GestureDetector
89import android.view.MotionEvent
910import android.view.View
1011import android.view.animation.OvershootInterpolator
12+ import androidx.dynamicanimation.animation.FlingAnimation
13+ import androidx.dynamicanimation.animation.FloatPropertyCompat
1114import java.util.*
12- import kotlin.math.abs
1315
1416class TaskProgressView @JvmOverloads constructor(
1517 context : Context ,
@@ -37,6 +39,8 @@ class TaskProgressView @JvmOverloads constructor(
3739
3840 private var head = Calendar .getInstance()
3941
42+ private val flingAnimation = FlingAnimation (this , HEAD_TRANSLATION )
43+
4044 // Global Getters
4145 private val rangeSize get() = (width - sidePadding * 2 ) / (rangeLength - 1 )
4246
@@ -50,7 +54,66 @@ class TaskProgressView @JvmOverloads constructor(
5054 interpolator = OvershootInterpolator ()
5155 }
5256
57+ // Gestures
58+ private val gestureListener = object : GestureDetector .SimpleOnGestureListener () {
59+ override fun onDown (event : MotionEvent ) = true
60+
61+ override fun onSingleTapUp (event : MotionEvent ): Boolean {
62+ for (task in tasks) {
63+ if (getTaskRect(task).contains(event.x, event.y)) {
64+ onTaskClickListener?.invoke(task)
65+ focusRange(task.startDate)
66+ return true
67+ }
68+ }
69+ return false
70+ }
71+
72+ override fun onScroll (
73+ event1 : MotionEvent ,
74+ event2 : MotionEvent ,
75+ distanceX : Float ,
76+ distanceY : Float
77+ ): Boolean {
78+ if (scrollingEnabled) {
79+ val distanceFactor = distanceX / (width - sidePadding * 2 )
80+ currentTimeInMillis + = ((viewEndTime - viewStartTime) * distanceFactor).toLong()
81+ return true
82+ }
83+ return false
84+ }
85+
86+ override fun onFling (
87+ event1 : MotionEvent ,
88+ event2 : MotionEvent ,
89+ velocityX : Float ,
90+ velocityY : Float
91+ ): Boolean {
92+ if (scrollingEnabled) {
93+ animator.cancel()
94+ val velocityFactor = velocityX / (width - sidePadding * 2 )
95+ flingAnimation.apply {
96+ cancel()
97+ setStartVelocity(- velocityFactor * (viewEndTime - viewStartTime))
98+ start()
99+ }
100+ return true
101+ }
102+ return false
103+ }
104+ }
105+
106+ private val gestureDetector = GestureDetector (context, gestureListener)
107+
53108 // Core Attributes
109+ var scrollingEnabled: Boolean = true
110+ set(value) {
111+ field = value
112+ if (! value) {
113+ flingAnimation.cancel()
114+ }
115+ }
116+
54117 var sidePadding: Float
55118 get() = _sidePadding
56119 set(value) {
@@ -101,6 +164,15 @@ class TaskProgressView @JvmOverloads constructor(
101164 invalidate()
102165 }
103166
167+ private var currentTimeInMillis: Long
168+ get() = head.timeInMillis
169+ set(value) {
170+ animator.cancel()
171+ flingAnimation.cancel()
172+ head.timeInMillis = value
173+ invalidate()
174+ }
175+
104176 // Listeners
105177 var onTaskClickListener: ((Task ) -> Unit )? = null
106178
@@ -148,6 +220,11 @@ class TaskProgressView @JvmOverloads constructor(
148220
149221 try {
150222 with (typedArray) {
223+ scrollingEnabled = getBoolean(
224+ R .styleable.TaskProgressView_scrollingEnabled ,
225+ scrollingEnabled
226+ )
227+
151228 sidePadding = getDimension(
152229 R .styleable.TaskProgressView_sidePadding ,
153230 sidePadding
@@ -268,6 +345,7 @@ class TaskProgressView @JvmOverloads constructor(
268345
269346 fun focusRange (head : Calendar ) {
270347 post {
348+ flingAnimation.cancel()
271349 animator.setFloatValues(
272350 this .head.timeInMillis.toFloat(),
273351 head.timeInMillis.toFloat()
@@ -282,18 +360,7 @@ class TaskProgressView @JvmOverloads constructor(
282360 }
283361
284362 @SuppressLint(" ClickableViewAccessibility" )
285- override fun onTouchEvent (event : MotionEvent ): Boolean {
286- if (event.action == MotionEvent .ACTION_UP && abs(event.downTime - event.eventTime) < TOUCH_EVENT_DURATION ) {
287- for (task in tasks) {
288- if (getTaskRect(task).contains(event.x, event.y)) {
289- onTaskClickListener?.invoke(task)
290- focusRange(task.startDate)
291- return true
292- }
293- }
294- }
295- return true
296- }
363+ override fun onTouchEvent (event : MotionEvent ) = gestureDetector.onTouchEvent(event)
297364
298365 override fun onAttachedToWindow () {
299366 super .onAttachedToWindow()
@@ -304,6 +371,7 @@ class TaskProgressView @JvmOverloads constructor(
304371 super .onDetachedFromWindow()
305372 animator.removeUpdateListener(animatorUpdateListener)
306373 animator.cancel()
374+ flingAnimation.cancel()
307375 }
308376
309377 private fun Canvas.drawTextCentred (text : String , cx : Float , cy : Float , paint : Paint ) {
@@ -313,13 +381,21 @@ class TaskProgressView @JvmOverloads constructor(
313381
314382 companion object {
315383 private const val DAY_MULTIPLIER = 86400000 // 1000 * 60 * 60 * 24
316- private const val TOUCH_EVENT_DURATION = 500
317384 private const val TASK_STROKE_WIDTH = 2f
318385 private const val ANIMATION_DURATION = 500L
319386 private const val TASK_LINE_WIDTH = 25f
320387
321388 private val DEFAULT_DATE_TEXT_COLOR = Color .parseColor(" #5e5e5e" )
322389 private val DEFAULT_DATE_LINE_COLOR = Color .parseColor(" #f1f1f1" )
323390 private val DEFAULT_TASK_BACKGROUND_COLOR = Color .parseColor(" #eaeaea" )
391+
392+ private val HEAD_TRANSLATION = object : FloatPropertyCompat <TaskProgressView >(" HeadTranslation" ) {
393+ override fun getValue (view : TaskProgressView ): Float = view.currentTimeInMillis.toFloat()
394+
395+ override fun setValue (view : TaskProgressView , value : Float ) {
396+ view.head.timeInMillis = value.toLong()
397+ view.invalidate()
398+ }
399+ }
324400 }
325401}
0 commit comments