Skip to content

Commit

Permalink
feat: Auto-hide Image Viewer toolbar
Browse files Browse the repository at this point in the history
When viewing an Image, it is not straightforward to understand that tapping on it will hide the toolbar and alt sheet, and those elements might get in the way of them viewing the full image.

Automatically hiding the toolbar and alt sheet after some short time might help the user two ways:
1. Get out of their way when they're viewing an image with an aspect ratio so that those elements are on top of it.
2. Showing them that those elements are hide-able, which might nudge into tapping to restore them and, consequently, learn about the "tap to hide/show" feature.
  • Loading branch information
tinsukE committed Mar 6, 2024
1 parent 32a6d34 commit 6177c46
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 13 deletions.
65 changes: 61 additions & 4 deletions app/src/main/java/app/pachli/fragment/ViewImageFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@ import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.GestureDetectorCompat
import androidx.lifecycle.lifecycleScope
import app.pachli.R
import app.pachli.ViewMediaActivity
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible
Expand All @@ -41,33 +42,44 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.ortiz.touchview.OnTouchCoordinatesListener
import com.ortiz.touchview.TouchImageView
import kotlin.math.abs
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class ViewImageFragment : ViewMediaFragment() {

private val binding by viewBinding(FragmentViewImageBinding::bind)

private lateinit var toolbar: View
/** Hoist toolbar hiding to activity so it can track state across different fragments */
private var hideToolbarJob: Job? = null

// 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.
@Volatile
private var startedTransition = false

override fun setupMediaView(showingDescription: Boolean) {
override fun setupMediaView(
isToolbarVisible: Boolean,
showingDescription: Boolean,
) {
binding.photoView.transitionName = attachment.url
binding.mediaDescription.text = attachment.description
binding.captionSheet.visible(showingDescription)

startedTransition = false
loadImageFromNetwork(attachment.url, attachment.previewUrl, binding.photoView)

if (isToolbarVisible) {
hideToolbarAfterDelay()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = (requireActivity() as ViewMediaActivity).toolbar
return inflater.inflate(R.layout.fragment_view_image, container, false)
}

Expand Down Expand Up @@ -183,6 +195,27 @@ class ViewImageFragment : ViewMediaFragment() {
}
},
)

val captionSheetParams = (binding.captionSheet.layoutParams as CoordinatorLayout.LayoutParams)
(captionSheetParams.behavior as BottomSheetBehavior).addBottomSheetCallback(
object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
hideToolbarJob?.cancel()
}

override fun onSlide(bottomSheet: View, slideOffset: Float) {
hideToolbarJob?.cancel()
}
},
)
}

private fun hideToolbarAfterDelay() {
hideToolbarJob?.cancel()
hideToolbarJob = lifecycleScope.launch {
delay(TOOLBAR_TIMEOUT_MS)
mediaActivity.onMediaTap()
}
}

override fun onToolbarVisibilityChange(visible: Boolean) {
Expand All @@ -201,11 +234,31 @@ class ViewImageFragment : ViewMediaFragment() {
},
)
.start()

if (!visible) {
hideToolbarJob?.cancel()
}
}

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) {
hideToolbarJob?.cancel()
}
}

override fun onStop() {
super.onStop()
Glide.with(this).clear(binding.photoView)

// 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) {
hideToolbarJob?.cancel()
}
}

@SuppressLint("CheckResult")
Expand Down Expand Up @@ -314,4 +367,8 @@ class ViewImageFragment : ViewMediaFragment() {
return true
}
}

companion object {
private const val TOOLBAR_TIMEOUT_MS = 2000L
}
}
16 changes: 11 additions & 5 deletions app/src/main/java/app/pachli/fragment/ViewMediaFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ abstract class ViewMediaFragment : Fragment() {
* Called after [onResume], subclasses should override this and update
* the contents of views (including loading any media).
*
* @param isToolbarVisible True if the toolbar is visible
* @param showingDescription True if the media's description should be shown
*/
abstract fun setupMediaView(showingDescription: Boolean)
abstract fun setupMediaView(
isToolbarVisible: Boolean,
showingDescription: Boolean,
)

/**
* Called when the visibility of the toolbar changes.
Expand All @@ -62,6 +66,9 @@ abstract class ViewMediaFragment : Fragment() {
*/
abstract fun onToolbarVisibilityChange(visible: Boolean)

protected lateinit var mediaActivity: ViewMediaActivity
private set

protected var showingDescription = false
protected var isDescriptionVisible = false

Expand All @@ -82,6 +89,7 @@ abstract class ViewMediaFragment : Fragment() {

override fun onAttach(context: Context) {
super.onAttach(context)
mediaActivity = activity as ViewMediaActivity
mediaActionsListener = context as MediaActionsListener
}

Expand Down Expand Up @@ -109,13 +117,11 @@ abstract class ViewMediaFragment : Fragment() {
}

private fun finalizeViewSetup() {
val mediaActivity = activity as ViewMediaActivity

showingDescription = !TextUtils.isEmpty(attachment.description)
isDescriptionVisible = showingDescription
setupMediaView(showingDescription && mediaActivity.isToolbarVisible)
setupMediaView(mediaActivity.isToolbarVisible, showingDescription && mediaActivity.isToolbarVisible)

removeToolbarListener = (activity as ViewMediaActivity)
removeToolbarListener = mediaActivity
.addToolbarVisibilityListener { isVisible ->
onToolbarVisibilityChange(isVisible)
}
Expand Down
8 changes: 4 additions & 4 deletions app/src/main/java/app/pachli/fragment/ViewVideoFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.ui.AspectRatioFrameLayout
import app.pachli.BuildConfig
import app.pachli.R
import app.pachli.ViewMediaActivity
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
Expand Down Expand Up @@ -92,7 +91,6 @@ class ViewVideoFragment : ViewMediaFragment() {
/** Hoist toolbar hiding to activity so it can track state across different fragments */
private var hideToolbarJob: Job? = null

private lateinit var mediaActivity: ViewMediaActivity
private lateinit var mediaPlayerListener: Player.Listener
private var isAudio = false

Expand All @@ -116,7 +114,6 @@ class ViewVideoFragment : ViewMediaFragment() {

@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)

Expand Down Expand Up @@ -365,7 +362,10 @@ class ViewVideoFragment : ViewMediaFragment() {
}

@SuppressLint("ClickableViewAccessibility")
override fun setupMediaView(showingDescription: Boolean) {
override fun setupMediaView(
isToolbarVisible: Boolean,
showingDescription: Boolean,
) {
startedTransition = false

binding.mediaDescription.text = attachment.description
Expand Down

0 comments on commit 6177c46

Please sign in to comment.