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.
This commit is contained in:
Nik Clayton 2022-12-05 14:33:38 +01:00 committed by GitHub
parent 4de778d7d4
commit 64a06bfbe2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 1 deletions

View File

@ -52,6 +52,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@ -68,7 +69,7 @@ import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
private val binding by viewBinding(ActivityViewMediaBinding::inflate) private val binding by viewBinding(ActivityViewMediaBinding::inflate)

View File

@ -18,27 +18,36 @@ package com.keylesspalace.tusky.fragment
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.GestureDetector
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import androidx.core.view.GestureDetectorCompat
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlin.math.abs
class ViewVideoFragment : ViewMediaFragment() { class ViewVideoFragment : ViewMediaFragment() {
interface VideoActionsListener {
fun onDismiss()
}
private var _binding: FragmentViewVideoBinding? = null private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var videoActionsListener: VideoActionsListener
private lateinit var toolbar: View private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable { private val hideToolbar = Runnable {
@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() {
private lateinit var mediaController: MediaController private lateinit var mediaController: MediaController
private var isAudio = false private var isAudio = false
override fun onAttach(context: Context) {
super.onAttach(context)
videoActionsListener = context as VideoActionsListener
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) { override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden // Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser) super.setUserVisibleHint(isVisibleToUser)
@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() {
return binding.root return binding.root
} }
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT) val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
val url = attachment.url val url = attachment.url
isAudio = attachment.type == Attachment.Type.AUDIO 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) finalizeViewSetup(url, attachment.previewUrl, attachment.description)
} }