Original: How to Use Shared Element Transition with Glide in 4 steps: author Bart ł omiej Osma ł ek Markdown: the original |

By the end of this article, you will know how to implement shared element transitions using image loading libraries such as Glide and how to handle the various possible load states. By sharing transition animations, the application interaction experience can be improved and users are more happy to use them

The shared element transition effect is an important transition effect in Material Design. This is easy to implement if the image resource is statically local. But downloading images over the Web and creating a seamless animation is trickier.

Prior to the start

This article is a summary of transition effects during the development of Toast App. The app is the client app for TOAST — the Android Developer gathering (the largest Android developer gathering website in Poland). The app contains every TOAST event, regular lectures and event photos. We mainly use shared element transitions as page switching effects. Use Glide to get all the pictures as well.

The method described in this article should also work with other image loading libraries, such as Picasso or Fresco (you need to find the corresponding code implementation for Glide specific functionality). I made a demo application for this and posted it on Github. All code snippets are from this sample program.

We start with a grid of images. When the user clicks on an image, a new Activity opens, and the image is cropped and fills the entire screen. Here is the code for loading images using Glide:

fun ImageView.load(url: String) {
    Glide.with(this)
            .load(url)
            .apply(RequestOptions.placeholderOf(R.drawable.placeholder))
            .into(this)}Copy the code

The effect we want:

Step 1: Naive sharing transition

We can create the transition effect by adding the correct options and modifying the goToDetails method as follows: mainActivity.kt

fun goToDetails(url: String, imageView: View) {
    val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, imageView, imageView.transitionName).toBundle()
    Intent(this, DetailActivity::class.java)
            .putExtra(IMAGE_URL_KEY, url)
            .let {
                startActivity(it, options)
            }
}
Copy the code

We now pass in the view to share in the above method and set transitionName. This name must be unique within each activity, and the view corresponding to the main view and details page should have the same transitionName. Here, for simplicity, we use the image URL as transitionName: detailActivity.kt

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_detail)
    val url = intent.getStringExtra(IMAGE_URL_KEY)
    detailImage.transitionName = url
    detailImage.load(url)
}
Copy the code

ImageAdapter.kt

inner class ImageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    fun bind(url: String) {
        (itemView as ImageView).apply {
            load(url)
            transitionName = url
            setOnClickListener { onClick(url, it) }
        }
    }
}
Copy the code

Ok, we have the transition animation as follows:

The bad thing is that this is not what we want 🙁

Step 2: Delay the transition effect

Glide takes time to load the image into the ImageView. That’s why in onCreate we have to delay the transition animation until the image is downloaded:

DetailActivity.kt

override fun onCreate(savedInstanceState: Bundle?).{... supportPostponeEnterTransition() detailImage.load(url) { supportStartPostponedEnterTransition() } }Copy the code

GlideLoader.kt

fun ImageView.load(url: String, onLoadingFinished: () -> Unit= {{})val listener = object : RequestListener<Drawable> {
        override fun onLoadFailed(e: GlideException? , model:Any? , target:Target<Drawable>? , isFirstResource:Boolean): Boolean {
            onLoadingFinished()
            return false
        }

        override fun onResourceReady(resource: Drawable? , model:Any? , target:Target<Drawable>? , dataSource:DataSource? , isFirstResource:Boolean): Boolean {
            onLoadingFinished()
            return false
        }
    }
    Glide.with(this)
            .load(url)
            .apply(RequestOptions.placeholderOf(R.drawable.placeholder))
            .listener(listener)
            .into(this)}Copy the code

Now we have the image loaded before the transition, but you can see that there are strange burrs before entering the transition and after exiting the transition, which we will deal with in the next step.

Step 3: Disable conversion

This burr effect is due to the optimization made by Glide during loading. By default, Glide resizes and crops the image to match the target view. But the Android transition framework takes the image from the target view at the start of the transition and converts it to the image from the source view. We can make Glide without these optimizations: glideloader.kt

fun ImageView.load(url: String, onLoadingFinished: () -> Unit= {}) {...val requestOptions = RequestOptions.placeholderOf(R.drawable.placeholder)
            .dontTransform()
    Glide.with(this)
            .load(url)
            .apply(requestOptions)
            .listener(listener)
            .into(this)}Copy the code

We can also use the original picture size in Glide. This reduces transition latency because the original image will be in memory, not in the disk cache. Note: This operation will slow down your application, so use it with caution. If you must do this with flowers, here is a modified branch to refer to.

It is now working normally, but there is a small problem in some error links where images are not loaded yet.

Step 4: Cache only

The easiest way to make the transition seamless under any conditions is to get the image from the cache (or use a placeholder if the image is not loaded). DetailActivity.kt

override fun onCreate(savedInstanceState: Bundle?).{... detailImage.load(url, loadOnlyFromCache =true) {
        supportStartPostponedEnterTransition()
    }
}
Copy the code

GlideLoader.kt

fun ImageView.load(url: String, loadOnlyFromCache: Boolean = false, onLoadingFinished: () -> Unit= {}) {...val requestOptions = RequestOptions.placeholderOf(R.drawable.placeholder)
            .dontTransform()
            .onlyRetrieveFromCache(loadOnlyFromCache)
    ...
}
Copy the code

Of course, this means that when the user opens the details page, only the placeholder is displayed until the image has loaded. It is fixed after the transition with a second request: detailActivity.kt

override fun onCreate(savedInstanceState: Bundle?).{... window.sharedElementEnterTransition = TransitionSet() .addTransition(ChangeImageTransform()) .addTransition(ChangeBounds()) .apply { doOnEnd { detailImage.load(url) } } }Copy the code

In the end, we have a very nice transition effect that works in a variety of conditions. You can see the whole example on GitHub, and more examples on the Toast App.

conclusion

Shared element transitions provide visual continuity and keep the user focused. But we should keep in mind that our Internet connections can be poor, and frozen transitions can irritate users. I believe these four steps will help you make your application beautiful and fast in any case.

A link to the

Workcation App — Part 4. Shared Element Transition with RecyclerView and Scenes Meaningful Motion with Shared Element Transition and Circular Reveal Animation Workcation App — Part 1. Fragment Custom Transition How to Learn Android Development Programming — 6 Steps for Beginners 6 Misknowledge TDD — Part 4. There is one right Granularity of steps