refactor: Restructure ViewMediaActivity and related fragments (#489)
Highlights: - Implement fragment transitions for video to improve the UX, video won't start playing until the transition completes - Remove rxJava - Move duplicate code in to base classes Details: `MediaActionsListener`: - Move to `ViewMediaFragment` as it's used by both subclasses - Remove need for separate `VideoActionsListener` - Rename methods to better reflect their purpose and improve readability `ViewMediaFragment`: - Move duplicated code from `ViewImageFragment` and `ViewVideoFragment` - Rewrite code that handles fragment transitions to use a `CompleteableDeferred` instead of `BehaviorSubject` (removes rxJava). - Rename methods and properties to better reflect their purpose and improve readability - Add extra comments `ViewImageFragment`: - Rewrite code that handles fragment transitions to use a `CompleteableDeferred` instead of `BehaviorSubject` (removes rxJava). `ViewVideoFragment`: - Implement fragment transitions for video to improve the UX, video won't start playing until the transition completes - Manage toolbar visibility with a coroutine instead of a handler - Add extra comments `ViewMediaActivity`: - Rename properties to better reflect their purpose and improve readability - Add extra comments `ImagePagerAdapter`: - Rename properties to better reflect their purpose and improve readability - Add extra comments
This commit is contained in:
parent
72ef8cfe4a
commit
1026fccc40
|
@ -51,8 +51,7 @@ import app.pachli.core.navigation.AttachmentViewData
|
||||||
import app.pachli.core.navigation.ViewMediaActivityIntent
|
import app.pachli.core.navigation.ViewMediaActivityIntent
|
||||||
import app.pachli.core.navigation.ViewThreadActivityIntent
|
import app.pachli.core.navigation.ViewThreadActivityIntent
|
||||||
import app.pachli.databinding.ActivityViewMediaBinding
|
import app.pachli.databinding.ActivityViewMediaBinding
|
||||||
import app.pachli.fragment.ViewImageFragment
|
import app.pachli.fragment.MediaActionsListener
|
||||||
import app.pachli.fragment.ViewVideoFragment
|
|
||||||
import app.pachli.pager.ImagePagerAdapter
|
import app.pachli.pager.ImagePagerAdapter
|
||||||
import app.pachli.pager.SingleImagePagerAdapter
|
import app.pachli.pager.SingleImagePagerAdapter
|
||||||
import app.pachli.util.getTemporaryMediaFilename
|
import app.pachli.util.getTemporaryMediaFilename
|
||||||
|
@ -76,7 +75,7 @@ typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||||
* Show one or more media items (pictures, video, audio, etc).
|
* Show one or more media items (pictures, video, audio, etc).
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
|
class ViewMediaActivity : BaseActivity(), MediaActionsListener {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
|
@ -88,15 +87,21 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
var isToolbarVisible = true
|
var isToolbarVisible = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var attachments: List<AttachmentViewData>? = null
|
private var attachmentViewData: List<AttachmentViewData>? = null
|
||||||
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
|
||||||
private var imageUrl: String? = null
|
private var imageUrl: String? = null
|
||||||
|
|
||||||
/** True if a download to share media is in progress */
|
/** True if a download to share media is in progress */
|
||||||
private var isDownloading: Boolean = false
|
private var isDownloading: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds [listener] to the list of toolbar listeners and immediately calls
|
||||||
|
* it with the current toolbar visibility.
|
||||||
|
*
|
||||||
|
* @return A function that must be called to remove the listener.
|
||||||
|
*/
|
||||||
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
|
||||||
this.toolbarVisibilityListeners.add(listener)
|
toolbarVisibilityListeners.add(listener)
|
||||||
listener(isToolbarVisible)
|
listener(isToolbarVisible)
|
||||||
return { toolbarVisibilityListeners.remove(listener) }
|
return { toolbarVisibilityListeners.remove(listener) }
|
||||||
}
|
}
|
||||||
|
@ -108,16 +113,16 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
supportPostponeEnterTransition()
|
supportPostponeEnterTransition()
|
||||||
|
|
||||||
// Gather the parameters.
|
// Gather the parameters.
|
||||||
attachments = ViewMediaActivityIntent.getAttachments(intent)
|
attachmentViewData = ViewMediaActivityIntent.getAttachments(intent)
|
||||||
val initialPosition = ViewMediaActivityIntent.getAttachmentIndex(intent)
|
val initialPosition = ViewMediaActivityIntent.getAttachmentIndex(intent)
|
||||||
|
|
||||||
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
|
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
|
||||||
// but it cannot be expressed and if I don't specify type explicitly compilation fails
|
// but it cannot be expressed and if I don't specify type explicitly compilation fails
|
||||||
// (probably a bug in compiler)
|
// (probably a bug in compiler)
|
||||||
val adapter: ViewMediaAdapter = if (attachments != null) {
|
val adapter: ViewMediaAdapter = if (attachmentViewData != null) {
|
||||||
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
|
val attachments = attachmentViewData!!.map(AttachmentViewData::attachment)
|
||||||
// Setup the view pager.
|
// Setup the view pager.
|
||||||
ImagePagerAdapter(this, realAttachs, initialPosition)
|
ImagePagerAdapter(this, attachments, initialPosition)
|
||||||
} else {
|
} else {
|
||||||
imageUrl = ViewMediaActivityIntent.getImageUrl(intent)
|
imageUrl = ViewMediaActivityIntent.getImageUrl(intent)
|
||||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||||
|
@ -137,12 +142,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
|
|
||||||
// Setup the toolbar.
|
// Setup the toolbar.
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
val actionBar = supportActionBar
|
supportActionBar?.apply {
|
||||||
if (actionBar != null) {
|
setDisplayHomeAsUpEnabled(true)
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
actionBar.setDisplayShowHomeEnabled(true)
|
title = getPageTitle(initialPosition)
|
||||||
actionBar.title = getPageTitle(initialPosition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
|
binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
|
||||||
binding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
|
binding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
@ -170,7 +175,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.view_media_toolbar, menu)
|
menuInflater.inflate(R.menu.view_media_toolbar, menu)
|
||||||
// We don't support 'open status' from single image views
|
// We don't support 'open status' from single image views
|
||||||
menu.findItem(R.id.action_open_status)?.isVisible = (attachments != null)
|
menu.findItem(R.id.action_open_status)?.isVisible = (attachmentViewData != null)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,15 +184,15 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBringUp() {
|
override fun onMediaReady() {
|
||||||
supportStartPostponedEnterTransition()
|
supportStartPostponedEnterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDismiss() {
|
override fun onMediaDismiss() {
|
||||||
supportFinishAfterTransition()
|
supportFinishAfterTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPhotoTap() {
|
override fun onMediaTap() {
|
||||||
isToolbarVisible = !isToolbarVisible
|
isToolbarVisible = !isToolbarVisible
|
||||||
for (listener in toolbarVisibilityListeners) {
|
for (listener in toolbarVisibilityListeners) {
|
||||||
listener(isToolbarVisible)
|
listener(isToolbarVisible)
|
||||||
|
@ -214,14 +219,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPageTitle(position: Int): CharSequence {
|
private fun getPageTitle(position: Int): CharSequence {
|
||||||
if (attachments == null) {
|
attachmentViewData ?: return ""
|
||||||
return ""
|
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachmentViewData?.size)
|
||||||
}
|
|
||||||
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadMedia() {
|
private fun downloadMedia() {
|
||||||
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
val url = imageUrl ?: attachmentViewData!![binding.viewPager.currentItem].attachment.url
|
||||||
val filename = Uri.parse(url).lastPathSegment
|
val filename = Uri.parse(url).lastPathSegment
|
||||||
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
|
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
@ -248,12 +251,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenStatus() {
|
private fun onOpenStatus() {
|
||||||
val attach = attachments!![binding.viewPager.currentItem]
|
val attach = attachmentViewData!![binding.viewPager.currentItem]
|
||||||
startActivityWithSlideInAnimation(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
startActivityWithSlideInAnimation(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyLink() {
|
private fun copyLink() {
|
||||||
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
val url = imageUrl ?: attachmentViewData!![binding.viewPager.currentItem].attachment.url
|
||||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
|
clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
|
||||||
}
|
}
|
||||||
|
@ -268,7 +271,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
if (imageUrl != null) {
|
if (imageUrl != null) {
|
||||||
shareMediaFile(directory, imageUrl!!)
|
shareMediaFile(directory, imageUrl!!)
|
||||||
} else {
|
} else {
|
||||||
val attachment = attachments!![binding.viewPager.currentItem].attachment
|
val attachment = attachmentViewData!![binding.viewPager.currentItem].attachment
|
||||||
shareMediaFile(directory, attachment.url)
|
shareMediaFile(directory, attachment.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ package app.pachli.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.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -30,6 +29,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.ViewMediaActivity
|
import app.pachli.ViewMediaActivity
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
|
@ -43,35 +43,21 @@ import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.ortiz.touchview.OnTouchCoordinatesListener
|
import com.ortiz.touchview.OnTouchCoordinatesListener
|
||||||
import com.ortiz.touchview.TouchImageView
|
import com.ortiz.touchview.TouchImageView
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ViewImageFragment : ViewMediaFragment() {
|
class ViewImageFragment : ViewMediaFragment() {
|
||||||
interface PhotoActionsListener {
|
|
||||||
fun onBringUp()
|
|
||||||
fun onDismiss()
|
|
||||||
fun onPhotoTap()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentViewImageBinding::bind)
|
private val binding by viewBinding(FragmentViewImageBinding::bind)
|
||||||
|
|
||||||
private lateinit var photoActionsListener: PhotoActionsListener
|
|
||||||
private lateinit var toolbar: View
|
private lateinit var toolbar: View
|
||||||
private var transition = BehaviorSubject.create<Unit>()
|
|
||||||
|
|
||||||
// Volatile: Image requests happen on background thread and we want to see updates to it
|
// Volatile: Image requests happen on background thread and we want to see updates to it
|
||||||
// immediately on another thread. Atomic is an overkill for such thing.
|
// immediately on another thread. Atomic is an overkill for such thing.
|
||||||
@Volatile
|
@Volatile
|
||||||
private var startedTransition = false
|
private var startedTransition = false
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun setupMediaView(showingDescription: Boolean) {
|
||||||
super.onAttach(context)
|
|
||||||
photoActionsListener = context as PhotoActionsListener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupMediaView(
|
|
||||||
showingDescription: Boolean,
|
|
||||||
) {
|
|
||||||
binding.photoView.transitionName = attachment.url
|
binding.photoView.transitionName = attachment.url
|
||||||
binding.mediaDescription.text = attachment.description
|
binding.mediaDescription.text = attachment.description
|
||||||
binding.captionSheet.visible(showingDescription)
|
binding.captionSheet.visible(showingDescription)
|
||||||
|
@ -82,7 +68,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
toolbar = (requireActivity() as ViewMediaActivity).toolbar
|
toolbar = (requireActivity() as ViewMediaActivity).toolbar
|
||||||
this.transition = BehaviorSubject.create()
|
|
||||||
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +80,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onDown(e: MotionEvent) = true
|
override fun onDown(e: MotionEvent) = true
|
||||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
photoActionsListener.onPhotoTap()
|
mediaActionsListener.onMediaTap()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -191,7 +176,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
*/
|
*/
|
||||||
private fun onGestureEnd(view: View) {
|
private fun onGestureEnd(view: View) {
|
||||||
if (abs(view.translationY) > 180) {
|
if (abs(view.translationY) > 180) {
|
||||||
photoActionsListener.onDismiss()
|
mediaActionsListener.onMediaDismiss()
|
||||||
} else {
|
} else {
|
||||||
view.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
view.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
||||||
}
|
}
|
||||||
|
@ -200,11 +185,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
finalizeViewSetup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onToolbarVisibilityChange(visible: Boolean) {
|
override fun onToolbarVisibilityChange(visible: Boolean) {
|
||||||
if (!userVisibleHint) return
|
if (!userVisibleHint) return
|
||||||
|
|
||||||
|
@ -228,11 +208,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
Glide.with(this).clear(binding.photoView)
|
Glide.with(this).clear(binding.photoView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
@SuppressLint("CheckResult")
|
||||||
transition.onComplete()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) {
|
private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) {
|
||||||
val glide = Glide.with(this)
|
val glide = Glide.with(this)
|
||||||
// Request image from the any cache
|
// Request image from the any cache
|
||||||
|
@ -240,18 +216,16 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
.load(url)
|
.load(url)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.onlyRetrieveFromCache(true)
|
.onlyRetrieveFromCache(true)
|
||||||
.let {
|
.apply {
|
||||||
if (previewUrl != null) {
|
previewUrl?.let {
|
||||||
it.thumbnail(
|
thumbnail(
|
||||||
glide
|
glide
|
||||||
.load(previewUrl)
|
.load(it)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.onlyRetrieveFromCache(true)
|
.onlyRetrieveFromCache(true)
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.addListener(ImageRequestListener(true, isThumbnailRequest = true)),
|
.addListener(ImageRequestListener(true, isThumbnailRequest = true)),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Request image from the network on fail load image from cache
|
// Request image from the network on fail load image from cache
|
||||||
|
@ -297,10 +271,10 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
isFirstResource: Boolean,
|
isFirstResource: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
// If cache for full image failed complete transition
|
// If cache for full image failed complete transition
|
||||||
if (isCacheRequest && !isThumbnailRequest && shouldStartTransition &&
|
if (isCacheRequest && !isThumbnailRequest && shouldCallMediaReady &&
|
||||||
!startedTransition
|
!startedTransition
|
||||||
) {
|
) {
|
||||||
photoActionsListener.onBringUp()
|
mediaActionsListener.onMediaReady()
|
||||||
}
|
}
|
||||||
// Hide progress bar only on fail request from internet
|
// Hide progress bar only on fail request from internet
|
||||||
if (!isCacheRequest) binding.progressBar.hide()
|
if (!isCacheRequest) binding.progressBar.hide()
|
||||||
|
@ -318,29 +292,26 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
): Boolean {
|
): Boolean {
|
||||||
binding.progressBar.hide() // Always hide the progress bar on success
|
binding.progressBar.hide() // Always hide the progress bar on success
|
||||||
|
|
||||||
if (!startedTransition || !shouldStartTransition) {
|
if (!startedTransition || !shouldCallMediaReady) {
|
||||||
// Set this right away so that we don't have to concurrent post() requests
|
// Set this right away so that we don't have to concurrent post() requests
|
||||||
startedTransition = true
|
startedTransition = true
|
||||||
// post() because load() replaces image with null. Sometimes after we set
|
// post() because load() replaces image with null. Sometimes after we set
|
||||||
// the thumbnail.
|
// the thumbnail.
|
||||||
binding.photoView.post {
|
binding.photoView.post {
|
||||||
target.onResourceReady(resource, null)
|
target.onResourceReady(resource, null)
|
||||||
if (shouldStartTransition) photoActionsListener.onBringUp()
|
if (shouldCallMediaReady) mediaActionsListener.onMediaReady()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This wait for transition. If there's no transition then we should hit
|
// Wait for the fragment transition to complete before signalling to
|
||||||
// another branch. take() will unsubscribe after we have it to not leak memory
|
// Glide that the resource is ready.
|
||||||
transition
|
transitionComplete?.let {
|
||||||
.take(1)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
.subscribe {
|
it.await()
|
||||||
target.onResourceReady(resource, null)
|
target.onResourceReady(resource, null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTransitionEnd() {
|
|
||||||
this.transition.onNext(Unit)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package app.pachli.fragment
|
package app.pachli.fragment
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -24,23 +25,65 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import app.pachli.ViewMediaActivity
|
import app.pachli.ViewMediaActivity
|
||||||
import app.pachli.core.network.model.Attachment
|
import app.pachli.core.network.model.Attachment
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
|
||||||
|
/** Interface for actions that may happen while media is being displayed */
|
||||||
|
interface MediaActionsListener {
|
||||||
|
/**
|
||||||
|
* The media fragment is ready for the hosting activity to complete the
|
||||||
|
* fragment transition; typically because any media to be displayed has
|
||||||
|
* been loaded.
|
||||||
|
*/
|
||||||
|
fun onMediaReady()
|
||||||
|
|
||||||
|
/** The user is dismissing the media (e.g., by flinging up) */
|
||||||
|
fun onMediaDismiss()
|
||||||
|
|
||||||
|
/** The user has tapped on the media (typically to show/hide UI controls) */
|
||||||
|
fun onMediaTap()
|
||||||
|
}
|
||||||
|
|
||||||
abstract class ViewMediaFragment : Fragment() {
|
abstract class ViewMediaFragment : Fragment() {
|
||||||
private var toolbarVisibilityDisposable: Function0<Boolean>? = null
|
/** Function to remove the toolbar listener */
|
||||||
|
private var removeToolbarListener: Function0<Boolean>? = null
|
||||||
|
|
||||||
abstract fun setupMediaView(
|
/**
|
||||||
showingDescription: Boolean,
|
* Called after [onResume], subclasses should override this and update
|
||||||
)
|
* the contents of views (including loading any media).
|
||||||
|
*
|
||||||
|
* @param showingDescription True if the media's description should be shown
|
||||||
|
*/
|
||||||
|
abstract fun setupMediaView(showingDescription: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the visibility of the toolbar changes.
|
||||||
|
*
|
||||||
|
* @param visible True if the toolbar is visible
|
||||||
|
*/
|
||||||
abstract fun onToolbarVisibilityChange(visible: Boolean)
|
abstract fun onToolbarVisibilityChange(visible: Boolean)
|
||||||
|
|
||||||
protected var showingDescription = false
|
protected var showingDescription = false
|
||||||
protected var isDescriptionVisible = false
|
protected var isDescriptionVisible = false
|
||||||
|
|
||||||
/** The attachment to show. Set in [onViewCreated] */
|
/** The attachment to show */
|
||||||
protected lateinit var attachment: Attachment
|
protected lateinit var attachment: Attachment
|
||||||
|
|
||||||
protected var shouldStartTransition = false
|
/** Listener to call as media is loaded or on user interaction */
|
||||||
|
protected lateinit var mediaActionsListener: MediaActionsListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the fragment should call [MediaActionsListener.onMediaReady]
|
||||||
|
* when the media is loaded.
|
||||||
|
*/
|
||||||
|
protected var shouldCallMediaReady = false
|
||||||
|
|
||||||
|
/** Awaitable signal that the transition has completed */
|
||||||
|
var transitionComplete: CompletableDeferred<Unit>? = CompletableDeferred()
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
mediaActionsListener = context as MediaActionsListener
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
@ -48,23 +91,60 @@ abstract class ViewMediaFragment : Fragment() {
|
||||||
attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
|
attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
|
||||||
?: throw IllegalArgumentException("ARG_ATTACHMENT has to be set")
|
?: throw IllegalArgumentException("ARG_ATTACHMENT has to be set")
|
||||||
|
|
||||||
shouldStartTransition = arguments?.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
shouldCallMediaReady = arguments?.getBoolean(ARG_SHOULD_CALL_MEDIA_READY)
|
||||||
?: throw IllegalArgumentException("ARG_START_POSTPONED_TRANSITION has to be set")
|
?: throw IllegalArgumentException("ARG_START_POSTPONED_TRANSITION has to be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the fragment adapter to notify the fragment that the shared
|
||||||
|
* element transition has been completed.
|
||||||
|
*/
|
||||||
|
open fun onTransitionEnd() {
|
||||||
|
this.transitionComplete?.complete(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
finalizeViewSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finalizeViewSetup() {
|
||||||
|
val mediaActivity = activity as ViewMediaActivity
|
||||||
|
|
||||||
|
showingDescription = !TextUtils.isEmpty(attachment.description)
|
||||||
|
isDescriptionVisible = showingDescription
|
||||||
|
setupMediaView(showingDescription && mediaActivity.isToolbarVisible)
|
||||||
|
|
||||||
|
removeToolbarListener = (activity as ViewMediaActivity)
|
||||||
|
.addToolbarVisibilityListener { isVisible ->
|
||||||
|
onToolbarVisibilityChange(isVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
removeToolbarListener?.invoke()
|
||||||
|
transitionComplete = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
protected const val ARG_SHOULD_CALL_MEDIA_READY = "shouldCallMediaReady"
|
||||||
protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
|
|
||||||
|
|
||||||
@JvmStatic
|
protected const val ARG_ATTACHMENT = "attach"
|
||||||
protected val ARG_ATTACHMENT = "attach"
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param attachment The media attachment to display in the fragment
|
||||||
|
* @param shouldCallMediaReady If true this fragment should call
|
||||||
|
* [MediaActionsListener.onMediaReady] when it has finished loading
|
||||||
|
* media, so the calling activity can perform any final actions.
|
||||||
|
* @return A fragment that shows [attachment]
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
|
fun newInstance(attachment: Attachment, shouldCallMediaReady: Boolean): ViewMediaFragment {
|
||||||
val arguments = Bundle(2)
|
val arguments = Bundle(2)
|
||||||
arguments.putParcelable(ARG_ATTACHMENT, attachment)
|
arguments.putParcelable(ARG_ATTACHMENT, attachment)
|
||||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition)
|
arguments.putBoolean(ARG_SHOULD_CALL_MEDIA_READY, shouldCallMediaReady)
|
||||||
|
|
||||||
val fragment = when (attachment.type) {
|
val fragment = when (attachment.type) {
|
||||||
Attachment.Type.IMAGE -> ViewImageFragment()
|
Attachment.Type.IMAGE -> ViewImageFragment()
|
||||||
|
@ -94,30 +174,10 @@ abstract class ViewMediaFragment : Fragment() {
|
||||||
blurhash = null,
|
blurhash = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true)
|
arguments.putBoolean(ARG_SHOULD_CALL_MEDIA_READY, true)
|
||||||
|
|
||||||
fragment.arguments = arguments
|
fragment.arguments = arguments
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onTransitionEnd()
|
|
||||||
|
|
||||||
protected fun finalizeViewSetup() {
|
|
||||||
val mediaActivity = activity as ViewMediaActivity
|
|
||||||
|
|
||||||
showingDescription = !TextUtils.isEmpty(attachment.description)
|
|
||||||
isDescriptionVisible = showingDescription
|
|
||||||
setupMediaView(showingDescription && mediaActivity.isToolbarVisible)
|
|
||||||
|
|
||||||
toolbarVisibilityDisposable = (activity as ViewMediaActivity)
|
|
||||||
.addToolbarVisibilityListener { isVisible ->
|
|
||||||
onToolbarVisibilityChange(isVisible)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
toolbarVisibilityDisposable?.invoke()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@ import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
@ -36,6 +34,7 @@ import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
@ -50,10 +49,12 @@ import app.pachli.BuildConfig
|
||||||
import app.pachli.R
|
import app.pachli.R
|
||||||
import app.pachli.ViewMediaActivity
|
import app.pachli.ViewMediaActivity
|
||||||
import app.pachli.core.common.extensions.hide
|
import app.pachli.core.common.extensions.hide
|
||||||
|
import app.pachli.core.common.extensions.show
|
||||||
import app.pachli.core.common.extensions.viewBinding
|
import app.pachli.core.common.extensions.viewBinding
|
||||||
import app.pachli.core.common.extensions.visible
|
import app.pachli.core.common.extensions.visible
|
||||||
import app.pachli.core.network.model.Attachment
|
import app.pachli.core.network.model.Attachment
|
||||||
import app.pachli.databinding.FragmentViewVideoBinding
|
import app.pachli.databinding.FragmentViewVideoBinding
|
||||||
|
import app.pachli.fragment.ViewVideoFragment.Companion.CONTROLS_TIMEOUT
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
@ -61,6 +62,10 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -69,31 +74,24 @@ import okhttp3.OkHttpClient
|
||||||
* UI behaviour:
|
* UI behaviour:
|
||||||
*
|
*
|
||||||
* - Fragment starts, media description is visible at top of screen, video starts playing
|
* - Fragment starts, media description is visible at top of screen, video starts playing
|
||||||
* - Media description + toolbar disappears after CONTROLS_TIMEOUT_MS
|
* - Media description + toolbar disappears after [CONTROLS_TIMEOUT]
|
||||||
* - Tapping shows controls + media description + toolbar, which fade after CONTROLS_TIMEOUT_MS
|
* - Tapping shows controls + media description + toolbar, which fade after [CONTROLS_TIMEOUT]
|
||||||
* - Tapping pause, or the media description, pauses the video and the controls + media description
|
* - Tapping pause, or the media description, pauses the video and the controls + media description
|
||||||
* remain visible
|
* remain visible
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ViewVideoFragment : ViewMediaFragment() {
|
class ViewVideoFragment : ViewMediaFragment() {
|
||||||
interface VideoActionsListener {
|
|
||||||
fun onDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentViewVideoBinding::bind)
|
private val binding by viewBinding(FragmentViewVideoBinding::bind)
|
||||||
|
|
||||||
private lateinit var videoActionsListener: VideoActionsListener
|
|
||||||
private lateinit var toolbar: View
|
private lateinit var toolbar: View
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
private val hideToolbar = Runnable {
|
/** Hoist toolbar hiding to activity so it can track state across different fragments */
|
||||||
// Hoist toolbar hiding to activity so it can track state across different fragments
|
private var hideToolbarJob: Job? = null
|
||||||
// This is explicitly stored as runnable so that we pass it to the handler later for cancellation
|
|
||||||
mediaActivity.onPhotoTap()
|
|
||||||
}
|
|
||||||
private lateinit var mediaActivity: ViewMediaActivity
|
private lateinit var mediaActivity: ViewMediaActivity
|
||||||
private lateinit var mediaPlayerListener: Player.Listener
|
private lateinit var mediaPlayerListener: Player.Listener
|
||||||
private var isAudio = false
|
private var isAudio = false
|
||||||
|
@ -107,13 +105,13 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
|
|
||||||
private lateinit var mediaSourceFactory: DefaultMediaSourceFactory
|
private lateinit var mediaSourceFactory: DefaultMediaSourceFactory
|
||||||
|
|
||||||
|
private var startedTransition = false
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
mediaSourceFactory = DefaultMediaSourceFactory(context)
|
mediaSourceFactory = DefaultMediaSourceFactory(context)
|
||||||
.setDataSourceFactory(DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient)))
|
.setDataSourceFactory(DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient)))
|
||||||
|
|
||||||
videoActionsListener = context as VideoActionsListener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateResource", "MissingInflatedId")
|
@SuppressLint("PrivateResource", "MissingInflatedId")
|
||||||
|
@ -137,13 +135,12 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.videoView.controllerShowTimeoutMs = CONTROLS_TIMEOUT_MS
|
binding.videoView.controllerShowTimeoutMs = CONTROLS_TIMEOUT.inWholeMilliseconds.toInt()
|
||||||
|
|
||||||
isAudio = attachment.type == Attachment.Type.AUDIO
|
isAudio = attachment.type == Attachment.Type.AUDIO
|
||||||
|
binding.progressBar.show()
|
||||||
|
|
||||||
/**
|
/** Handle single taps, flings, and dragging */
|
||||||
* Handle single taps, flings, and dragging
|
|
||||||
*/
|
|
||||||
val touchListener = object : View.OnTouchListener {
|
val touchListener = object : View.OnTouchListener {
|
||||||
var lastY = 0f
|
var lastY = 0f
|
||||||
|
|
||||||
|
@ -160,7 +157,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
|
|
||||||
/** A single tap should show/hide the media description */
|
/** A single tap should show/hide the media description */
|
||||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
mediaActivity.onPhotoTap()
|
mediaActivity.onMediaTap()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +169,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
velocityY: Float,
|
velocityY: Float,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (abs(velocityY) > abs(velocityX)) {
|
if (abs(velocityY) > abs(velocityX)) {
|
||||||
videoActionsListener.onDismiss()
|
mediaActionsListener.onMediaDismiss()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -196,7 +193,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
}
|
}
|
||||||
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||||
if (abs(contentFrame.translationY) > 180) {
|
if (abs(contentFrame.translationY) > 180) {
|
||||||
videoActionsListener.onDismiss()
|
mediaActionsListener.onMediaDismiss()
|
||||||
} else {
|
} else {
|
||||||
contentFrame.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
contentFrame.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
||||||
}
|
}
|
||||||
|
@ -222,6 +219,18 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
|
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
binding.videoView.useController = true
|
binding.videoView.useController = true
|
||||||
|
|
||||||
|
if (shouldCallMediaReady && !startedTransition) {
|
||||||
|
startedTransition = true
|
||||||
|
mediaActivity.onMediaReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionComplete?.let {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
it.await()
|
||||||
|
player?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> { /* do nothing */ }
|
else -> { /* do nothing */ }
|
||||||
}
|
}
|
||||||
|
@ -234,7 +243,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
hideToolbarAfterDelay()
|
hideToolbarAfterDelay()
|
||||||
} else {
|
} else {
|
||||||
handler.removeCallbacks(hideToolbar)
|
hideToolbarJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,8 +275,6 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
finalizeViewSetup()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= 23 || player == null) {
|
if (Build.VERSION.SDK_INT <= 23 || player == null) {
|
||||||
initializePlayer()
|
initializePlayer()
|
||||||
if (mediaActivity.isToolbarVisible && !isAudio) {
|
if (mediaActivity.isToolbarVisible && !isAudio) {
|
||||||
|
@ -294,7 +301,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
if (Build.VERSION.SDK_INT <= 23) {
|
if (Build.VERSION.SDK_INT <= 23) {
|
||||||
binding.videoView.onPause()
|
binding.videoView.onPause()
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
handler.removeCallbacks(hideToolbar)
|
hideToolbarJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,7 +313,7 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
if (Build.VERSION.SDK_INT > 23) {
|
if (Build.VERSION.SDK_INT > 23) {
|
||||||
binding.videoView.onPause()
|
binding.videoView.onPause()
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
handler.removeCallbacks(hideToolbar)
|
hideToolbarJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +330,10 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
setMediaItem(MediaItem.fromUri(mediaAttachment.url))
|
setMediaItem(MediaItem.fromUri(mediaAttachment.url))
|
||||||
addListener(mediaPlayerListener)
|
addListener(mediaPlayerListener)
|
||||||
repeatMode = Player.REPEAT_MODE_ONE
|
repeatMode = Player.REPEAT_MODE_ONE
|
||||||
playWhenReady = true
|
|
||||||
|
// Playback is automatically started in onPlaybackStateChanged when
|
||||||
|
// any transitions have completed.
|
||||||
|
playWhenReady = false
|
||||||
seekTo(savedSeekPosition)
|
seekTo(savedSeekPosition)
|
||||||
prepare()
|
prepare()
|
||||||
player = this
|
player = this
|
||||||
|
@ -355,9 +365,9 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun setupMediaView(
|
override fun setupMediaView(showingDescription: Boolean) {
|
||||||
showingDescription: Boolean,
|
startedTransition = false
|
||||||
) {
|
|
||||||
binding.mediaDescription.text = attachment.description
|
binding.mediaDescription.text = attachment.description
|
||||||
binding.mediaDescription.visible(showingDescription)
|
binding.mediaDescription.visible(showingDescription)
|
||||||
binding.mediaDescription.movementMethod = ScrollingMovementMethod()
|
binding.mediaDescription.movementMethod = ScrollingMovementMethod()
|
||||||
|
@ -377,21 +387,18 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.videoView.requestFocus()
|
binding.videoView.requestFocus()
|
||||||
|
|
||||||
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
|
|
||||||
mediaActivity.onBringUp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideToolbarAfterDelay() {
|
private fun hideToolbarAfterDelay() {
|
||||||
handler.postDelayed(hideToolbar, CONTROLS_TIMEOUT_MS.toLong())
|
hideToolbarJob?.cancel()
|
||||||
|
hideToolbarJob = lifecycleScope.launch {
|
||||||
|
delay(CONTROLS_TIMEOUT)
|
||||||
|
mediaActivity.onMediaTap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onToolbarVisibilityChange(visible: Boolean) {
|
override fun onToolbarVisibilityChange(visible: Boolean) {
|
||||||
if (!userVisibleHint) {
|
if (!userVisibleHint) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
view ?: return
|
view ?: return
|
||||||
|
|
||||||
isDescriptionVisible = showingDescription && visible
|
isDescriptionVisible = showingDescription && visible
|
||||||
|
@ -417,14 +424,12 @@ class ViewVideoFragment : ViewMediaFragment() {
|
||||||
if (visible && (binding.videoView.player?.isPlaying == true) && !isAudio) {
|
if (visible && (binding.videoView.player?.isPlaying == true) && !isAudio) {
|
||||||
hideToolbarAfterDelay()
|
hideToolbarAfterDelay()
|
||||||
} else {
|
} else {
|
||||||
handler.removeCallbacks(hideToolbar)
|
hideToolbarJob?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTransitionEnd() { }
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CONTROLS_TIMEOUT_MS = 2000 // Consistent with YouTube player
|
private val CONTROLS_TIMEOUT = 2.seconds // Consistent with YouTube player
|
||||||
private const val SEEK_POSITION = "seekPosition"
|
private const val SEEK_POSITION = "seekPosition"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ class ImagePagerAdapter(
|
||||||
private val initialPosition: Int,
|
private val initialPosition: Int,
|
||||||
) : ViewMediaAdapter(activity) {
|
) : ViewMediaAdapter(activity) {
|
||||||
|
|
||||||
private var didTransition = false
|
/** True if the animated transition to the fragment has completed */
|
||||||
|
private var transitionComplete = false
|
||||||
private val fragments = MutableList<WeakReference<ViewMediaFragment>?>(attachments.size) { null }
|
private val fragments = MutableList<WeakReference<ViewMediaFragment>?>(attachments.size) { null }
|
||||||
|
|
||||||
override fun getItemCount() = attachments.size
|
override fun getItemCount() = attachments.size
|
||||||
|
@ -26,7 +27,7 @@ class ImagePagerAdapter(
|
||||||
// transition and wait until it's over and it will never take place.
|
// transition and wait until it's over and it will never take place.
|
||||||
val fragment = ViewMediaFragment.newInstance(
|
val fragment = ViewMediaFragment.newInstance(
|
||||||
attachment = attachments[position],
|
attachment = attachments[position],
|
||||||
shouldStartPostponedTransition = !didTransition && position == initialPosition,
|
shouldCallMediaReady = !transitionComplete && position == initialPosition,
|
||||||
)
|
)
|
||||||
fragments[position] = WeakReference(fragment)
|
fragments[position] = WeakReference(fragment)
|
||||||
return fragment
|
return fragment
|
||||||
|
@ -35,8 +36,14 @@ class ImagePagerAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the hosting activity to notify the adapter that the shared element
|
||||||
|
* transition to the first displayed item in the adapter has completed.
|
||||||
|
*
|
||||||
|
* Forward the notification to the fragment.
|
||||||
|
*/
|
||||||
override fun onTransitionEnd(position: Int) {
|
override fun onTransitionEnd(position: Int) {
|
||||||
this.didTransition = true
|
this.transitionComplete = true
|
||||||
fragments[position]?.get()?.onTransitionEnd()
|
fragments[position]?.get()?.onTransitionEnd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue