Merge pull request #397 from vector-im/feature/animation_image_preview

Better image fullscreen preview animation
This commit is contained in:
Valere 2019-07-22 23:37:15 +02:00 committed by GitHub
commit ab87a3caea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 249 additions and 17 deletions

View file

@ -5,7 +5,7 @@ Features:
- -
Improvements: Improvements:
- - UX image preview screen transition (#393)
Other changes: Other changes:
- -

View file

@ -35,7 +35,9 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -623,8 +625,12 @@ class RoomDetailFragment :
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator // 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) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
@ -45,6 +46,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout) contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(longClickListener) holder.imageView.setOnLongClickListener(longClickListener)
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(cellClickListener) holder.mediaContentView.setOnClickListener(cellClickListener)
holder.mediaContentView.setOnLongClickListener(longClickListener) holder.mediaContentView.setOnLongClickListener(longClickListener)
// The sending state color will be apply to the progress text // The sending state color will be apply to the progress text

View file

@ -16,11 +16,16 @@
package im.vector.riotx.features.media package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.widget.ImageView import android.widget.ImageView
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
@ -87,6 +92,55 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.transform(RoundedCorners(dpToPx(8, imageView.context))) .transform(RoundedCorners(dpToPx(8, imageView.context)))
.thumbnail(0.3f) .thumbnail(0.3f)
.into(imageView) .into(imageView)
}
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback :((Boolean) -> 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<Drawable> {
override fun onLoadFailed(e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean): Boolean {
callback?.invoke(false)
return false
}
override fun onResourceReady(resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean): Boolean {
callback?.invoke(true)
return false
}
})
.fitCenter()
.into(imageView)
} }
fun render(data: Data, imageView: BigImageView) { fun render(data: Data, imageView: BigImageView) {

View file

@ -18,15 +18,29 @@ package im.vector.riotx.features.media
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar 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.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.indicator.progresspie.ProgressPieIndicator
import com.github.piasy.biv.view.GlideImageViewFactory import com.github.piasy.biv.view.GlideImageViewFactory
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import kotlinx.android.synthetic.main.activity_image_media_viewer.* import kotlinx.android.synthetic.main.activity_image_media_viewer.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -34,6 +48,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
@Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var imageContentRenderer: ImageContentRenderer
lateinit var mediaData: ImageContentRenderer.Data
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
@ -41,11 +57,31 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(im.vector.riotx.R.layout.activity_image_media_viewer) setContentView(im.vector.riotx.R.layout.activity_image_media_viewer)
val mediaData = intent.getParcelableExtra<ImageContentRenderer.Data>(EXTRA_MEDIA_DATA) mediaData = intent.getParcelableExtra<ImageContentRenderer.Data>(EXTRA_MEDIA_DATA)
intent.extras.getString(EXTRA_SHARED_TRANSITION_NAME)?.let {
ViewCompat.setTransitionName(imageTransitionView, it)
}
if (mediaData.url.isNullOrEmpty()) { if (mediaData.url.isNullOrEmpty()) {
finish() 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 { } else {
configureToolbar(imageMediaViewerToolbar, mediaData) imageTransitionView.isVisible = false
if (mediaData.elementToDecrypt != null) { if (mediaData.elementToDecrypt != null) {
// Encrypted image // 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<Drawable> {
override fun onLoadFailed(e: GlideException?,
model: Any?,
target: Target<Drawable>?,
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<Drawable>?,
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 { companion object {
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" 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 { return Intent(context, ImageMediaViewerActivity::class.java).apply {
putExtra(EXTRA_MEDIA_DATA, mediaData) putExtra(EXTRA_MEDIA_DATA, mediaData)
putExtra(EXTRA_SHARED_TRANSITION_NAME, shareTransitionName)
} }
} }
} }

View file

@ -12,18 +12,35 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="4dp" /> android:elevation="4dp" />
<com.github.piasy.biv.view.BigImageView
android:id="@+id/imageMediaViewerImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:failureImageInitScaleType="center"
app:optimizeDisplay="true" />
<ImageView <FrameLayout
android:id="@+id/encryptedImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:visibility="gone"
tools:visibility="visible" /> <ImageView
android:id="@+id/imageTransitionView"
android:transitionName="imagePreview"
android:scaleType="fitCenter"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="visible"
android:visibility="gone"
/>
<com.github.piasy.biv.view.BigImageView
android:id="@+id/imageMediaViewerImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:failureImageInitScaleType="center"
app:optimizeDisplay="true" />
<ImageView
android:id="@+id/encryptedImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!--
The transitions which us used for the entrance and exit of shared elements. Here we declare
two different transitions which are targeting specific views.
-->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="200">
<!-- changeBounds is used for the TextViews which are shared -->
<changeBounds/>
<!-- changeImageTransform is used for the ImageViews which are shared -->
<changeImageTransform />
</transitionSet>

View file

@ -7,6 +7,11 @@
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>
<!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
</style> </style>
<style name="AppTheme.Black" parent="AppTheme.Black.v21" /> <style name="AppTheme.Black" parent="AppTheme.Black.v21" />

View file

@ -7,6 +7,11 @@
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>
<!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
</style> </style>
<style name="AppTheme.Dark" parent="AppTheme.Dark.v21" /> <style name="AppTheme.Dark" parent="AppTheme.Dark.v21" />

View file

@ -7,6 +7,10 @@
<!-- enable window content transitions --> <!-- enable window content transitions -->
<item name="android:windowContentTransitions">true</item> <item name="android:windowContentTransitions">true</item>
<!-- specify shared element enter and exit transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
</style> </style>
<style name="AppTheme.Light" parent="AppTheme.Light.v21"/> <style name="AppTheme.Light" parent="AppTheme.Light.v21"/>