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:
-
- UX image preview screen transition (#393)
Other changes:
-

View file

@ -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) {

View file

@ -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<MessageImageVideoItem.Hold
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(longClickListener)
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(cellClickListener)
holder.mediaContentView.setOnLongClickListener(longClickListener)
// The sending state color will be apply to the progress text

View file

@ -16,11 +16,16 @@
package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
import android.widget.ImageView
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.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.session.content.ContentUrlResolver
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)))
.thumbnail(0.3f)
.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) {

View file

@ -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<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()) {
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<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 {
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)
}
}
}

View file

@ -12,18 +12,35 @@
android:layout_height="?attr/actionBarSize"
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
android:id="@+id/encryptedImageView"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />
android:layout_height="match_parent">
<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>

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 -->
<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 name="AppTheme.Black" parent="AppTheme.Black.v21" />

View file

@ -7,6 +7,11 @@
<!-- enable window content transitions -->
<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 name="AppTheme.Dark" parent="AppTheme.Dark.v21" />

View file

@ -7,6 +7,10 @@
<!-- enable window content transitions -->
<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 name="AppTheme.Light" parent="AppTheme.Light.v21"/>