Epoxy: Random Glide images disappear on scroll

Created on 19 Jul 2019  路  13Comments  路  Source: airbnb/epoxy

Hi,

I built a view with several carousel of the same type. The viewholder contains an Glide image and a title. All the view holder are displayed correctly with the image and the title at first. But when I scroll down and then scroll up, a few random viewholders that are created again only display the title, not the image. I have to scroll down and up to make the image appear again. Unfortunately, other random viewholders encounter that issue when I do that. All carousels and viewholders have different id. Note that the viewholder relies on data binding and the image is loaded through a binding adapter.

Do you have any idea regarding the cause of this issue ?

Thanks

Most helpful comment

Hello everyone, in my case the problem disappears if you do not use transition from the androidX package. There is a problem with androidx.transition.ChangeBounds() from the androidX package.
sharedElementEnterTransition = android.transition.Transition() -solved my problem

All 13 comments

You are likely not recycling views correctly and/or not canceling glide loads when the view is recycled.

Thank you for your reply. I realized that Glide is not the cause of this issue. I applied the same drawable resource to all the ImageView without Glide, and the issue was still reproductible.

It appears that it is related to animations because when I disable Shared Element Transitions, the issue can't be reproduced. Howerver, I wasn't using Epoxy for this view and the Shared Element Transitions worked perfectly.

I noticed that this issue can't be reproduced when the view is launched for the first time. However, if I click on a viewholder, it opens another fragment with a shared element transitoon and when I press back or up, it returns to the view with the carousel and the issue appears. Some random viewholders have no image and scrolling up and down change the viewholders that are concerned by the issue. Note that each viewholder has a different transition name passed through a databinding value from the Controller.

Given that the Shared Element Transitions were working perfectly before I switch to Epoxy, I guess that there is extra work to do or a specific management of back navigation when using Shared Element Transitions with Epoxy.

Do you have any avenue to solve this issue ? Thanks

The same issue.

I've never seen this issue personally, and I'm not sure what is going wrong.

Epoxy isn't related to shared image transitions, there isn't anything we can do for you there. Epoxy is just a means to set data on your view - this isn't any different than manually binding the viewholder so it's just a matter of making sure you bind your view correctly.

Ok I have created a simple project that reproduces the issue to be clearer : https://github.com/corentin521/epoxy_sample

As a picture often speaks better than words, here is a demonstration of the issue :

Let me introduce the key parts of that project :

The first fragment contains an epoxy recyclerview which displays carousels :

class FirstFragment : Fragment() {

    private lateinit var recyclerView: EpoxyRecyclerView

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_first, container, false)

        recyclerView = view.findViewById(R.id.recycler_view)

        postponeEnterTransition()
        recyclerView.doOnPreDraw {
            startPostponedEnterTransition()
        }

        recyclerView.withModels {
            for (i in 0 until 10) {
                carousel {
                    id("carousel $i")
                    models(mutableListOf<ItemBindingModel_>().apply {
                        val lastPage = 10
                        for (j in 0 until lastPage) {
                            add(
                                ItemBindingModel_()
                                    .id("carousel $i-$j")
                                    .transitionName("item-$i-$j")
                                    .clickListener { view ->
                                        val direction = FirstFragmentDirections.showDetails("item-$i-$j")
                                        val image = view.findViewById<ImageView>(R.id.item_image)

                                        val extras = FragmentNavigatorExtras(
                                            image to "item-$i-$j"
                                        )
                                        findNavController().navigate(direction, extras)
                                    }
                            )
                        }
                    })
                }
            }
        }

        return view
    }
}

The second fragment only set the correct transition name

class SecondFragment : Fragment() {

    private lateinit var binding: FragmentSecondBinding
    private val args: SecondFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentSecondBinding.inflate(inflater, container, false)

        sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)

        ViewCompat.setTransitionName(binding.itemImage, args.transitionName)
        return binding.root
    }
}

Here is the model :

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
                name="clickListener"
                type="android.view.View.OnClickListener"/>
        <variable
                name="transitionName"
                type="String"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{clickListener}"
            android:paddingLeft="6dp"
            android:paddingTop="8dp"
            android:paddingRight="6dp"
            android:paddingBottom="8dp">

        <ImageView
                android:id="@+id/item_image"
                android:layout_width="100dp"
                android:layout_height="0dp"
                android:scaleType="centerCrop"
                android:transitionName="@{transitionName}"
                android:src="@drawable/cat_1"
                app:layout_constraintDimensionRatio="2:3"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

        <TextView
                android:id="@+id/item_title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:text="Title"
                app:layout_constraintEnd_toEndOf="@id/item_image"
                app:layout_constraintRight_toRightOf="@id/item_image"
                app:layout_constraintStart_toStartOf="@+id/item_image"
                app:layout_constraintTop_toBottomOf="@+id/item_image"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here is how I bind the view. It seems to be an issue that happens when the epoxy controller has already been set up. I hope the problem is clearer.

I encountered the same issue after migrating from regular RecyclerView to an EpoxyRecyclerVieW. There wasn't any error at the end of the back shared element transition with the regular RecyclerView.

@corentin521 can you confirm if the issue goes away if you keep the same EpoxyController code, but switch to using vanilla RecyclerView instead of EpoxyRecyclerView?

I confirm that it works well with vanilla RecyclerView, the images do not disappear. I created a branch with the vanilla RecyclerView : https://github.com/corentin521/epoxy_sample/tree/vanilla_recyclerview
Did you notice something wrong in my code with EpoxyRecyclerView ? It seems that the EpoxyRecyclerView state isn't saved correctly.

I'm facing the same problem for shared elements case. the issue will be gone if I use regular recyclerView instead of EpoxyRecyclerView. and if I use regular recyclerView the ItemModel in Carousel, which I override as below won't save state anymore.

override fun shouldSaveViewState() = true

I have the same issue as described above. The same conditions. Can't really figure out what's wrong. No errors, no clues in logcat. Any progress on here?

I have a hunch what the problem could be and maybe it will help somebody to find the solution:
All views which are not shown have their transitionAlpha set to 0.0, I think that happens when the shared element transition is triggered. When the view is recycled after that, the attribute is not reset for this view and thus it is invisible.
One fast workaround is to set the transitionAlpha attribute onBind back to 1.0, this setter was added on SDK29, I'm not sure what to do before that because this error happens also for SDKs < 29.
For my tests the error is not reproducible with a RecyclerView.

That does sound like it could be the case, which I suppose would either be a RecyclerView or shared element bug? Resetting the transitionAlpha on unbind, or possibly in onFailedToRecycle might be better than bind, if those work. I'm not sure how to handle the case before API29 though

Hello everyone, in my case the problem disappears if you do not use transition from the androidX package. There is a problem with androidx.transition.ChangeBounds() from the androidX package.
sharedElementEnterTransition = android.transition.Transition() -solved my problem

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jamesarich picture jamesarich  路  5Comments

JasonHezz picture JasonHezz  路  5Comments

qbait picture qbait  路  3Comments

TonnyL picture TonnyL  路  5Comments

AdamMc331 picture AdamMc331  路  6Comments