diff --git a/CHANGES.md b/CHANGES.md index 1ba5b11ebd..f84f1fc503 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features: - Improvements: - - + - UX image preview screen transition (#393) Other changes: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 1ff2c67dc1..cabb479575 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -35,7 +35,9 @@ import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -623,8 +625,12 @@ class RoomDetailFragment : override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { // TODO Use navigator - val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) - startActivity(intent) + + val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), view, ViewCompat.getTransitionName(view) + ?: "").toBundle() + startActivity(intent, bundle) } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index d551e44c23..6a68557f86 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.view.ViewGroup import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -45,6 +46,7 @@ abstract class MessageImageVideoItem : AbsMessageItem Unit)? = null) { + val (width, height) = processSize(data, mode) + + val glideRequest = if (data.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(imageView) + .load(data) + } else { + // Clear image + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val resolvedUrl = when (mode) { + Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url) + Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + } + //Fallback to base url + ?: data.url + + GlideApp + .with(imageView) + .load(resolvedUrl) + } + + glideRequest + .listener(object: RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + callback?.invoke(false) + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + callback?.invoke(true) + return false + } + + }) + .fitCenter() + .into(imageView) + } fun render(data: Data, imageView: BigImageView) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index bd3f4480f2..a44672a55d 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -18,15 +18,29 @@ package im.vector.riotx.features.media import android.content.Context import android.content.Intent +import android.graphics.drawable.Drawable +import android.os.Build import android.os.Bundle +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar +import androidx.core.transition.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.transition.Transition +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.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseActivity import kotlinx.android.synthetic.main.activity_image_media_viewer.* +import timber.log.Timber import javax.inject.Inject @@ -34,6 +48,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() { @Inject lateinit var imageContentRenderer: ImageContentRenderer + lateinit var mediaData: ImageContentRenderer.Data + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -41,11 +57,31 @@ class ImageMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(im.vector.riotx.R.layout.activity_image_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + intent.extras.getString(EXTRA_SHARED_TRANSITION_NAME)?.let { + ViewCompat.setTransitionName(imageTransitionView, it) + } if (mediaData.url.isNullOrEmpty()) { finish() + return + } + + configureToolbar(imageMediaViewerToolbar, mediaData) + + if (isFirstCreation() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && addTransitionListener()) { + // Encrypted image + imageTransitionView.isVisible = true + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = false + //Postpone transaction a bit until thumbnail is loaded + supportPostponeEnterTransition() + imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) { + //Proceed with transaction + scheduleStartPostponedTransition(imageTransitionView) + } + } else { - configureToolbar(imageMediaViewerToolbar, mediaData) + imageTransitionView.isVisible = false if (mediaData.elementToDecrypt != null) { // Encrypted image @@ -78,13 +114,101 @@ class ImageMediaViewerActivity : VectorBaseActivity() { } } + override fun onBackPressed() { + //show again for exit animation + imageTransitionView.isVisible = true + super.onBackPressed() + } + + private fun scheduleStartPostponedTransition(sharedElement: View) { + sharedElement.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + sharedElement.viewTreeObserver.removeOnPreDrawListener(this) + supportStartPostponedEnterTransition() + return true + } + }) + } + + /** + * Try and add a [Transition.TransitionListener] to the entering shared element + * [Transition]. We do this so that we can load the full-size image after the transition + * has completed. + * + * @return true if we were successful in adding a listener to the enter transition + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun addTransitionListener(): Boolean { + val transition = window.sharedElementEnterTransition + + if (transition != null) { + // There is an entering shared element transition so add a listener to it + transition.addListener( + onEnd = { + if (mediaData.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(this) + .load(mediaData) + .dontAnimate() + .listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + //TODO ? + Timber.e("TRANSITION onLoadFailed") + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = true + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + Timber.e("TRANSITION onResourceReady") + imageTransitionView.isInvisible = true + imageMediaViewerImageView.isVisible = false + encryptedImageView.isVisible = true + return false + } + + }) + .into(encryptedImageView) + } else { + imageTransitionView.isInvisible = true + // Clear image + imageMediaViewerImageView.isVisible = true + encryptedImageView.isVisible = false + + imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) + imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) + imageContentRenderer.render(mediaData, imageMediaViewerImageView) + } + }, + onCancel = { + //Something to do? + } + ) + return true + } + + // If we reach here then we have not added a listener + return false + } + companion object { private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" + private const val EXTRA_SHARED_TRANSITION_NAME = "EXTRA_SHARED_TRANSITION_NAME" - fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent { + fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, shareTransitionName: String?): Intent { return Intent(context, ImageMediaViewerActivity::class.java).apply { putExtra(EXTRA_MEDIA_DATA, mediaData) + putExtra(EXTRA_SHARED_TRANSITION_NAME, shareTransitionName) } } } diff --git a/vector/src/main/res/layout/activity_image_media_viewer.xml b/vector/src/main/res/layout/activity_image_media_viewer.xml index 61d5d286f9..f1d557492f 100644 --- a/vector/src/main/res/layout/activity_image_media_viewer.xml +++ b/vector/src/main/res/layout/activity_image_media_viewer.xml @@ -12,18 +12,35 @@ android:layout_height="?attr/actionBarSize" android:elevation="4dp" /> - - + android:layout_height="match_parent"> + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/transition/image_preview_transition.xml b/vector/src/main/res/transition/image_preview_transition.xml new file mode 100644 index 0000000000..3674324c4e --- /dev/null +++ b/vector/src/main/res/transition/image_preview_transition.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/values-v21/theme_black.xml b/vector/src/main/res/values-v21/theme_black.xml index 5ab2ed8901..74ec2cd9e2 100644 --- a/vector/src/main/res/values-v21/theme_black.xml +++ b/vector/src/main/res/values-v21/theme_black.xml @@ -7,6 +7,11 @@ true + + + @transition/image_preview_transition + @transition/image_preview_transition +