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
|
2020-06-30 18:21:36 +02:00
|
|
|
import android.net.Uri
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.os.Bundle
|
2020-06-30 18:21:36 +02:00
|
|
|
import android.util.Log
|
2020-06-22 21:26:37 +02:00
|
|
|
import android.view.*
|
2018-11-01 14:52:22 +01:00
|
|
|
import android.widget.TextView
|
2020-07-26 02:11:33 +02:00
|
|
|
import androidx.exifinterface.media.ExifInterface
|
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
|
2020-07-26 02:11:33 +02:00
|
|
|
import com.bumptech.glide.request.target.CustomTarget
|
2019-04-16 21:39:12 +02:00
|
|
|
import com.bumptech.glide.request.target.Target
|
2020-07-26 02:11:33 +02:00
|
|
|
import com.bumptech.glide.request.transition.Transition
|
|
|
|
import com.github.piasy.biv.BigImageViewer
|
2020-06-30 18:21:36 +02:00
|
|
|
import com.github.piasy.biv.loader.ImageLoader
|
|
|
|
import com.github.piasy.biv.view.GlideImageViewFactory
|
2018-10-15 19:56:11 +02:00
|
|
|
import com.keylesspalace.tusky.R
|
|
|
|
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
|
2018-10-15 19:56:11 +02:00
|
|
|
import kotlinx.android.synthetic.main.activity_view_media.*
|
|
|
|
import kotlinx.android.synthetic.main.fragment_view_image.*
|
2020-06-30 18:21:36 +02:00
|
|
|
import java.io.File
|
|
|
|
import java.lang.Exception
|
2019-08-04 20:22:57 +02:00
|
|
|
import kotlin.math.abs
|
2020-07-26 02:11:33 +02:00
|
|
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-06-30 18:21:36 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
class ViewImageFragment : ViewMediaFragment() {
|
2018-10-15 19:56:11 +02:00
|
|
|
interface PhotoActionsListener {
|
|
|
|
fun onBringUp()
|
|
|
|
fun onDismiss()
|
|
|
|
fun onPhotoTap()
|
|
|
|
}
|
|
|
|
|
|
|
|
private lateinit var photoActionsListener: PhotoActionsListener
|
|
|
|
private lateinit var toolbar: View
|
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
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
private var uri = Uri.EMPTY
|
|
|
|
private var previewUri = Uri.EMPTY
|
|
|
|
private var showingPreview = false
|
|
|
|
|
2019-08-04 20:22:57 +02:00
|
|
|
override lateinit var descriptionView: TextView
|
2018-10-15 19:56:11 +02:00
|
|
|
override fun onAttach(context: Context) {
|
|
|
|
super.onAttach(context)
|
|
|
|
photoActionsListener = context as PhotoActionsListener
|
|
|
|
}
|
|
|
|
|
2019-08-04 20:22:57 +02:00
|
|
|
override fun setupMediaView(url: String, previewUrl: String?) {
|
2018-11-01 14:52:22 +01:00
|
|
|
descriptionView = mediaDescription
|
2018-12-17 15:25:35 +01:00
|
|
|
photoView.transitionName = url
|
2019-08-17 20:05:24 +02:00
|
|
|
startedTransition = false
|
2020-07-26 02:11:33 +02:00
|
|
|
uri = Uri.parse(url)
|
|
|
|
if(previewUrl != null && !previewUrl.equals(url)) {
|
|
|
|
previewUri = Uri.parse(previewUrl)
|
|
|
|
}
|
|
|
|
loadImageFromNetwork()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
|
|
toolbar = activity!!.toolbar
|
|
|
|
return inflater.inflate(R.layout.fragment_view_image, container, false)
|
|
|
|
}
|
|
|
|
|
2020-06-30 18:21:36 +02:00
|
|
|
private lateinit var gestureDetector : GestureDetector
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
private val imageOnTouchListener = object : View.OnTouchListener {
|
|
|
|
private var lastY = 0.0f
|
|
|
|
private var swipeStartedWithOneFinger = false
|
2020-06-22 21:26:37 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
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)
|
2020-06-22 21:26:37 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
if(event.pointerCount != 1) {
|
2020-06-30 18:21:36 +02:00
|
|
|
swipeStartedWithOneFinger = false
|
2020-07-26 02:11:33 +02:00
|
|
|
return false
|
2020-06-30 18:21:36 +02:00
|
|
|
}
|
2020-07-26 02:11:33 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2020-06-22 21:26:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
return false
|
|
|
|
}
|
2020-06-30 18:21:36 +02:00
|
|
|
}
|
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
|
2020-06-30 18:21:36 +02:00
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
|
|
|
gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
|
|
|
|
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
|
|
|
|
onMediaTap()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// photoView.setOnTouchListener(this)
|
2020-07-26 02:11:33 +02:00
|
|
|
photoView.setImageLoaderCallback(imageLoaderCallback)
|
2020-06-30 18:21:36 +02:00
|
|
|
photoView.setImageViewFactory(GlideImageViewFactory())
|
|
|
|
|
2020-06-22 21:26:37 +02:00
|
|
|
val arguments = this.requireArguments()
|
2018-10-15 19:56:11 +02:00
|
|
|
val attachment = arguments.getParcelable<Attachment>(ARG_ATTACHMENT)
|
2019-08-17 20:05:24 +02:00
|
|
|
this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION)
|
2018-10-15 19:56:11 +02:00
|
|
|
val url: String?
|
2019-04-16 21:39:12 +02:00
|
|
|
var description: String? = null
|
2018-10-15 19:56:11 +02:00
|
|
|
|
|
|
|
if (attachment != null) {
|
|
|
|
url = attachment.url
|
2018-11-01 14:52:22 +01:00
|
|
|
description = attachment.description
|
2018-10-15 19:56:11 +02:00
|
|
|
} else {
|
|
|
|
url = arguments.getString(ARG_AVATAR_URL)
|
|
|
|
if (url == null) {
|
|
|
|
throw IllegalArgumentException("attachment or avatar url has to be set")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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() {
|
|
|
|
if (abs(photoView.translationY) > 180) {
|
|
|
|
photoActionsListener.onDismiss()
|
|
|
|
} else {
|
|
|
|
photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-15 19:56:11 +02:00
|
|
|
private fun onMediaTap() {
|
|
|
|
photoActionsListener.onPhotoTap()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onToolbarVisibilityChange(visible: Boolean) {
|
|
|
|
if (photoView == null || !userVisibleHint) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
isDescriptionVisible = showingDescription && visible
|
|
|
|
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
|
|
|
|
descriptionView.animate().alpha(alpha)
|
|
|
|
.setListener(object : AnimatorListenerAdapter() {
|
|
|
|
override fun onAnimationEnd(animation: Animator) {
|
2018-11-01 14:52:22 +01:00
|
|
|
descriptionView.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() {
|
|
|
|
super.onDestroyView()
|
2020-06-30 18:21:36 +02:00
|
|
|
photoView.ssiv?.recycle()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
private inner class DummyCacheTarget(val ctx: Context, val requestPreview : Boolean) : CustomTarget<File>() {
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
2019-04-16 21:39:12 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2020-06-30 18:21:36 +02:00
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
private fun loadImageFromNetwork() {
|
|
|
|
Glide.with(this).asFile()
|
|
|
|
.load(uri)
|
|
|
|
.onlyRetrieveFromCache(true)
|
|
|
|
.dontAnimate()
|
|
|
|
.into(DummyCacheTarget(context!!, false))
|
2020-06-30 18:21:36 +02:00
|
|
|
}
|
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
override fun onTransitionEnd() {
|
|
|
|
// if we had preview, load full image, as transition has ended
|
|
|
|
if (showingPreview) {
|
|
|
|
showingPreview = false
|
|
|
|
photoView.loadMainImageNow()
|
|
|
|
}
|
2020-06-30 18:21:36 +02:00
|
|
|
}
|
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-30 18:21:36 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
override fun onFail(error: Exception?) {
|
|
|
|
progressBar?.hide()
|
|
|
|
}
|
2020-06-30 18:21:36 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
override fun onCacheHit(imageType: Int, image: File?) {
|
|
|
|
// image is here, bring up the activity!
|
|
|
|
photoActionsListener.onBringUp()
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2020-07-26 02:11:33 +02:00
|
|
|
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 :)
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
}
|