Use cached preview as thumbnail in ViewImageFragment, fix #1267 (#1344)

* Use cached preview as thumbnail in ViewImageFragment, fix #1267

* Use cached preview as thumbnail in ViewImageFragment, fix #1267
This commit is contained in:
Ivan Kupalov 2019-08-04 20:22:57 +02:00 committed by Konrad Pozniak
parent 70b3ce7487
commit 9805a985b2
7 changed files with 164 additions and 111 deletions

View file

@ -29,21 +29,22 @@ import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import androidx.core.content.FileProvider
import androidx.viewpager.widget.ViewPager
import android.transition.Transition
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@ -53,14 +54,14 @@ import com.uber.autodispose.autoDisposable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_view_media.*
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.ArrayList
import java.util.*
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener {
companion object {
@ -84,25 +85,18 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
}
var isToolbarVisible = true
private set
private var attachments: ArrayList<AttachmentViewData>? = null
private var toolbarVisible = true
private val toolbarVisibilityListeners = ArrayList<ToolbarVisibilityListener>()
interface ToolbarVisibilityListener {
fun onToolbarVisiblityChanged(isVisible: Boolean)
}
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
this.toolbarVisibilityListeners.add(listener)
listener.onToolbarVisiblityChanged(toolbarVisible)
listener(isToolbarVisible)
return { toolbarVisibilityListeners.remove(listener) }
}
fun isToolbarVisible(): Boolean {
return toolbarVisible
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_media)
@ -113,7 +107,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS)
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
val adapter = if (attachments != null) {
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
// but it cannot be expressed and if I don't specify type explicitly compilation fails
// (probably a bug in compiler)
val adapter: PagerAdapter = if (attachments != null) {
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
// Setup the view pager.
ImagePagerAdapter(supportFragmentManager, realAttachs, initialPosition)
@ -154,6 +151,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
window.statusBarColor = Color.BLACK
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
override fun onTransitionEnd(transition: Transition) {
(adapter as SharedElementTransitionListener).onTransitionEnd()
window.sharedElementEnterTransition.removeListener(this)
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -178,20 +181,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
override fun onPhotoTap() {
toolbarVisible = !toolbarVisible
isToolbarVisible = !isToolbarVisible
for (listener in toolbarVisibilityListeners) {
listener.onToolbarVisiblityChanged(toolbarVisible)
}
val visibility = if (toolbarVisible) {
View.VISIBLE
} else {
View.INVISIBLE
}
val alpha = if (toolbarVisible) {
1.0f
} else {
0.0f
listener(isToolbarVisible)
}
val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE
val alpha = if (isToolbarVisible) 1.0f else 0.0f
toolbar.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
@ -327,3 +322,24 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
shareFile(file, mimeType)
}
}
interface SharedElementTransitionListener {
fun onTransitionEnd()
}
interface NoopTransitionListener : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {
}
override fun onTransitionResume(transition: Transition) {
}
override fun onTransitionPause(transition: Transition) {
}
override fun onTransitionCancel(transition: Transition) {
}
override fun onTransitionStart(transition: Transition) {
}
}

View file

@ -30,14 +30,15 @@ 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
import com.github.chrisbanes.photoview.PhotoViewAttacher
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_image.*
import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() {
interface PhotoActionsListener {
@ -49,35 +50,35 @@ class ViewImageFragment : ViewMediaFragment() {
private lateinit var attacher: PhotoViewAttacher
private lateinit var photoActionsListener: PhotoActionsListener
private lateinit var toolbar: View
override lateinit var descriptionView: TextView
private var transition = BehaviorSubject.create<Unit>()
override lateinit var descriptionView: TextView
override fun onAttach(context: Context) {
super.onAttach(context)
photoActionsListener = context as PhotoActionsListener
}
override fun setupMediaView(url: String) {
override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription
photoView.transitionName = url
attacher = PhotoViewAttacher(photoView)
attacher = PhotoViewAttacher(photoView).apply {
// Clicking outside the photo closes the viewer.
setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
setOnClickListener { onMediaTap() }
// Clicking outside the photo closes the viewer.
attacher.setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
attacher.setOnClickListener { onMediaTap() }
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo
* mostly fills the screen so clicking outside is difficult. */
attacher.setOnSingleFlingListener { _, _, velocityX, velocityY ->
var result = false
if (Math.abs(velocityY) > Math.abs(velocityX)) {
photoActionsListener.onDismiss()
result = true
/* 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
}
result
}
loadImageFromNetwork(url, photoView)
loadImageFromNetwork(url, previewUrl, photoView)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -103,7 +104,7 @@ class ViewImageFragment : ViewMediaFragment() {
}
}
finalizeViewSetup(url, description)
finalizeViewSetup(url, attachment?.previewUrl, description)
}
private fun onMediaTap() {
@ -131,49 +132,71 @@ class ViewImageFragment : ViewMediaFragment() {
super.onDestroyView()
}
private fun loadImageFromNetwork(url: String, photoView: ImageView) =
//Request image from the any cache
Glide.with(this)
.load(url)
.dontAnimate()
.onlyRetrieveFromCache(true)
.error(
//Request image from the network on fail load image from cache
Glide.with(this)
.load(url)
.centerInside()
.addListener(ImageRequestListener(false))
)
.centerInside()
.addListener(ImageRequestListener(true))
.into(photoView)
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)
.centerInside()
.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))
)
.centerInside()
.addListener(ImageRequestListener(true, isThumnailRequest = false))
.into(photoView)
}
/**
* @param isCacheRequest - is this listener for request image from cache or from the network
*/
private inner class ImageRequestListener(private val isCacheRequest: Boolean) : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
if (isCacheRequest) //Complete the transition on failed image from cache
completeTransition()
else
progressBar?.hide() //Hide progress bar only on fail request from internet
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 {
// If cache for full image failed, complete transition
if (isCacheRequest && !isThumnailRequest) photoActionsListener.onBringUp()
// Hide progress bar only on fail request from internet
if (!isCacheRequest) progressBar?.hide()
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
progressBar?.hide() //Always hide the progress bar on success
resource?.let {
target?.onResourceReady(resource, null)
if (isCacheRequest) completeTransition() //Complete transition on cache request only, because transition already completed on Network request
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 (isThumnailRequest) {
photoView.post {
target.onResourceReady(resource, null)
photoActionsListener.onBringUp()
}
} else {
transition
.take(1)
.subscribe {
target.onResourceReady(resource, null)
photoActionsListener.onBringUp()
}
}
return true
}
return false
}
}
private fun completeTransition() {
attacher.update()
photoActionsListener.onBringUp()
override fun onTransitionEnd() {
this.transition.onNext(Unit)
}
}

View file

@ -18,25 +18,29 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle
import android.text.TextUtils
import android.widget.TextView
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.visible
abstract class ViewMediaFragment : BaseFragment() {
abstract class ViewMediaFragment : BaseFragment(), SharedElementTransitionListener {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView(url: String)
abstract fun setupMediaView(url: String, previewUrl: String?)
abstract fun onToolbarVisibilityChange(visible: Boolean)
abstract val descriptionView : TextView
abstract val descriptionView: TextView
protected var showingDescription = false
protected var isDescriptionVisible = false
companion object {
@JvmStatic protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
@JvmStatic protected val ARG_ATTACHMENT = "attach"
@JvmStatic protected val ARG_AVATAR_URL = "avatarUrl"
@JvmStatic
protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
@JvmStatic
protected val ARG_ATTACHMENT = "attach"
@JvmStatic
protected val ARG_AVATAR_URL = "avatarUrl"
@JvmStatic
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
@ -66,21 +70,20 @@ abstract class ViewMediaFragment : BaseFragment() {
}
}
protected fun finalizeViewSetup(url: String, description: String?) {
protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) {
val mediaActivity = activity as ViewMediaActivity
setupMediaView(url)
setupMediaView(url, previewUrl)
descriptionView.text = description ?: ""
showingDescription = !TextUtils.isEmpty(description)
isDescriptionVisible = showingDescription
descriptionView.visible(showingDescription && mediaActivity.isToolbarVisible())
descriptionView.visible(showingDescription && mediaActivity.isToolbarVisible)
toolbarVisibiltyDisposable = (activity as ViewMediaActivity).addToolbarVisibilityListener(object: ViewMediaActivity.ToolbarVisibilityListener {
override fun onToolbarVisiblityChanged(isVisible: Boolean) {
onToolbarVisibilityChange(isVisible)
}
})
toolbarVisibiltyDisposable = (activity as ViewMediaActivity)
.addToolbarVisibilityListener { isVisible ->
onToolbarVisibilityChange(isVisible)
}
}
override fun onDestroyView() {

View file

@ -26,7 +26,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import android.widget.TextView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
@ -56,7 +55,7 @@ class ViewVideoFragment : ViewMediaFragment() {
}
if (isVisibleToUser) {
if (mediaActivity.isToolbarVisible()) {
if (mediaActivity.isToolbarVisible) {
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
}
videoPlayer.start()
@ -68,7 +67,7 @@ class ViewVideoFragment : ViewMediaFragment() {
}
@SuppressLint("ClickableViewAccessibility")
override fun setupMediaView(url: String) {
override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription
val videoView = videoPlayer
videoView.transitionName = url
@ -114,7 +113,7 @@ class ViewVideoFragment : ViewMediaFragment() {
throw IllegalArgumentException("attachment has to be set")
}
url = attachment.url
finalizeViewSetup(url, attachment.description)
finalizeViewSetup(url, attachment.previewUrl, attachment.description)
}
override fun onToolbarVisibilityChange(visible: Boolean) {
@ -139,4 +138,7 @@ class ViewVideoFragment : ViewMediaFragment() {
handler.removeCallbacks(hideToolbar)
}
}
override fun onTransitionEnd() {
}
}

View file

@ -3,12 +3,10 @@ package com.keylesspalace.tusky.pager
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.fragment.ViewMediaFragment
import java.lang.IllegalStateException
class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avatarUrl: String) : FragmentPagerAdapter(fragmentManager) {
class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avatarUrl: String) : FragmentPagerAdapter(fragmentManager), SharedElementTransitionListener {
override fun getItem(position: Int): Fragment {
return if (position == 0) {
ViewMediaFragment.newAvatarInstance(avatarUrl)
@ -19,4 +17,6 @@ class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avat
override fun getCount() = 1
override fun onTransitionEnd() {
}
}

View file

@ -1,20 +1,26 @@
package com.keylesspalace.tusky.pager
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewMediaFragment
import java.lang.IllegalStateException
import java.util.Locale
import java.util.*
class ImagePagerAdapter(
fragmentManager: FragmentManager,
private val attachments: List<Attachment>,
private val initialPosition: Int
) : FragmentStatePagerAdapter(fragmentManager) {
) : FragmentStatePagerAdapter(fragmentManager), SharedElementTransitionListener {
private var primaryItem: ViewMediaFragment? = null
override fun setPrimaryItem(container: ViewGroup, position: Int, item: Any) {
super.setPrimaryItem(container, position, item)
this.primaryItem = item as ViewMediaFragment
}
override fun getItem(position: Int): Fragment {
return if (position >= 0 && position < attachments.size) {
@ -31,4 +37,8 @@ class ImagePagerAdapter(
override fun getPageTitle(position: Int): CharSequence {
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size)
}
override fun onTransitionEnd() {
primaryItem?.onTransitionEnd()
}
}

View file

@ -5,7 +5,6 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK