Improve image viewer (#1843)
This commit does 3 things: 1. Replaces PhotoView (which is abandonware) with modern TouchImageView 2. Fixes an issue with panning images. Gesture was not intercepted properly and pager was taking control instead of image being moved. 3. Adds feedback to dismissing of images with vertical gesture.
This commit is contained in:
parent
8245e00a29
commit
58a1046348
|
@ -159,7 +159,7 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.github.connyduck:sparkbutton:4.0.0"
|
implementation "com.github.connyduck:sparkbutton:4.0.0"
|
||||||
|
|
||||||
implementation "com.github.chrisbanes:PhotoView:2.3.0"
|
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.1'
|
||||||
|
|
||||||
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||||
|
|
|
@ -33,9 +33,9 @@ import at.connyduck.sparkbutton.helpers.Utils
|
||||||
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
|
||||||
import com.github.chrisbanes.photoview.PhotoView
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||||
|
import com.ortiz.touchview.TouchImageView
|
||||||
|
|
||||||
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
|
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
|
||||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
|
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
|
||||||
|
@ -50,9 +50,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
dialogLayout.setPadding(padding, padding, padding, padding)
|
dialogLayout.setPadding(padding, padding, padding, padding)
|
||||||
|
|
||||||
dialogLayout.orientation = LinearLayout.VERTICAL
|
dialogLayout.orientation = LinearLayout.VERTICAL
|
||||||
val imageView = PhotoView(this).apply {
|
val imageView = TouchImageView(this).apply {
|
||||||
// If it seems a lot, try opening an image of A4 format or similar
|
maxZoom = 6f
|
||||||
maximumScale = 6.0f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayMetrics = DisplayMetrics()
|
val displayMetrics = DisplayMetrics()
|
||||||
|
|
|
@ -21,9 +21,7 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.*
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
@ -31,7 +29,6 @@ import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.github.chrisbanes.photoview.PhotoViewAttacher
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
@ -48,11 +45,11 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
fun onPhotoTap()
|
fun onPhotoTap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var attacher: PhotoViewAttacher
|
|
||||||
private lateinit var photoActionsListener: PhotoActionsListener
|
private lateinit var photoActionsListener: PhotoActionsListener
|
||||||
private lateinit var toolbar: View
|
private lateinit var toolbar: View
|
||||||
private var transition = BehaviorSubject.create<Unit>()
|
private var transition = BehaviorSubject.create<Unit>()
|
||||||
private var shouldStartTransition = false
|
private var shouldStartTransition = false
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -67,23 +64,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
override fun setupMediaView(url: String, previewUrl: String?) {
|
override fun setupMediaView(url: String, previewUrl: String?) {
|
||||||
descriptionView = mediaDescription
|
descriptionView = mediaDescription
|
||||||
photoView.transitionName = url
|
photoView.transitionName = url
|
||||||
attacher = PhotoViewAttacher(photoView).apply {
|
|
||||||
// Clicking outside the photo closes the viewer.
|
|
||||||
setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
|
|
||||||
setOnClickListener { onMediaTap() }
|
|
||||||
|
|
||||||
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo
|
|
||||||
* mostly fills the screen so clicking outside is difficult. */
|
|
||||||
setOnSingleFlingListener { _, _, velocityX, velocityY ->
|
|
||||||
var result = false
|
|
||||||
if (abs(velocityY) > abs(velocityX)) {
|
|
||||||
photoActionsListener.onDismiss()
|
|
||||||
result = true
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startedTransition = false
|
startedTransition = false
|
||||||
loadImageFromNetwork(url, previewUrl, photoView)
|
loadImageFromNetwork(url, previewUrl, photoView)
|
||||||
}
|
}
|
||||||
|
@ -94,9 +74,65 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
||||||
|
onMediaTap()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var lastY = 0f
|
||||||
|
photoView.setOnTouchListener { _, event ->
|
||||||
|
// This part is for scaling/translating on vertical move.
|
||||||
|
// We use raw coordinates to get the correct ones during scaling
|
||||||
|
var result = true
|
||||||
|
|
||||||
|
gestureDetector.onTouchEvent(event)
|
||||||
|
|
||||||
|
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||||
|
lastY = event.rawY
|
||||||
|
} else if (!photoView.isZoomed && event.action == MotionEvent.ACTION_MOVE) {
|
||||||
|
val diff = event.rawY - lastY
|
||||||
|
// This code is to prevent transformations during page scrolling
|
||||||
|
// If we are already translating or we reached the threshold, then transform.
|
||||||
|
if (photoView.translationY != 0f || abs(diff) > 40) {
|
||||||
|
photoView.translationY += (diff)
|
||||||
|
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
|
||||||
|
photoView.scaleY = scale
|
||||||
|
photoView.scaleX = scale
|
||||||
|
lastY = event.rawY
|
||||||
|
}
|
||||||
|
return@setOnTouchListener true
|
||||||
|
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||||
|
onGestureEnd()
|
||||||
|
} else if (event.pointerCount >= 2 || photoView.canScrollHorizontally(1) && photoView.canScrollHorizontally(-1)) {
|
||||||
|
// Starting from here is adapted code from TouchImageView to play nice with pager.
|
||||||
|
|
||||||
|
// Can scroll horizontally checks if there's still a part of the image.
|
||||||
|
// That can be scrolled until you reach the edge multi-touch event.
|
||||||
|
val parent = view.parent
|
||||||
|
result = when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||||
|
// Disallow RecyclerView to intercept touch events.
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
// Disable touch on view
|
||||||
|
false
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
// Allow RecyclerView to intercept touch events.
|
||||||
|
parent.requestDisallowInterceptTouchEvent(false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
val arguments = this.requireArguments()
|
val arguments = this.requireArguments()
|
||||||
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
||||||
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
||||||
|
@ -116,6 +152,14 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
finalizeViewSetup(url, attachment?.previewUrl, description)
|
finalizeViewSetup(url, attachment?.previewUrl, description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onGestureEnd() {
|
||||||
|
if (abs(photoView.translationY) > 180) {
|
||||||
|
photoActionsListener.onDismiss()
|
||||||
|
} else {
|
||||||
|
photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMediaTap() {
|
private fun onMediaTap() {
|
||||||
photoActionsListener.onPhotoTap()
|
photoActionsListener.onPhotoTap()
|
||||||
}
|
}
|
||||||
|
@ -155,7 +199,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
.load(previewUrl)
|
.load(previewUrl)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.onlyRetrieveFromCache(true)
|
.onlyRetrieveFromCache(true)
|
||||||
.centerInside()
|
|
||||||
.addListener(ImageRequestListener(true, isThumnailRequest = true)))
|
.addListener(ImageRequestListener(true, isThumnailRequest = true)))
|
||||||
else it
|
else it
|
||||||
}
|
}
|
||||||
|
@ -164,7 +207,6 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.addListener(ImageRequestListener(false, isThumnailRequest = false))
|
.addListener(ImageRequestListener(false, isThumnailRequest = false))
|
||||||
)
|
)
|
||||||
.centerInside()
|
|
||||||
.addListener(ImageRequestListener(true, isThumnailRequest = false))
|
.addListener(ImageRequestListener(true, isThumnailRequest = false))
|
||||||
.into(photoView)
|
.into(photoView)
|
||||||
}
|
}
|
||||||
|
@ -225,13 +267,7 @@ class ViewImageFragment : ViewMediaFragment() {
|
||||||
// another branch. take() will unsubscribe after we have it to not leak menmory
|
// another branch. take() will unsubscribe after we have it to not leak menmory
|
||||||
transition
|
transition
|
||||||
.take(1)
|
.take(1)
|
||||||
.subscribe {
|
.subscribe { target.onResourceReady(resource, null) }
|
||||||
target.onResourceReady(resource, null)
|
|
||||||
// It's needed. Don't ask why, I don't know, setImageDrawable() should
|
|
||||||
// do it by itself but somehow it doesn't work automatically.
|
|
||||||
// Just do it. If you don't, image will jump around when touched.
|
|
||||||
attacher.update()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,10 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
|
||||||
<com.github.chrisbanes.photoview.PhotoView
|
<com.ortiz.touchview.TouchImageView
|
||||||
android:id="@+id/photoView"
|
android:id="@+id/photoView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
Loading…
Reference in New Issue