|
|
|
|
@ -19,33 +19,60 @@ import android.animation.Animator
|
|
|
|
|
import android.animation.AnimatorListenerAdapter |
|
|
|
|
import android.annotation.SuppressLint |
|
|
|
|
import android.content.Context |
|
|
|
|
import android.graphics.drawable.Drawable |
|
|
|
|
import android.os.Build |
|
|
|
|
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.Gravity |
|
|
|
|
import android.view.LayoutInflater |
|
|
|
|
import android.view.MotionEvent |
|
|
|
|
import android.view.View |
|
|
|
|
import android.view.ViewGroup |
|
|
|
|
import android.widget.MediaController |
|
|
|
|
import android.widget.FrameLayout |
|
|
|
|
import android.widget.LinearLayout |
|
|
|
|
import androidx.annotation.OptIn |
|
|
|
|
import androidx.core.view.GestureDetectorCompat |
|
|
|
|
import androidx.media3.common.MediaItem |
|
|
|
|
import androidx.media3.common.PlaybackException |
|
|
|
|
import androidx.media3.common.Player |
|
|
|
|
import androidx.media3.common.util.UnstableApi |
|
|
|
|
import androidx.media3.datasource.DefaultDataSource |
|
|
|
|
import androidx.media3.datasource.okhttp.OkHttpDataSource |
|
|
|
|
import androidx.media3.exoplayer.ExoPlayer |
|
|
|
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory |
|
|
|
|
import androidx.media3.exoplayer.util.EventLogger |
|
|
|
|
import androidx.media3.ui.AspectRatioFrameLayout |
|
|
|
|
import androidx.media3.ui.PlayerControlView |
|
|
|
|
import com.bumptech.glide.Glide |
|
|
|
|
import com.bumptech.glide.request.target.CustomTarget |
|
|
|
|
import com.bumptech.glide.request.transition.Transition |
|
|
|
|
import com.google.android.material.snackbar.Snackbar |
|
|
|
|
import com.keylesspalace.tusky.BuildConfig |
|
|
|
|
import com.keylesspalace.tusky.R |
|
|
|
|
import com.keylesspalace.tusky.ViewMediaActivity |
|
|
|
|
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding |
|
|
|
|
import com.keylesspalace.tusky.di.Injectable |
|
|
|
|
import com.keylesspalace.tusky.entity.Attachment |
|
|
|
|
import com.keylesspalace.tusky.util.hide |
|
|
|
|
import com.keylesspalace.tusky.util.viewBinding |
|
|
|
|
import com.keylesspalace.tusky.util.visible |
|
|
|
|
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView |
|
|
|
|
import okhttp3.OkHttpClient |
|
|
|
|
import javax.inject.Inject |
|
|
|
|
import kotlin.math.abs |
|
|
|
|
|
|
|
|
|
class ViewVideoFragment : ViewMediaFragment() { |
|
|
|
|
@UnstableApi |
|
|
|
|
class ViewVideoFragment : ViewMediaFragment(), Injectable { |
|
|
|
|
interface VideoActionsListener { |
|
|
|
|
fun onDismiss() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private var _binding: FragmentViewVideoBinding? = null |
|
|
|
|
private val binding get() = _binding!! |
|
|
|
|
@Inject |
|
|
|
|
lateinit var okHttpClient: OkHttpClient |
|
|
|
|
|
|
|
|
|
private val binding by viewBinding(FragmentViewVideoBinding::bind) |
|
|
|
|
|
|
|
|
|
private lateinit var videoActionsListener: VideoActionsListener |
|
|
|
|
private lateinit var toolbar: View |
|
|
|
|
@ -54,204 +81,298 @@ class ViewVideoFragment : ViewMediaFragment() {
|
|
|
|
|
// Hoist toolbar hiding to activity so it can track state across different fragments |
|
|
|
|
// This is explicitly stored as runnable so that we pass it to the handler later for cancellation |
|
|
|
|
mediaActivity.onPhotoTap() |
|
|
|
|
mediaController.hide() |
|
|
|
|
} |
|
|
|
|
private lateinit var mediaActivity: ViewMediaActivity |
|
|
|
|
private lateinit var mediaController: MediaController |
|
|
|
|
private lateinit var mediaPlayerListener: Player.Listener |
|
|
|
|
private var isAudio = false |
|
|
|
|
|
|
|
|
|
companion object { |
|
|
|
|
private const val TOOLBAR_HIDE_DELAY_MS = 3000L |
|
|
|
|
} |
|
|
|
|
private lateinit var mediaAttachment: Attachment |
|
|
|
|
|
|
|
|
|
private var player: ExoPlayer? = null |
|
|
|
|
|
|
|
|
|
/** The saved seek position, if the fragment is being resumed */ |
|
|
|
|
private var savedSeekPosition: Long = 0 |
|
|
|
|
|
|
|
|
|
private lateinit var mediaSourceFactory: DefaultMediaSourceFactory |
|
|
|
|
|
|
|
|
|
override fun onAttach(context: Context) { |
|
|
|
|
super.onAttach(context) |
|
|
|
|
videoActionsListener = context as VideoActionsListener |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onResume() { |
|
|
|
|
super.onResume() |
|
|
|
|
mediaSourceFactory = DefaultMediaSourceFactory(context) |
|
|
|
|
.setDataSourceFactory(DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient))) |
|
|
|
|
|
|
|
|
|
if (_binding != null) { |
|
|
|
|
if (mediaActivity.isToolbarVisible && !isAudio) { |
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) |
|
|
|
|
} |
|
|
|
|
binding.videoView.start() |
|
|
|
|
} |
|
|
|
|
videoActionsListener = context as VideoActionsListener |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onPause() { |
|
|
|
|
super.onPause() |
|
|
|
|
@SuppressLint("PrivateResource", "MissingInflatedId") |
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { |
|
|
|
|
mediaActivity = activity as ViewMediaActivity |
|
|
|
|
toolbar = mediaActivity.toolbar |
|
|
|
|
val rootView = inflater.inflate(R.layout.fragment_view_video, container, false) |
|
|
|
|
|
|
|
|
|
if (_binding != null) { |
|
|
|
|
handler.removeCallbacks(hideToolbar) |
|
|
|
|
binding.videoView.pause() |
|
|
|
|
mediaController.hide() |
|
|
|
|
} |
|
|
|
|
// Move the controls to the bottom of the screen, with enough bottom margin to clear the seekbar |
|
|
|
|
val controls = rootView.findViewById<LinearLayout>(androidx.media3.ui.R.id.exo_center_controls) |
|
|
|
|
val layoutParams = controls.layoutParams as FrameLayout.LayoutParams |
|
|
|
|
layoutParams.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM |
|
|
|
|
layoutParams.bottomMargin = rootView.context.resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_bottom_bar_height) |
|
|
|
|
.toInt() |
|
|
|
|
controls.layoutParams = layoutParams |
|
|
|
|
|
|
|
|
|
return rootView |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility") |
|
|
|
|
override fun setupMediaView( |
|
|
|
|
url: String, |
|
|
|
|
previewUrl: String?, |
|
|
|
|
description: String?, |
|
|
|
|
showingDescription: Boolean |
|
|
|
|
) { |
|
|
|
|
binding.mediaDescription.text = description |
|
|
|
|
binding.mediaDescription.visible(showingDescription) |
|
|
|
|
binding.mediaDescription.movementMethod = ScrollingMovementMethod() |
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|
|
|
|
super.onViewCreated(view, savedInstanceState) |
|
|
|
|
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT) |
|
|
|
|
?: throw IllegalArgumentException("attachment has to be set") |
|
|
|
|
|
|
|
|
|
// Ensure the description is visible over the video |
|
|
|
|
binding.mediaDescription.elevation = binding.videoView.elevation + 1 |
|
|
|
|
val url = attachment.url |
|
|
|
|
isAudio = attachment.type == Attachment.Type.AUDIO |
|
|
|
|
|
|
|
|
|
binding.videoView.transitionName = url |
|
|
|
|
binding.videoView.setVideoPath(url) |
|
|
|
|
mediaController = object : MediaController(mediaActivity) { |
|
|
|
|
override fun show(timeout: Int) { |
|
|
|
|
// We're doing manual auto-close management. |
|
|
|
|
// Also, take focus back from the pause button so we can use the back button. |
|
|
|
|
super.show(0) |
|
|
|
|
mediaController.requestFocus() |
|
|
|
|
} |
|
|
|
|
/** |
|
|
|
|
* Handle single taps, flings, and dragging |
|
|
|
|
*/ |
|
|
|
|
val touchListener = object : View.OnTouchListener { |
|
|
|
|
var lastY = 0f |
|
|
|
|
|
|
|
|
|
/** The view that contains the playing content */ |
|
|
|
|
// binding.videoView is fullscreen, and includes the controls, so don't use that |
|
|
|
|
// when scaling in response to the user dragging on the screen |
|
|
|
|
val contentFrame = binding.videoView.findViewById<AspectRatioFrameLayout>(androidx.media3.ui.R.id.exo_content_frame) |
|
|
|
|
|
|
|
|
|
/** Handle taps and flings */ |
|
|
|
|
val simpleGestureDetector = GestureDetectorCompat( |
|
|
|
|
requireContext(), |
|
|
|
|
object : GestureDetector.SimpleOnGestureListener() { |
|
|
|
|
override fun onDown(e: MotionEvent) = true |
|
|
|
|
|
|
|
|
|
/** A single tap should show/hide the media description */ |
|
|
|
|
override fun onSingleTapUp(e: MotionEvent): Boolean { |
|
|
|
|
mediaActivity.onPhotoTap() |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** A fling up/down should dismiss the fragment */ |
|
|
|
|
override fun onFling( |
|
|
|
|
e1: MotionEvent, |
|
|
|
|
e2: MotionEvent, |
|
|
|
|
velocityX: Float, |
|
|
|
|
velocityY: Float |
|
|
|
|
): Boolean { |
|
|
|
|
if (abs(velocityY) > abs(velocityX)) { |
|
|
|
|
videoActionsListener.onDismiss() |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
override fun dispatchKeyEvent(event: KeyEvent?): Boolean { |
|
|
|
|
if (event?.keyCode == KeyEvent.KEYCODE_BACK) { |
|
|
|
|
if (event.action == KeyEvent.ACTION_UP) { |
|
|
|
|
hide() |
|
|
|
|
activity?.supportFinishAfterTransition() |
|
|
|
|
@SuppressLint("ClickableViewAccessibility") |
|
|
|
|
override fun onTouch(v: View?, event: MotionEvent): Boolean { |
|
|
|
|
// Track movement, and scale / translate the video display accordingly |
|
|
|
|
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 (contentFrame.translationY != 0f || abs(diff) > 40) { |
|
|
|
|
contentFrame.translationY += diff |
|
|
|
|
val scale = (-abs(contentFrame.translationY) / 720 + 1).coerceAtLeast(0.5f) |
|
|
|
|
contentFrame.scaleY = scale |
|
|
|
|
contentFrame.scaleX = scale |
|
|
|
|
lastY = event.rawY |
|
|
|
|
} |
|
|
|
|
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { |
|
|
|
|
if (abs(contentFrame.translationY) > 180) { |
|
|
|
|
videoActionsListener.onDismiss() |
|
|
|
|
} else { |
|
|
|
|
contentFrame.animate().translationY(0f).scaleX(1f).scaleY(1f).start() |
|
|
|
|
} |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
return super.dispatchKeyEvent(event) |
|
|
|
|
|
|
|
|
|
simpleGestureDetector.onTouchEvent(event) |
|
|
|
|
|
|
|
|
|
// Allow the player's normal onTouch handler to run as well (e.g., to show the |
|
|
|
|
// player controls on tap) |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
mediaController.setMediaPlayer(binding.videoView) |
|
|
|
|
binding.videoView.setMediaController(mediaController) |
|
|
|
|
binding.videoView.requestFocus() |
|
|
|
|
binding.videoView.setPlayPauseListener(object : ExposedPlayPauseVideoView.PlayPauseListener { |
|
|
|
|
override fun onPlay() { |
|
|
|
|
if (!isAudio) { |
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) |
|
|
|
|
mediaPlayerListener = object : Player.Listener { |
|
|
|
|
@SuppressLint("ClickableViewAccessibility", "SyntheticAccessor") |
|
|
|
|
@OptIn(UnstableApi::class) |
|
|
|
|
override fun onPlaybackStateChanged(playbackState: Int) { |
|
|
|
|
when (playbackState) { |
|
|
|
|
Player.STATE_READY -> { |
|
|
|
|
// Wait until the media is loaded before accepting taps as we don't want toolbar to |
|
|
|
|
// be hidden until then. |
|
|
|
|
binding.videoView.setOnTouchListener(touchListener) |
|
|
|
|
|
|
|
|
|
binding.progressBar.hide() |
|
|
|
|
binding.videoView.useController = true |
|
|
|
|
binding.videoView.showController() |
|
|
|
|
} |
|
|
|
|
else -> { /* do nothing */ } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onPause() { |
|
|
|
|
if (!isAudio) { |
|
|
|
|
override fun onIsPlayingChanged(isPlaying: Boolean) { |
|
|
|
|
if (isAudio) return |
|
|
|
|
if (isPlaying) { |
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) |
|
|
|
|
} else { |
|
|
|
|
handler.removeCallbacks(hideToolbar) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
binding.videoView.setOnPreparedListener { mp -> |
|
|
|
|
val containerWidth = binding.videoContainer.measuredWidth.toFloat() |
|
|
|
|
val containerHeight = binding.videoContainer.measuredHeight.toFloat() |
|
|
|
|
val videoWidth = mp.videoWidth.toFloat() |
|
|
|
|
val videoHeight = mp.videoHeight.toFloat() |
|
|
|
|
|
|
|
|
|
if (isAudio) { |
|
|
|
|
binding.videoView.layoutParams.height = 1 |
|
|
|
|
binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT |
|
|
|
|
} else if (containerWidth / containerHeight > videoWidth / videoHeight) { |
|
|
|
|
binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT |
|
|
|
|
binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT |
|
|
|
|
} else { |
|
|
|
|
binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT |
|
|
|
|
binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Wait until the media is loaded before accepting taps as we don't want toolbar to |
|
|
|
|
// be hidden until then. |
|
|
|
|
binding.videoView.setOnTouchListener { _, _ -> |
|
|
|
|
mediaActivity.onPhotoTap() |
|
|
|
|
false |
|
|
|
|
@SuppressLint("SyntheticAccessor") |
|
|
|
|
override fun onPlayerError(error: PlaybackException) { |
|
|
|
|
binding.progressBar.hide() |
|
|
|
|
val message = getString( |
|
|
|
|
R.string.error_media_playback, |
|
|
|
|
error.cause?.message ?: error.message |
|
|
|
|
) |
|
|
|
|
Snackbar.make(binding.root, message, Snackbar.LENGTH_INDEFINITE) |
|
|
|
|
.setTextMaxLines(10) |
|
|
|
|
.setAction(R.string.action_retry) { player?.prepare() } |
|
|
|
|
.show() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Audio doesn't cause the controller to show automatically |
|
|
|
|
if (isAudio) { |
|
|
|
|
mediaController.show() |
|
|
|
|
} |
|
|
|
|
savedSeekPosition = savedInstanceState?.getLong(SEEK_POSITION) ?: 0 |
|
|
|
|
|
|
|
|
|
mediaAttachment = attachment |
|
|
|
|
|
|
|
|
|
finalizeViewSetup(url, attachment.previewUrl, attachment.description) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
binding.progressBar.hide() |
|
|
|
|
mp.isLooping = true |
|
|
|
|
override fun onStart() { |
|
|
|
|
super.onStart() |
|
|
|
|
if (Build.VERSION.SDK_INT > 23) { |
|
|
|
|
initializePlayer() |
|
|
|
|
binding.videoView.onResume() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { |
|
|
|
|
mediaActivity.onBringUp() |
|
|
|
|
override fun onResume() { |
|
|
|
|
super.onResume() |
|
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT <= 23 || player == null) { |
|
|
|
|
initializePlayer() |
|
|
|
|
if (mediaActivity.isToolbarVisible && !isAudio) { |
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) |
|
|
|
|
} |
|
|
|
|
binding.videoView.onResume() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun hideToolbarAfterDelay(delayMilliseconds: Long) { |
|
|
|
|
handler.postDelayed(hideToolbar, delayMilliseconds) |
|
|
|
|
private fun releasePlayer() { |
|
|
|
|
player?.let { |
|
|
|
|
savedSeekPosition = it.currentPosition |
|
|
|
|
it.release() |
|
|
|
|
player = null |
|
|
|
|
binding.videoView.player = null |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { |
|
|
|
|
mediaActivity = activity as ViewMediaActivity |
|
|
|
|
toolbar = mediaActivity.toolbar |
|
|
|
|
_binding = FragmentViewVideoBinding.inflate(inflater, container, false) |
|
|
|
|
return binding.root |
|
|
|
|
override fun onPause() { |
|
|
|
|
super.onPause() |
|
|
|
|
|
|
|
|
|
// If <= API 23 then multi-window mode is not available, so this is a good time to |
|
|
|
|
// pause everything |
|
|
|
|
if (Build.VERSION.SDK_INT <= 23) { |
|
|
|
|
binding.videoView.onPause() |
|
|
|
|
releasePlayer() |
|
|
|
|
handler.removeCallbacks(hideToolbar) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility") |
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|
|
|
|
super.onViewCreated(view, savedInstanceState) |
|
|
|
|
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT) |
|
|
|
|
?: throw IllegalArgumentException("attachment has to be set") |
|
|
|
|
override fun onStop() { |
|
|
|
|
super.onStop() |
|
|
|
|
|
|
|
|
|
val url = attachment.url |
|
|
|
|
isAudio = attachment.type == Attachment.Type.AUDIO |
|
|
|
|
// If > API 23 then this might be multi-window, and definitely wasn't paused in onPause, |
|
|
|
|
// so pause everything now. |
|
|
|
|
if (Build.VERSION.SDK_INT > 23) { |
|
|
|
|
binding.videoView.onPause() |
|
|
|
|
releasePlayer() |
|
|
|
|
handler.removeCallbacks(hideToolbar) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
val gestureDetector = GestureDetectorCompat( |
|
|
|
|
requireContext(), |
|
|
|
|
object : GestureDetector.SimpleOnGestureListener() { |
|
|
|
|
override fun onDown(event: MotionEvent): Boolean { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) { |
|
|
|
|
super.onSaveInstanceState(outState) |
|
|
|
|
outState.putLong(SEEK_POSITION, savedSeekPosition) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onFling( |
|
|
|
|
e1: MotionEvent, |
|
|
|
|
e2: MotionEvent, |
|
|
|
|
velocityX: Float, |
|
|
|
|
velocityY: Float |
|
|
|
|
): Boolean { |
|
|
|
|
if (abs(velocityY) > abs(velocityX)) { |
|
|
|
|
videoActionsListener.onDismiss() |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
private fun initializePlayer() { |
|
|
|
|
ExoPlayer.Builder(requireContext()) |
|
|
|
|
.setMediaSourceFactory(mediaSourceFactory) |
|
|
|
|
.build().apply { |
|
|
|
|
if (BuildConfig.DEBUG) addAnalyticsListener(EventLogger("$TAG:ExoPlayer")) |
|
|
|
|
setMediaItem(MediaItem.fromUri(mediaAttachment.url)) |
|
|
|
|
addListener(mediaPlayerListener) |
|
|
|
|
repeatMode = Player.REPEAT_MODE_ONE |
|
|
|
|
playWhenReady = true |
|
|
|
|
seekTo(savedSeekPosition) |
|
|
|
|
prepare() |
|
|
|
|
player = this |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
binding.videoView.player = player |
|
|
|
|
|
|
|
|
|
// Audio-only files might have a preview image. If they do, set it as the artwork |
|
|
|
|
if (isAudio) { |
|
|
|
|
mediaAttachment.previewUrl?.let { url -> |
|
|
|
|
Glide.with(this).load(url).into(object : CustomTarget<Drawable>() { |
|
|
|
|
@SuppressLint("SyntheticAccessor") |
|
|
|
|
override fun onResourceReady( |
|
|
|
|
resource: Drawable, |
|
|
|
|
transition: Transition<in Drawable>? |
|
|
|
|
) { |
|
|
|
|
view ?: return |
|
|
|
|
binding.videoView.defaultArtwork = resource |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressLint("SyntheticAccessor") |
|
|
|
|
override fun onLoadCleared(placeholder: Drawable?) { |
|
|
|
|
view ?: return |
|
|
|
|
binding.videoView.defaultArtwork = null |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility") |
|
|
|
|
override fun setupMediaView( |
|
|
|
|
url: String, |
|
|
|
|
previewUrl: String?, |
|
|
|
|
description: String?, |
|
|
|
|
showingDescription: Boolean |
|
|
|
|
) { |
|
|
|
|
binding.mediaDescription.text = description |
|
|
|
|
binding.mediaDescription.visible(showingDescription) |
|
|
|
|
binding.mediaDescription.movementMethod = ScrollingMovementMethod() |
|
|
|
|
|
|
|
|
|
// Ensure the description is visible over the video |
|
|
|
|
binding.mediaDescription.elevation = binding.videoView.elevation + 1 |
|
|
|
|
|
|
|
|
|
binding.videoView.transitionName = url |
|
|
|
|
|
|
|
|
|
binding.videoView.requestFocus() |
|
|
|
|
|
|
|
|
|
gestureDetector.onTouchEvent(event) |
|
|
|
|
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { |
|
|
|
|
mediaActivity.onBringUp() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
finalizeViewSetup(url, attachment.previewUrl, attachment.description) |
|
|
|
|
private fun hideToolbarAfterDelay(delayMilliseconds: Int) { |
|
|
|
|
handler.postDelayed(hideToolbar, delayMilliseconds.toLong()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onToolbarVisibilityChange(visible: Boolean) { |
|
|
|
|
if (_binding == null || !userVisibleHint) { |
|
|
|
|
if (!userVisibleHint) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -265,27 +386,27 @@ class ViewVideoFragment : ViewMediaFragment() {
|
|
|
|
|
|
|
|
|
|
binding.mediaDescription.animate().alpha(alpha) |
|
|
|
|
.setListener(object : AnimatorListenerAdapter() { |
|
|
|
|
@SuppressLint("SyntheticAccessor") |
|
|
|
|
override fun onAnimationEnd(animation: Animator) { |
|
|
|
|
if (_binding != null) { |
|
|
|
|
binding.mediaDescription.visible(isDescriptionVisible) |
|
|
|
|
} |
|
|
|
|
view ?: return |
|
|
|
|
binding.mediaDescription.visible(isDescriptionVisible) |
|
|
|
|
animation.removeListener(this) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.start() |
|
|
|
|
|
|
|
|
|
if (visible && binding.videoView.isPlaying && !isAudio) { |
|
|
|
|
if (visible && (binding.videoView.player?.isPlaying == true) && !isAudio) { |
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) |
|
|
|
|
} else { |
|
|
|
|
handler.removeCallbacks(hideToolbar) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override fun onTransitionEnd() { |
|
|
|
|
} |
|
|
|
|
override fun onTransitionEnd() { } |
|
|
|
|
|
|
|
|
|
override fun onDestroyView() { |
|
|
|
|
super.onDestroyView() |
|
|
|
|
_binding = null |
|
|
|
|
companion object { |
|
|
|
|
private const val TAG = "ViewVideoFragment" |
|
|
|
|
private const val TOOLBAR_HIDE_DELAY_MS = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS |
|
|
|
|
private const val SEEK_POSITION = "seekPosition" |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|