mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #397 from vector-im/feature/animation_image_preview
Better image fullscreen preview animation
This commit is contained in:
commit
ab87a3caea
10 changed files with 249 additions and 17 deletions
|
@ -5,7 +5,7 @@ Features:
|
|||
-
|
||||
|
||||
Improvements:
|
||||
-
|
||||
- UX image preview screen transition (#393)
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
15
vector/src/main/res/transition/image_preview_transition.xml
Normal file
15
vector/src/main/res/transition/image_preview_transition.xml
Normal 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>
|
||||
|
||||
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in a new issue