ViewImageFragment: finalize transition to BigImageViewer. Now it should correctly handle EXIF rotation, do not stuck at transition and pick preview image if it's in cache but full image isn't

This commit is contained in:
Alibek Omarov 2020-07-26 03:11:33 +03:00
parent 08780eade8
commit c332a6893a
2 changed files with 152 additions and 64 deletions

View File

@ -35,9 +35,11 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.github.piasy.biv.loader.glide.GlideCustomImageLoader import com.github.piasy.biv.loader.glide.GlideCustomImageLoader
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import com.github.piasy.biv.loader.ImageLoader
import com.github.piasy.biv.view.GlideImageViewFactory import com.github.piasy.biv.view.GlideImageViewFactory
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.withLifecycleContext import com.keylesspalace.tusky.util.withLifecycleContext
import java.io.File
// 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
@ -53,8 +55,19 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
dialogLayout.orientation = LinearLayout.VERTICAL dialogLayout.orientation = LinearLayout.VERTICAL
val imageView = BigImageView(this) val imageView = BigImageView(this)
// imageView.ssiv.maxScale = 6f
imageView.setImageViewFactory(GlideImageViewFactory()) imageView.setImageViewFactory(GlideImageViewFactory())
imageView.setImageLoaderCallback(object : ImageLoader.Callback {
override fun onSuccess(image: File?) {
imageView.ssiv?.let { it.maxScale = 6f }
}
override fun onFail(error: Exception?) {}
override fun onStart() {}
override fun onCacheHit(imageType: Int, image: File?) {}
override fun onCacheMiss(imageType: Int, image: File?) {}
override fun onFinish() {}
override fun onProgress(progress: Int) {}
})
imageView.showImage(previewUri)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics) windowManager.defaultDisplay.getMetrics(displayMetrics)
@ -97,12 +110,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
dialog.show() dialog.show()
// Load the image and manually set it into the ImageView because it doesn't have a fixed
// size. Maybe we should limit the size of CustomTarget
imageView.showImage(previewUri)
} }
private fun Activity.showFailedCaptionMessage() { private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
} }

View File

@ -25,11 +25,15 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource 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.CustomTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.github.piasy.biv.view.GlideImageViewFactory import com.github.piasy.biv.view.GlideImageViewFactory
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -42,9 +46,10 @@ import kotlinx.android.synthetic.main.fragment_view_image.*
import java.io.File import java.io.File
import java.lang.Exception import java.lang.Exception
import kotlin.math.abs import kotlin.math.abs
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouchListener { class ViewImageFragment : ViewMediaFragment() {
interface PhotoActionsListener { interface PhotoActionsListener {
fun onBringUp() fun onBringUp()
fun onDismiss() fun onDismiss()
@ -60,6 +65,10 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc
@Volatile @Volatile
private var startedTransition = false private var startedTransition = false
private var uri = Uri.EMPTY
private var previewUri = Uri.EMPTY
private var showingPreview = false
override lateinit var descriptionView: TextView override lateinit var descriptionView: TextView
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@ -70,7 +79,11 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc
descriptionView = mediaDescription descriptionView = mediaDescription
photoView.transitionName = url photoView.transitionName = url
startedTransition = false startedTransition = false
loadImageFromNetwork(url, previewUrl) uri = Uri.parse(url)
if(previewUrl != null && !previewUrl.equals(url)) {
previewUri = Uri.parse(previewUrl)
}
loadImageFromNetwork()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -78,51 +91,53 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc
return inflater.inflate(R.layout.fragment_view_image, container, false) return inflater.inflate(R.layout.fragment_view_image, container, false)
} }
private var lastY = 0.0f
private var swipeStartedWithOneFinger = false
private lateinit var gestureDetector : GestureDetector private lateinit var gestureDetector : GestureDetector
override fun onTouch(v: View, event: MotionEvent): Boolean { private val imageOnTouchListener = object : View.OnTouchListener {
// This part is for scaling/translating on vertical move. private var lastY = 0.0f
// We use raw coordinates to get the correct ones during scaling private var swipeStartedWithOneFinger = false
gestureDetector.onTouchEvent(event)
if(event.pointerCount != 1) { override fun onTouch(v: View, event: MotionEvent): Boolean {
swipeStartedWithOneFinger = false // This part is for scaling/translating on vertical move.
return false // We use raw coordinates to get the correct ones during scaling
} gestureDetector.onTouchEvent(event)
var result = false if(event.pointerCount != 1) {
when(event.action) {
MotionEvent.ACTION_DOWN -> {
swipeStartedWithOneFinger = true
lastY = event.rawY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
onGestureEnd() onGestureEnd()
swipeStartedWithOneFinger = false swipeStartedWithOneFinger = false
return false
} }
MotionEvent.ACTION_MOVE -> {
if(swipeStartedWithOneFinger && photoView.ssiv.scale <= photoView.ssiv.minScale) { when(event.action) {
val diff = event.rawY - lastY MotionEvent.ACTION_DOWN -> {
// This code is to prevent transformations during page scrolling swipeStartedWithOneFinger = true
// If we are already translating or we reached the threshold, then transform. lastY = event.rawY
if (photoView.translationY != 0f || abs(diff) > 40) { }
photoView.translationY += (diff) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) onGestureEnd()
photoView.scaleY = scale swipeStartedWithOneFinger = false
photoView.scaleX = scale }
lastY = event.rawY MotionEvent.ACTION_MOVE -> {
if(swipeStartedWithOneFinger && photoView.ssiv.scale <= photoView.ssiv.minScale) {
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
}
} }
result = true
} }
} }
}
return result return false
}
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -135,7 +150,7 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc
}) })
// photoView.setOnTouchListener(this) // photoView.setOnTouchListener(this)
photoView.setImageLoaderCallback(this) photoView.setImageLoaderCallback(imageLoaderCallback)
photoView.setImageViewFactory(GlideImageViewFactory()) photoView.setImageViewFactory(GlideImageViewFactory())
val arguments = this.requireArguments() val arguments = this.requireArguments()
@ -190,33 +205,97 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc
photoView.ssiv?.recycle() photoView.ssiv?.recycle()
} }
private fun loadImageFromNetwork(url: String, previewUrl: String?) { private inner class DummyCacheTarget(val ctx: Context, val requestPreview : Boolean) : CustomTarget<File>() {
photoView.showImage(Uri.parse(previewUrl), Uri.parse(url)) override fun onLoadCleared(placeholder: Drawable?) {}
override fun onLoadFailed(errorDrawable: Drawable?) {
if(requestPreview) {
// no preview, no full image in cache, load full image
// forget about fancy transition
showingPreview = false
photoView.showImage(uri)
} else {
// let's start downloading full image that we supposedly don't have
BigImageViewer.prefetch(uri)
// meanwhile poke cache about preview image
Glide.with(ctx).asFile()
.load(previewUri)
.dontAnimate()
.onlyRetrieveFromCache(true)
.into(DummyCacheTarget(ctx, true))
}
}
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
showingPreview = requestPreview
if(requestPreview) {
// have preview cached but not full image
photoView.showImage(previewUri, uri, true)
} else {
photoView.showImage(uri)
}
}
} }
override fun onSuccess(image: File?) { private fun loadImageFromNetwork() {
progressBar?.hide() // Always hide the progress bar on success if(previewUri != Uri.EMPTY) {
photoActionsListener.onBringUp() // check if we have full image in the cache, if yes, use it
photoView.ssiv?.setOnTouchListener(this) // if not, look for preview in cache and use it if available
} // if not, load full image anyway
Glide.with(this).asFile()
override fun onFail(error: Exception?) { .load(uri)
progressBar?.hide() .onlyRetrieveFromCache(true)
photoActionsListener.onBringUp() .dontAnimate()
} .into(DummyCacheTarget(context!!, false))
} else {
override fun onCacheHit(imageType: Int, image: File?) { // no need in cache lookup, just load full image
} showingPreview = false
photoView.showImage(uri)
override fun onCacheMiss(imageType: Int, image: File?) { }
}
override fun onFinish() {
}
override fun onProgress(progress: Int) {
} }
override fun onTransitionEnd() { override fun onTransitionEnd() {
// if we had preview, load full image, as transition has ended
if (showingPreview) {
showingPreview = false
photoView.loadMainImageNow()
}
}
private val imageLoaderCallback = object : ImageLoader.Callback {
override fun onSuccess(image: File?) {
if(!showingPreview) {
progressBar?.hide()
photoView.ssiv?.let {
it.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF
it.setOnTouchListener(imageOnTouchListener)
}
}
}
override fun onFail(error: Exception?) {
progressBar?.hide()
}
override fun onCacheHit(imageType: Int, image: File?) {
// image is here, bring up the activity!
photoActionsListener.onBringUp()
}
override fun onStart() {
// cache miss but image is downloading, bring up the activity
photoActionsListener.onBringUp()
}
override fun onCacheMiss(imageType: Int, image: File?) {
// this callback is useless because it's called after
// image is downloaded or pulled from cache
// so in case of cache miss, onStart is used
}
override fun onFinish() {}
override fun onProgress(progress: Int) {
// TODO: make use of it :)
}
} }
} }