diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index ec3568f9..7dc237ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -35,9 +35,11 @@ import com.bumptech.glide.request.target.CustomTarget 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.loader.ImageLoader import com.github.piasy.biv.view.GlideImageViewFactory import com.keylesspalace.tusky.R import com.keylesspalace.tusky.util.withLifecycleContext +import java.io.File // https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420 @@ -53,8 +55,19 @@ fun T.makeCaptionDialog(existingDescription: String?, dialogLayout.orientation = LinearLayout.VERTICAL val imageView = BigImageView(this) - // imageView.ssiv.maxScale = 6f 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() windowManager.defaultDisplay.getMetrics(displayMetrics) @@ -97,12 +110,8 @@ fun T.makeCaptionDialog(existingDescription: String?, WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) 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() { Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() -} +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index 86948403..7da2559d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -25,11 +25,15 @@ import android.os.Bundle import android.util.Log import android.view.* import android.widget.TextView +import androidx.exifinterface.media.ExifInterface 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.CustomTarget 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.view.GlideImageViewFactory import com.keylesspalace.tusky.R @@ -42,9 +46,10 @@ import kotlinx.android.synthetic.main.fragment_view_image.* import java.io.File import java.lang.Exception import kotlin.math.abs +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouchListener { +class ViewImageFragment : ViewMediaFragment() { interface PhotoActionsListener { fun onBringUp() fun onDismiss() @@ -60,6 +65,10 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc @Volatile private var startedTransition = false + private var uri = Uri.EMPTY + private var previewUri = Uri.EMPTY + private var showingPreview = false + override lateinit var descriptionView: TextView override fun onAttach(context: Context) { super.onAttach(context) @@ -70,7 +79,11 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc descriptionView = mediaDescription photoView.transitionName = url 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 { @@ -78,51 +91,53 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc 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 - } + private val imageOnTouchListener = object : View.OnTouchListener { + private var lastY = 0.0f + private var swipeStartedWithOneFinger = false - var result = false + 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) - when(event.action) { - MotionEvent.ACTION_DOWN -> { - swipeStartedWithOneFinger = true - lastY = event.rawY - } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if(event.pointerCount != 1) { onGestureEnd() swipeStartedWithOneFinger = false + return 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 + + 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 + return false + } } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -135,7 +150,7 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc }) // photoView.setOnTouchListener(this) - photoView.setImageLoaderCallback(this) + photoView.setImageLoaderCallback(imageLoaderCallback) photoView.setImageViewFactory(GlideImageViewFactory()) val arguments = this.requireArguments() @@ -190,33 +205,97 @@ class ViewImageFragment : ViewMediaFragment(), ImageLoader.Callback, View.OnTouc photoView.ssiv?.recycle() } - private fun loadImageFromNetwork(url: String, previewUrl: String?) { - photoView.showImage(Uri.parse(previewUrl), Uri.parse(url)) - } + private inner class DummyCacheTarget(val ctx: Context, val requestPreview : Boolean) : CustomTarget() { + 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 onSuccess(image: File?) { - progressBar?.hide() // Always hide the progress bar on success - photoActionsListener.onBringUp() - photoView.ssiv?.setOnTouchListener(this) + override fun onResourceReady(resource: File, transition: Transition?) { + showingPreview = requestPreview + if(requestPreview) { + // have preview cached but not full image + photoView.showImage(previewUri, uri, true) + } else { + photoView.showImage(uri) + } + } } - override fun onFail(error: Exception?) { - progressBar?.hide() - photoActionsListener.onBringUp() + private fun loadImageFromNetwork() { + if(previewUri != Uri.EMPTY) { + // check if we have full image in the cache, if yes, use it + // if not, look for preview in cache and use it if available + // if not, load full image anyway + Glide.with(this).asFile() + .load(uri) + .onlyRetrieveFromCache(true) + .dontAnimate() + .into(DummyCacheTarget(context!!, false)) + } else { + // no need in cache lookup, just load full image + showingPreview = false + photoView.showImage(uri) + } } - override fun onCacheHit(imageType: Int, image: File?) { + override fun onTransitionEnd() { + // if we had preview, load full image, as transition has ended + if (showingPreview) { + showingPreview = false + photoView.loadMainImageNow() + } } - override fun onCacheMiss(imageType: Int, image: File?) { - } + 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 onFinish() { - } + override fun onFail(error: Exception?) { + progressBar?.hide() + } - override fun onProgress(progress: Int) { - } + override fun onCacheHit(imageType: Int, image: File?) { + // image is here, bring up the activity! + photoActionsListener.onBringUp() + } - override fun onTransitionEnd() { + 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 :) + } } }