ViewImageFragment: replace TouchImageView by BigImageView based on SSIV and with proper GIF support

This commit is contained in:
Alibek Omarov 2020-06-30 19:21:36 +03:00
parent ef6343faaa
commit 9c29cf1640
7 changed files with 104 additions and 154 deletions

View File

@ -178,7 +178,9 @@ dependencies {
implementation "com.github.connyduck:sparkbutton:4.0.0" implementation "com.github.connyduck:sparkbutton:4.0.0"
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.1' implementation 'com.github.piasy:BigImageViewer:1.6.5'
implementation 'com.github.piasy:GlideImageLoader:1.6.5'
implementation 'com.github.piasy:GlideImageViewFactory:1.6.5'
implementation "com.mikepenz:materialdrawer:$materialdrawerVersion" implementation "com.mikepenz:materialdrawer:$materialdrawerVersion"
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"

View File

@ -22,6 +22,8 @@ import android.util.Log
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkManager import androidx.work.WorkManager
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideCustomImageLoader
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
@ -73,6 +75,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
RxJavaPlugins.setErrorHandler { RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it) Log.w("RxJava", "undeliverable exception", it)
} }
BigImageViewer.initialize(GlideCustomImageLoader.with(this))
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {

View File

@ -33,9 +33,11 @@ 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.piasy.biv.loader.glide.GlideCustomImageLoader
import com.github.piasy.biv.view.BigImageView
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 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 +52,9 @@ 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 = TouchImageView(this).apply { val imageView = BigImageView(this)
maxZoom = 6f // imageView.ssiv.maxScale = 6f
} imageView.setImageViewFactory(GlideImageViewFactory())
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics) windowManager.defaultDisplay.getMetrics(displayMetrics)
@ -98,18 +100,9 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
// Load the image and manually set it into the ImageView because it doesn't have a fixed // 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 // size. Maybe we should limit the size of CustomTarget
Glide.with(this) imageView.showImage(previewUri)
.load(previewUri)
.into(object : CustomTarget<Drawable>() {
override fun onLoadCleared(placeholder: Drawable?) {}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
imageView.setImageDrawable(resource)
}
})
} }
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

@ -20,15 +20,18 @@ import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.* import android.view.*
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
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.Target import com.bumptech.glide.request.target.Target
import com.github.piasy.biv.loader.ImageLoader
import com.github.piasy.biv.view.GlideImageViewFactory
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
@ -36,9 +39,12 @@ import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.activity_view_media.* import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_image.* import kotlinx.android.synthetic.main.fragment_view_image.*
import java.io.File
import java.lang.Exception
import kotlin.math.abs import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() {
class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouchListener {
interface PhotoActionsListener { interface PhotoActionsListener {
fun onBringUp() fun onBringUp()
fun onDismiss() fun onDismiss()
@ -47,7 +53,6 @@ class ViewImageFragment : ViewMediaFragment() {
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 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
@ -65,73 +70,73 @@ class ViewImageFragment : ViewMediaFragment() {
descriptionView = mediaDescription descriptionView = mediaDescription
photoView.transitionName = url photoView.transitionName = url
startedTransition = false startedTransition = false
loadImageFromNetwork(url, previewUrl, photoView) loadImageFromNetwork(url, previewUrl)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = activity!!.toolbar toolbar = activity!!.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)
} }
private var lastY = 0.0f
private var swipeStartedWithOneFinger = false
private lateinit var gestureDetector : GestureDetector
override fun onTouch(v: View, event: MotionEvent): Boolean {
// This part is for scaling/translating on vertical move.
// We use raw coordinates to get the correct ones during scaling
gestureDetector.onTouchEvent(event)
if(event.pointerCount != 1) {
swipeStartedWithOneFinger = false
return false
}
var result = false
when(event.action) {
MotionEvent.ACTION_DOWN -> {
swipeStartedWithOneFinger = true
lastY = event.rawY
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
onGestureEnd()
swipeStartedWithOneFinger = false
}
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
}
@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)
val gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() { gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
onMediaTap() onMediaTap()
return true return true
} }
}) })
var lastY = 0f // photoView.setOnTouchListener(this)
photoView.setOnTouchListener { _, event -> photoView.setImageLoaderCallback(this)
// This part is for scaling/translating on vertical move. photoView.setImageViewFactory(GlideImageViewFactory())
// 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)
@ -181,99 +186,37 @@ class ViewImageFragment : ViewMediaFragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
Glide.with(this).clear(photoView)
transition.onComplete()
super.onDestroyView() super.onDestroyView()
photoView.ssiv?.recycle()
} }
private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) { private fun loadImageFromNetwork(url: String, previewUrl: String?) {
val glide = Glide.with(this) photoView.showImage(Uri.parse(previewUrl), Uri.parse(url))
// Request image from the any cache
glide
.load(url)
.dontAnimate()
.onlyRetrieveFromCache(true)
.let {
if (previewUrl != null)
it.thumbnail(glide
.load(previewUrl)
.dontAnimate()
.onlyRetrieveFromCache(true)
.addListener(ImageRequestListener(true, isThumnailRequest = true)))
else it
}
//Request image from the network on fail load image from cache
.error(glide.load(url)
.centerInside()
.addListener(ImageRequestListener(false, isThumnailRequest = false))
)
.addListener(ImageRequestListener(true, isThumnailRequest = false))
.into(photoView)
} }
/** override fun onSuccess(image: File?) {
* We start transition as soon as we think reasonable but we must take care about couple of progressBar?.hide() // Always hide the progress bar on success
* things> photoActionsListener.onBringUp()
* - Do not change image in the middle of transition. It messes up the view. photoView.ssiv?.setOnTouchListener(this)
* - Do not transition for the views which don't require it. Starting transition from }
* multiple fragments does weird things
* - Do not wait to transition until the image loads from network
*
* Preview, cached image, network image, x - failed, o - succeeded
* P C N - start transition after...
* x x x - the cache fails
* x x o - the cache fails
* x o o - the cache succeeds
* o x o - the preview succeeds. Do not start on cache.
* o o o - the preview succeeds. Do not start on cache.
*
* So start transition after the first success or after anything with the cache
*
* @param isCacheRequest - is this listener for request image from cache or from the network
*/
private inner class ImageRequestListener(
private val isCacheRequest: Boolean,
private val isThumnailRequest: Boolean) : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>, override fun onFail(error: Exception?) {
isFirstResource: Boolean): Boolean { progressBar?.hide()
// If cache for full image failed complete transition photoActionsListener.onBringUp()
if (isCacheRequest && !isThumnailRequest && shouldStartTransition }
&& !startedTransition) {
photoActionsListener.onBringUp()
}
// Hide progress bar only on fail request from internet
if (!isCacheRequest) progressBar?.hide()
// We don't want to overwrite preview with null when main image fails to load
return !isCacheRequest
}
@SuppressLint("CheckResult") override fun onCacheHit(imageType: Int, image: File?) {
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>, }
dataSource: DataSource, isFirstResource: Boolean): Boolean {
progressBar?.hide() // Always hide the progress bar on success
if (!startedTransition || !shouldStartTransition) { override fun onCacheMiss(imageType: Int, image: File?) {
// Set this right away so that we don't have to concurrent post() requests }
startedTransition = true
// post() because load() replaces image with null. Sometimes after we set override fun onFinish() {
// the thumbnail. }
photoView.post {
target.onResourceReady(resource, null) override fun onProgress(progress: Int) {
if (shouldStartTransition) photoActionsListener.onBringUp()
}
} else {
// This wait for transition. If there's no transition then we should hit
// another branch. take() will unsubscribe after we have it to not leak menmory
transition
.take(1)
.subscribe { target.onResourceReady(resource, null) }
}
return true
}
} }
override fun onTransitionEnd() { override fun onTransitionEnd() {
this.transition.onNext(Unit)
} }
} }

View File

@ -7,10 +7,12 @@
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true">
<com.ortiz.touchview.TouchImageView <com.github.piasy.biv.view.BigImageView
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"
app:initScaleType="fitCenter"
app:optimizeDisplay="false" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"

View File

@ -4,6 +4,7 @@
<item android:id="@+id/action_open_in_web" <item android:id="@+id/action_open_in_web"
android:icon="@drawable/ic_exit_to_app_24px" android:icon="@drawable/ic_exit_to_app_24px"
android:iconTint="@color/textColorPrimary"
android:title="@string/action_open_in_web" android:title="@string/action_open_in_web"
app:showAsAction="always" /> app:showAsAction="always" />

View File

@ -14,7 +14,12 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" } maven {
url "http://dl.bintray.com/piasy/maven"
}
maven {
url "https://jitpack.io"
}
} }
} }