package com.github.libretube.ui.views import android.annotation.SuppressLint import android.content.Context import android.graphics.Rect import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.TransitionAdapter import com.github.libretube.R class SingleViewTouchableMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) { private val viewToDetectTouch by lazy { findViewById(R.id.main_container) ?: findViewById(R.id.audio_player_container) } private val isAudioPlayer by lazy { viewToDetectTouch.id == R.id.audio_player_container } private val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop private val viewRect = Rect() private val transitionListenerList = mutableListOf() private val swipeDownListener = mutableListOf<() -> Unit>() private val gestureDetector = GestureDetector(context, Listener()) private var startedMinimized = false private var isStrictlyDownSwipe = false private var touchInitialY = 0f private var isTouchDownInsideHitArea = false private var shouldInterceptTouchEvent = false init { super.setTransitionListener(object : TransitionAdapter() { override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) { transitionListenerList.filterNotNull() .forEach { it.onTransitionChange(p0, p1, p2, p3) } } override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { transitionListenerList.filterNotNull() .forEach { it.onTransitionCompleted(p0, p1) } } }) } override fun setTransitionListener(listener: TransitionListener?) { addTransitionListener(listener) } override fun addTransitionListener(listener: TransitionListener?) { transitionListenerList += listener } private inner class Listener : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { setTransitionDuration(200) transitionToStart() return true } override fun onScroll( e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { if (isStrictlyDownSwipe && distanceY > 0) { isStrictlyDownSwipe = false } if (isStrictlyDownSwipe && distanceY < -15F) { swipeDownListener.forEach { it.invoke() } return true } return false } } /** * Add a listener when the view is swiped down while the current transition's state is in * end state (minimized state) */ fun addSwipeDownListener(listener: () -> Unit) = apply { swipeDownListener.add(listener) } override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { shouldInterceptTouchEvent = false isTouchDownInsideHitArea = false // never intercept touch event if we're currently in audio player mode if (isAudioPlayer) return false viewToDetectTouch.getHitRect(viewRect) startedMinimized = progress == 1F // When minimized, strictly limit touch area to prevent intercepting clicks behind the mini-player if (startedMinimized) { // Only allow touches within the bottom 200px height of the screen (mini-player area) val screenHeight = resources.displayMetrics.heightPixels val miniPlayerThreshold = screenHeight - 200 isTouchDownInsideHitArea = event.y > miniPlayerThreshold && viewRect.contains(event.x.toInt(), event.y.toInt()) } else { // When expanded, use full hit rect isTouchDownInsideHitArea = viewRect.contains(event.x.toInt(), event.y.toInt()) } touchInitialY = event.y isStrictlyDownSwipe = true } MotionEvent.ACTION_MOVE -> { if (!startedMinimized && !shouldInterceptTouchEvent && isTouchDownInsideHitArea) { val deltaY = event.y - touchInitialY // swipe down detected if (deltaY > scaledTouchSlop) { // start intercepting and consume the event ourselves in onTouchEvent() shouldInterceptTouchEvent = true // inject down MotionEvent from current position to properly trigger // motion scene's swipe action MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_DOWN setLocation(event.x, event.y) }.also { downEvent -> onTouchEvent(downEvent) downEvent.recycle() } } } } } return shouldInterceptTouchEvent } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { if (isTouchDownInsideHitArea && startedMinimized) { // detect gesture only when the player is in minimized state gestureDetector.onTouchEvent(event) } return isTouchDownInsideHitArea && super.onTouchEvent(event) } }