2018-10-15 19:56:11 +02:00
|
|
|
/* Copyright 2017 Andrew Dawson
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.fragment
|
|
|
|
|
|
|
|
import android.animation.Animator
|
|
|
|
import android.animation.AnimatorListenerAdapter
|
2019-08-17 20:05:24 +02:00
|
|
|
import android.annotation.SuppressLint
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.content.Context
|
2019-04-16 21:39:12 +02:00
|
|
|
import android.graphics.drawable.Drawable
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.os.Bundle
|
2020-08-01 21:48:51 +02:00
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.MotionEvent
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.widget.ImageView
|
2019-04-16 21:39:12 +02:00
|
|
|
import com.bumptech.glide.Glide
|
|
|
|
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
|
2020-07-27 10:42:39 +02:00
|
|
|
import com.github.chrisbanes.photoview.PhotoViewAttacher
|
2021-03-13 21:27:20 +01:00
|
|
|
import com.keylesspalace.tusky.ViewMediaActivity
|
|
|
|
import com.keylesspalace.tusky.databinding.FragmentViewImageBinding
|
2018-10-15 19:56:11 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment
|
|
|
|
import com.keylesspalace.tusky.util.hide
|
2018-11-01 14:52:22 +01:00
|
|
|
import com.keylesspalace.tusky.util.visible
|
2019-08-04 20:22:57 +02:00
|
|
|
import io.reactivex.subjects.BehaviorSubject
|
|
|
|
import kotlin.math.abs
|
2018-10-15 19:56:11 +02:00
|
|
|
|
|
|
|
class ViewImageFragment : ViewMediaFragment() {
|
|
|
|
interface PhotoActionsListener {
|
|
|
|
fun onBringUp()
|
|
|
|
fun onDismiss()
|
|
|
|
fun onPhotoTap()
|
|
|
|
}
|
|
|
|
|
2021-03-13 21:27:20 +01:00
|
|
|
private var _binding: FragmentViewImageBinding? = null
|
|
|
|
private val binding get() = _binding!!
|
|
|
|
|
2020-07-27 10:42:39 +02:00
|
|
|
private lateinit var attacher: PhotoViewAttacher
|
2018-10-15 19:56:11 +02:00
|
|
|
private lateinit var photoActionsListener: PhotoActionsListener
|
|
|
|
private lateinit var toolbar: View
|
2019-08-04 20:22:57 +02:00
|
|
|
private var transition = BehaviorSubject.create<Unit>()
|
2019-08-17 20:05:24 +02:00
|
|
|
private var shouldStartTransition = false
|
2020-06-22 21:26:37 +02:00
|
|
|
|
2019-08-17 20:05:24 +02:00
|
|
|
// 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
|
2018-10-15 19:56:11 +02:00
|
|
|
|
|
|
|
override fun onAttach(context: Context) {
|
|
|
|
super.onAttach(context)
|
|
|
|
photoActionsListener = context as PhotoActionsListener
|
|
|
|
}
|
|
|
|
|
2020-08-01 21:48:51 +02:00
|
|
|
|
|
|
|
override fun setupMediaView(
|
|
|
|
url: String,
|
|
|
|
previewUrl: String?,
|
|
|
|
description: String?,
|
|
|
|
showingDescription: Boolean
|
|
|
|
) {
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.photoView.transitionName = url
|
|
|
|
binding.mediaDescription.text = description
|
|
|
|
binding.captionSheet.visible(showingDescription)
|
2020-08-01 21:48:51 +02:00
|
|
|
|
|
|
|
startedTransition = false
|
2021-03-13 21:27:20 +01:00
|
|
|
loadImageFromNetwork(url, previewUrl, binding.photoView)
|
2020-08-01 21:48:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
2021-03-13 21:27:20 +01:00
|
|
|
toolbar = (requireActivity() as ViewMediaActivity).toolbar
|
2020-08-01 21:48:51 +02:00
|
|
|
this.transition = BehaviorSubject.create()
|
2021-03-13 21:27:20 +01:00
|
|
|
_binding = FragmentViewImageBinding.inflate(inflater, container, false)
|
|
|
|
return binding.root
|
2020-08-01 21:48:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
|
|
|
val arguments = this.requireArguments()
|
|
|
|
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
|
|
|
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
|
|
|
val url: String?
|
|
|
|
var description: String? = null
|
|
|
|
|
|
|
|
if (attachment != null) {
|
|
|
|
url = attachment.url
|
|
|
|
description = attachment.description
|
|
|
|
} else {
|
2020-10-22 21:15:46 +02:00
|
|
|
url = arguments.getString(ARG_SINGLE_IMAGE_URL)
|
2020-08-01 21:48:51 +02:00
|
|
|
if (url == null) {
|
2020-10-22 21:15:46 +02:00
|
|
|
throw IllegalArgumentException("attachment or image url has to be set")
|
2020-08-01 21:48:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-13 21:27:20 +01:00
|
|
|
attacher = PhotoViewAttacher(binding.photoView).apply {
|
2020-07-27 10:42:39 +02:00
|
|
|
// This prevents conflicts with ViewPager
|
|
|
|
setAllowParentInterceptOnEdge(true)
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-07-27 10:42:39 +02:00
|
|
|
// Clicking outside the photo closes the viewer.
|
|
|
|
setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
|
|
|
|
setOnClickListener { onMediaTap() }
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-07-27 10:42:39 +02:00
|
|
|
/* 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
|
|
|
|
}
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-06-22 21:26:37 +02:00
|
|
|
var lastY = 0f
|
2020-07-27 10:42:39 +02:00
|
|
|
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.photoView.setOnTouchListener { v, event ->
|
2020-06-22 21:26:37 +02:00
|
|
|
// This part is for scaling/translating on vertical move.
|
|
|
|
// We use raw coordinates to get the correct ones during scaling
|
|
|
|
|
|
|
|
if (event.action == MotionEvent.ACTION_DOWN) {
|
|
|
|
lastY = event.rawY
|
2020-07-27 10:42:39 +02:00
|
|
|
} else if (event.pointerCount == 1
|
|
|
|
&& attacher.scale == 1f
|
|
|
|
&& event.action == MotionEvent.ACTION_MOVE
|
|
|
|
) {
|
2020-06-22 21:26:37 +02:00
|
|
|
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.
|
2021-03-13 21:27:20 +01:00
|
|
|
if (binding.photoView.translationY != 0f || abs(diff) > 40) {
|
|
|
|
binding.photoView.translationY += (diff)
|
|
|
|
val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
|
|
|
|
binding.photoView.scaleY = scale
|
|
|
|
binding.photoView.scaleX = scale
|
2020-06-22 21:26:37 +02:00
|
|
|
lastY = event.rawY
|
2020-07-27 10:42:39 +02:00
|
|
|
return@setOnTouchListener true
|
2020-06-22 21:26:37 +02:00
|
|
|
}
|
|
|
|
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
|
|
|
onGestureEnd()
|
|
|
|
}
|
2020-07-27 10:42:39 +02:00
|
|
|
attacher.onTouch(v, event)
|
2020-06-22 21:26:37 +02:00
|
|
|
}
|
|
|
|
|
2019-08-04 20:22:57 +02:00
|
|
|
finalizeViewSetup(url, attachment?.previewUrl, description)
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2020-06-22 21:26:37 +02:00
|
|
|
private fun onGestureEnd() {
|
2021-03-13 21:27:20 +01:00
|
|
|
if (_binding == null) {
|
2020-10-25 18:36:31 +01:00
|
|
|
return
|
|
|
|
}
|
2021-03-13 21:27:20 +01:00
|
|
|
if (abs(binding.photoView.translationY) > 180) {
|
2020-06-22 21:26:37 +02:00
|
|
|
photoActionsListener.onDismiss()
|
|
|
|
} else {
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
2020-06-22 21:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-15 19:56:11 +02:00
|
|
|
private fun onMediaTap() {
|
|
|
|
photoActionsListener.onPhotoTap()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onToolbarVisibilityChange(visible: Boolean) {
|
2021-03-13 21:27:20 +01:00
|
|
|
if (_binding == null || !userVisibleHint ) {
|
2018-10-15 19:56:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
isDescriptionVisible = showingDescription && visible
|
|
|
|
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.captionSheet.animate().alpha(alpha)
|
2018-10-15 19:56:11 +02:00
|
|
|
.setListener(object : AnimatorListenerAdapter() {
|
|
|
|
override fun onAnimationEnd(animation: Animator) {
|
2021-03-13 21:27:20 +01:00
|
|
|
if (_binding != null) {
|
|
|
|
binding.captionSheet.visible(isDescriptionVisible)
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
animation.removeListener(this)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.start()
|
|
|
|
}
|
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
override fun onDestroyView() {
|
2021-03-13 21:27:20 +01:00
|
|
|
Glide.with(this).clear(binding.photoView)
|
2019-08-17 20:05:24 +02:00
|
|
|
transition.onComplete()
|
2021-03-13 21:27:20 +01:00
|
|
|
_binding = null
|
2019-04-16 21:39:12 +02:00
|
|
|
super.onDestroyView()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2019-08-04 20:22:57 +02:00
|
|
|
private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) {
|
|
|
|
val glide = Glide.with(this)
|
|
|
|
// Request image from the any cache
|
|
|
|
glide
|
|
|
|
.load(url)
|
|
|
|
.dontAnimate()
|
|
|
|
.onlyRetrieveFromCache(true)
|
|
|
|
.let {
|
|
|
|
if (previewUrl != null)
|
|
|
|
it.thumbnail(glide
|
|
|
|
.load(previewUrl)
|
|
|
|
.dontAnimate()
|
|
|
|
.onlyRetrieveFromCache(true)
|
2020-08-01 13:26:59 +02:00
|
|
|
.centerInside()
|
2019-08-04 20:22:57 +02:00
|
|
|
.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))
|
|
|
|
)
|
2020-08-01 13:26:59 +02:00
|
|
|
.centerInside()
|
2019-08-04 20:22:57 +02:00
|
|
|
.addListener(ImageRequestListener(true, isThumnailRequest = false))
|
|
|
|
.into(photoView)
|
|
|
|
}
|
2019-04-16 21:39:12 +02:00
|
|
|
|
|
|
|
/**
|
2019-08-17 20:05:24 +02:00
|
|
|
* We start transition as soon as we think reasonable but we must take care about couple of
|
|
|
|
* things>
|
|
|
|
* - Do not change image in the middle of transition. It messes up the view.
|
|
|
|
* - 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
|
|
|
|
*
|
2019-04-16 21:39:12 +02:00
|
|
|
* @param isCacheRequest - is this listener for request image from cache or from the network
|
|
|
|
*/
|
2019-08-04 20:22:57 +02:00
|
|
|
private inner class ImageRequestListener(
|
|
|
|
private val isCacheRequest: Boolean,
|
|
|
|
private val isThumnailRequest: Boolean) : RequestListener<Drawable> {
|
|
|
|
|
|
|
|
override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>,
|
|
|
|
isFirstResource: Boolean): Boolean {
|
2019-08-17 20:05:24 +02:00
|
|
|
// If cache for full image failed complete transition
|
|
|
|
if (isCacheRequest && !isThumnailRequest && shouldStartTransition
|
|
|
|
&& !startedTransition) {
|
|
|
|
photoActionsListener.onBringUp()
|
|
|
|
}
|
2019-08-04 20:22:57 +02:00
|
|
|
// Hide progress bar only on fail request from internet
|
2021-03-13 21:27:20 +01:00
|
|
|
if (!isCacheRequest && _binding != null) binding.progressBar.hide()
|
2019-08-17 20:05:24 +02:00
|
|
|
// We don't want to overwrite preview with null when main image fails to load
|
|
|
|
return !isCacheRequest
|
2019-04-16 21:39:12 +02:00
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2019-08-17 20:05:24 +02:00
|
|
|
@SuppressLint("CheckResult")
|
2019-08-04 20:22:57 +02:00
|
|
|
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>,
|
|
|
|
dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
2021-03-13 21:27:20 +01:00
|
|
|
if (_binding != null) {
|
|
|
|
binding.progressBar.hide() // Always hide the progress bar on success
|
|
|
|
}
|
2019-08-17 20:05:24 +02:00
|
|
|
|
|
|
|
if (!startedTransition || !shouldStartTransition) {
|
|
|
|
// 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
|
|
|
|
// the thumbnail.
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.photoView.post {
|
2019-08-17 20:05:24 +02:00
|
|
|
target.onResourceReady(resource, null)
|
|
|
|
if (shouldStartTransition) photoActionsListener.onBringUp()
|
2019-08-04 20:22:57 +02:00
|
|
|
}
|
2019-08-17 20:05:24 +02:00
|
|
|
} 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)
|
2020-07-27 10:42:39 +02:00
|
|
|
.subscribe {
|
|
|
|
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()
|
|
|
|
}
|
2019-08-17 20:05:24 +02:00
|
|
|
}
|
|
|
|
return true
|
2019-04-16 21:39:12 +02:00
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2019-08-04 20:22:57 +02:00
|
|
|
override fun onTransitionEnd() {
|
|
|
|
this.transition.onNext(Unit)
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
}
|