From 64a06bfbe25ff311daf1dab5d7b2385717006f1b Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 5 Dec 2022 14:33:38 +0100 Subject: [PATCH] Support a swipe down to dismiss video (#2879) * Support a swipe down to dismiss video Images can already be dismissed with a swipe, this adds the same functionality to videos. - Add a VideoActionsListener interface for the hosting activity to dismiss the fragment - Add a gesture listener for swipes - Dismiss the fragment if a swipe has a greated Y component than X Fixes https://github.com/tuskyapp/Tusky/issues/2833 * Scale the video view when dragging Provides identical visual feedback to the same operation on images. --- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 +- .../tusky/fragment/ViewVideoFragment.kt | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 344ca3ac2..8c7dff594 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -52,6 +52,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment +import com.keylesspalace.tusky.fragment.ViewVideoFragment import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename @@ -68,7 +69,7 @@ import java.util.Locale typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit -class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { +class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener { private val binding by viewBinding(ActivityViewMediaBinding::inflate) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 214741a8e..28991c262 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -18,27 +18,36 @@ package com.keylesspalace.tusky.fragment import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.method.ScrollingMovementMethod +import android.view.GestureDetector import android.view.KeyEvent import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.MediaController +import androidx.core.view.GestureDetectorCompat import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView +import kotlin.math.abs class ViewVideoFragment : ViewMediaFragment() { + interface VideoActionsListener { + fun onDismiss() + } private var _binding: FragmentViewVideoBinding? = null private val binding get() = _binding!! + private lateinit var videoActionsListener: VideoActionsListener private lateinit var toolbar: View private val handler = Handler(Looper.getMainLooper()) private val hideToolbar = Runnable { @@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() { private lateinit var mediaController: MediaController private var isAudio = false + override fun onAttach(context: Context) { + super.onAttach(context) + videoActionsListener = context as VideoActionsListener + } + override fun setUserVisibleHint(isVisibleToUser: Boolean) { // Start/pause/resume video playback as fragment is shown/hidden super.setUserVisibleHint(isVisibleToUser) @@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() { return binding.root } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val attachment = arguments?.getParcelable(ARG_ATTACHMENT) @@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() { } val url = attachment.url isAudio = attachment.type == Attachment.Type.AUDIO + + val gestureDetector = GestureDetectorCompat( + requireContext(), + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(event: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (abs(velocityY) > abs(velocityX)) { + videoActionsListener.onDismiss() + return true + } + return false + } + } + ) + + var lastY = 0f + binding.root.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + lastY = event.rawY + } else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) { + val diff = event.rawY - lastY + if (binding.videoView.translationY != 0f || abs(diff) > 40) { + binding.videoView.translationY += diff + val scale = (-abs(binding.videoView.translationY) / 720 + 1).coerceAtLeast(0.5f) + binding.videoView.scaleY = scale + binding.videoView.scaleX = scale + lastY = event.rawY + return@setOnTouchListener true + } + } else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + if (abs(binding.videoView.translationY) > 180) { + videoActionsListener.onDismiss() + } else { + binding.videoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + } + } + + gestureDetector.onTouchEvent(event) + } + finalizeViewSetup(url, attachment.previewUrl, attachment.description) }