diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt index b04402abc..edaee6765 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt @@ -2,6 +2,7 @@ package io.casey.musikcube.remote.ui.shared.fragment import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.TextView @@ -14,12 +15,17 @@ import io.casey.musikcube.remote.ui.shared.extension.fallback import io.casey.musikcube.remote.ui.shared.extension.getColorCompat import io.casey.musikcube.remote.ui.shared.extension.topOfStack import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin +import io.casey.musikcube.remote.ui.shared.view.InterceptTouchFrameLayout +import me.zhanghai.android.materialprogressbar.MaterialProgressBar class TransportFragment: BaseFragment() { private lateinit var rootView: View private lateinit var buffering: View private lateinit var title: TextView private lateinit var playPause: TextView + private lateinit var progress: MaterialProgressBar + private val seekTracker = TouchTracker() + private var seekOverride = -1 lateinit var playback: PlaybackMixin private set @@ -30,6 +36,7 @@ class TransportFragment: BaseFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { this.rootView = inflater.inflate(R.layout.transport_fragment, container, false) + progress = this.rootView.findViewById(R.id.progress) bindEventHandlers() rebindUi() return this.rootView @@ -43,6 +50,12 @@ class TransportFragment: BaseFragment() { override fun onResume() { super.onResume() rebindUi() + scheduleUpdateTime() + } + + override fun onPause() { + super.onPause() + handler.removeCallbacks(updateTimeRunnable) } private fun bindEventHandlers() { @@ -52,10 +65,10 @@ class TransportFragment: BaseFragment() { this.buffering = this.rootView.findViewById(R.id.buffering) - val titleBar = this.rootView.findViewById(R.id.title_bar) + val titleBar = this.rootView.findViewById(R.id.title_bar) titleBar?.setOnClickListener { - if (playback.service.state != PlaybackState.Stopped) { + if (!seekTracker.processed && playback.service.state != PlaybackState.Stopped) { appCompatActivity.supportFragmentManager.run { when (topOfStack != PlayQueueFragment.TAG) { true -> Navigate.toPlayQueue( @@ -70,13 +83,51 @@ class TransportFragment: BaseFragment() { } titleBar?.setOnLongClickListener { - activity?.let { a -> - startActivity(MainActivity.getStartIntent(a)) - return@setOnLongClickListener true + if (!seekTracker.processed) { + activity?.let { a -> + startActivity(MainActivity.getStartIntent(a)) + return@setOnLongClickListener true + } } false } + titleBar?.setOnInterceptTouchEventListener(object: InterceptTouchFrameLayout.OnInterceptTouchEventListener { + override fun onInterceptTouchEvent(view: InterceptTouchFrameLayout, ev: MotionEvent, disallowIntercept: Boolean): Boolean { + return when (ev.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_CANCEL, + MotionEvent.ACTION_UP -> true + else -> false + } + } + + override fun onTouchEvent(view: InterceptTouchFrameLayout, ev: MotionEvent): Boolean { + seekTracker.update(ev) + if (seekTracker.processed) { + when (ev.action) { + MotionEvent.ACTION_MOVE -> { + seekOverride = ( + seekTracker.lastX.toFloat() / + view.width.toFloat() * + playback.service.duration.toFloat()).toInt() + rebindProgress() + } + MotionEvent.ACTION_UP -> { + if (seekOverride >= 0) { + playback.service.seekTo(seekOverride.toDouble()) + seekOverride = -1 + } + } + } + } + view.defaultOnTouchEvent(ev) + return true + } + }) + + this.rootView.findViewById(R.id.button_prev)?.setOnClickListener { playback.service.prev() } @@ -97,6 +148,27 @@ class TransportFragment: BaseFragment() { } } + private fun rebindProgress() { + if (playback.service.state == PlaybackState.Stopped) { + progress.progress = 0 + progress.secondaryProgress = 0 + } + else { + val buffered = playback.service.bufferedTime.toInt() + val total = playback.service.duration.toInt() + progress.max = total + progress.secondaryProgress = if (buffered >= 100) 0 else buffered + + progress.progress = if (seekTracker.down && seekOverride >= 0) { + seekOverride + } + else { + playback.service.currentTime.toInt() + } + } + } + + private fun rebindUi() { val state = playback.service.state @@ -115,6 +187,18 @@ class TransportFragment: BaseFragment() { title.text = fallback(playback.service.playingTrack.title, defaultValue) title.setTextColor(getColorCompat(R.color.theme_green)) } + + rebindProgress() + } + + private fun scheduleUpdateTime() { + handler.removeCallbacks(updateTimeRunnable) + handler.postDelayed(updateTimeRunnable, 1000L) + } + + private val updateTimeRunnable = Runnable { + rebindProgress() + scheduleUpdateTime() } private val playbackListener: () -> Unit = { @@ -122,6 +206,56 @@ class TransportFragment: BaseFragment() { modelChangedListener?.invoke(this@TransportFragment) } + private class TouchTracker { + var down = false + var startX = 0 + var startY = 0 + var totalDx = 0 + var totalDy = 0 + var lastX = 0 + var lastY = 0 + + fun reset() { + down = false + startX = 0 + startY = 0 + totalDx = 0 + totalDy = 0 + lastX = 0 + lastY = 0 + } + + fun update(ev: MotionEvent) { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + reset() + down = true + startX = ev.x.toInt() + startY = ev.y.toInt() + lastX = startX + lastY = startY + } + + MotionEvent.ACTION_MOVE -> { + val x = ev.x.toInt() + val y = ev.y.toInt() + totalDx += Math.abs(lastX - x) + totalDy += Math.abs(lastY - y) + lastX = x + lastY = y + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + down = false + } + } + } + + val processed: Boolean + get() { return totalDx >= 24 } + } + companion object { const val TAG = "TransportFragment" fun create(): TransportFragment = TransportFragment() diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/view/TouchInterceptFrameLayout.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/view/TouchInterceptFrameLayout.kt new file mode 100644 index 000000000..e9a94e29d --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/view/TouchInterceptFrameLayout.kt @@ -0,0 +1,51 @@ +package io.casey.musikcube.remote.ui.shared.view + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.FrameLayout + +class InterceptTouchFrameLayout : FrameLayout { + private var disallowIntercept: Boolean = false + private var interceptor: OnInterceptTouchEventListener = DEFAULT_INTERCEPTOR + + interface OnInterceptTouchEventListener { + fun onInterceptTouchEvent(view: InterceptTouchFrameLayout, ev: MotionEvent, disallowIntercept: Boolean): Boolean + fun onTouchEvent(view: InterceptTouchFrameLayout, event: MotionEvent): Boolean + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept) + this.disallowIntercept = disallowIntercept + } + + fun setOnInterceptTouchEventListener(interceptor: OnInterceptTouchEventListener?) { + this.interceptor = interceptor ?: DEFAULT_INTERCEPTOR + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + val stealTouchEvent = interceptor.onInterceptTouchEvent(this, ev, disallowIntercept) + return stealTouchEvent && !disallowIntercept || super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + val handled = interceptor.onTouchEvent(this, ev) + return handled || super.onTouchEvent(ev) + } + + fun defaultOnInterceptTouchEvent(ev: MotionEvent): Boolean = super.onInterceptTouchEvent(ev) + fun defaultOnTouchEvent(ev: MotionEvent): Boolean = super.onTouchEvent(ev) + + companion object { + private val DEFAULT_INTERCEPTOR = object: OnInterceptTouchEventListener { + override fun onInterceptTouchEvent(view: InterceptTouchFrameLayout, ev: MotionEvent, disallowIntercept: Boolean): Boolean = false + override fun onTouchEvent(view: InterceptTouchFrameLayout, event: MotionEvent): Boolean = false + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/layout/transport_fragment.xml b/src/musikdroid/app/src/main/res/layout/transport_fragment.xml index 86e897437..ca934dec4 100644 --- a/src/musikdroid/app/src/main/res/layout/transport_fragment.xml +++ b/src/musikdroid/app/src/main/res/layout/transport_fragment.xml @@ -52,11 +52,11 @@ android:layout_width="0dp" android:layout_height="2dp"/> - @@ -105,6 +105,6 @@ - + \ No newline at end of file