diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 93b69e0a..8806ae43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -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 } } - private var attachments: ArrayList? = null + var isToolbarVisible = true + private set - private var toolbarVisible = true - private val toolbarVisibilityListeners = ArrayList() - - interface ToolbarVisibilityListener { - fun onToolbarVisiblityChanged(isVisible: Boolean) - } + private var attachments: ArrayList? = null + private val toolbarVisibilityListeners = mutableListOf() fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0 { 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) { + } +} 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 dce1ff91..1f6596c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -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() + 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) - - // 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 + attacher = PhotoViewAttacher(photoView).apply { + // Clicking outside the photo closes the viewer. + setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() } + 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. */ + 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 { - override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, 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 { + + override fun onLoadFailed(e: GlideException?, model: Any, target: Target, + 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?, 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, + 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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index 3d88753a..fcc8ddae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -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? = 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() { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 429ac9e7..fd907c20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -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() { + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt index a5950f8d..1beab552 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AvatarImagePagerAdapter.kt @@ -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() { + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt index 794c0dad..55e5b25c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt @@ -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, 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() + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index b682c664..d32cf6b2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -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