airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.52k stars 726 forks source link

Help needed for shared element transitions #698

Open SimonGenin opened 5 years ago

SimonGenin commented 5 years ago

Hi,

I'm currently developing a project using MvRx and epoxy. I'm having troubles to make a simple shared element transition between two fragments build with the epoxy simple controller. Whatever I do, it just doesn't seem to take place.

Somebody asked a question #579 about shared elements before, not so sure if it is closely related to my problem.

First, to help me navigate through my fragments, I have set up this extension function, based on a method found in the BaseFragment code proposed in the samples.

fun BaseMvRxFragment.navigateTo(@IdRes actionId: Int, arg: Parcelable? = null, extras : Navigator.Extras? = null ) {
    /**
     * If we put a parcelable arg in [MvRx.KEY_ARG] then MvRx will attempt to call a secondary
     * constructor on any MvRxState objects and pass in this arg directly.
     */
    val bundle = arg?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
    findNavController().navigate(actionId, bundle, null, extras)
}

Then, my first fragment is made of ModelViews, the one of interest here being RouteItem


class NiceListFragment : BaseFragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        FirebaseAuth.getInstance().currentUser ?: navigateTo(R.id.action_global_loginFragment)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    }

    override fun epoxyController() = simpleController {

        routeItem {
            id("Item 1")
            name("Balade en Paris")
            time("Dure 4h")

            clickListener { view ->
                // Prepare extras for shared element transitions
                val extras = FragmentNavigatorExtras(
                    view.findViewById<ImageView>(R.id.cover_image) to "route_item_cover_image_transition_end"
                )

                navigateTo(R.id.action_niceListFragment_to_routeDetailsFragment, null, extras)
            }
        }

        routeItem {
            id("Item 2")
            name("Découverte de la tour Eiffel")
            time("Maximum 1h")
        }

RouteItem view code

@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class RouteItem @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    private val routeName: TextView
    private val routeTime: TextView

    init {
        inflate(context, R.layout.route_item, this)
        routeName = findViewById(R.id.route_name)
        routeTime = findViewById(R.id.route_time)
        orientation = VERTICAL
    }

    @TextProp
    fun setName(name: CharSequence) {
        routeName.text = name
    }

    @TextProp
    fun setTime(time: CharSequence?) {
        route_time.text = time
    }

    @CallbackProp
    fun setClickListener(clickListener: OnClickListener?) {
        setOnClickListener(clickListener)
    }
}

The reception fragment is again build with epoxy simple controller, and look as follows


class RouteDetailsFragment : BaseFragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    override fun epoxyController(): MvRxEpoxyController = simpleController {

        headerImageComponent {
            id(1)
            name("Balade dans Paris")
        }

    }

}

So it's basically empty.

Here are the two xml layouts for my ModelViews. Notice that I set the name of the transitions on the ImageView. route_item.xml, for the index view.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical"
       tools:parentTag="android.widget.LinearLayout"
       android:background="?android:attr/selectableItemBackground"
        >

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_margin="8dp"
            android:elevation="4dp"
            android:background="@android:color/white"
            android:foreground="?android:attr/selectableItemBackground"
            >

        <ImageView
                android:id="@+id/cover_image"
                android:layout_width="match_parent"
                android:layout_height="160dp"
                android:src="@drawable/paris"
                android:scrollY="20dp"
                android:contentDescription="background"
                android:scaleType="centerCrop"
                android:transitionName="route_item_cover_image_transition_start"
                />

        <View
                android:layout_marginTop="-2dp"
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:background="@color/colorAccent"
                />

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="8dp"
                >

            <TextView
                    android:id="@+id/route_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Ballade en Paris"
                    android:textSize="18sp"
                    android:textColor="@color/grey_90"
                    />

            <TextView
                    android:id="@+id/route_time"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Dure 4h"
                    />

        </LinearLayout>

    </LinearLayout>
</merge>

And header_image_component.xml, for the details view.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:tools="http://schemas.android.com/tools"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical"
       tools:parentTag="android.widget.LinearLayout"
        >

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:elevation="4dp"
            >

        <ImageView
                android:id="@+id/cover_image"
                android:layout_width="match_parent"
                android:layout_height="160dp"
                android:src="@drawable/paris"
                android:scrollY="20dp"
                android:contentDescription="background"
                android:scaleType="centerCrop"
                android:transitionName="route_item_cover_image_transition_end"
                />

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="8dp"
                android:background="@color/colorAccent"
                >

            <TextView
                    android:id="@+id/route_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="Ballade en Paris"
                    android:textSize="18sp"
                    android:textColor="@android:color/white"
                    />

        </LinearLayout>

    </LinearLayout>
</merge>

So, with all that, there's just no transition happening. I'm not really sure what to look into, so any help maybe ?

Also, is there a better way to access epoxy inflated views than what I do here

val extras = FragmentNavigatorExtras(
    view.findViewById<ImageView>(R.id.cover_image) to "route_item_cover_image_transition_end"
)

I don't really like the idea of looking for the view on the listener, but I can't see how I could access the view in another way, everything else I tried ended up in NullPointerException.

Thanks for your help and your great libraries :-)

SimonGenin commented 5 years ago

Here are the two fragments to help you visualize the concept.

From To
device-2019-02-25-115632 device-2019-02-25-115648
elihart commented 5 years ago

Epoxy and MvRx don't influence the shared element transition at all, it works just like a normal Recyclerview for transitions, so set it up like you would for a regular recyclerview

haroldadmin commented 5 years ago

@SimonGenin I think the trick is to postpone the transition when the fragment is created, and resume it in the onPreDraw call of the RecyclerView. Here's a post by Chris Banes which illustrates how to do it: Fragment Transitions

That being said, I have always had trouble getting Shared Element Transitions working reliably between two RecyclerViews.