Android shared element transition between 2 RecyclerViews
Asked Answered
E

1

10

I am using default shared element transitions between 2 RecyclerView items in 2 activities (MainActivity and DetailActivity). Animation from MainActivity to DetailActivity is working fine, but if user has scrolled to new item in the DetailActivity, then reenter animation shifts the item to top. I modified the sample shared on Android Developers Blog for my needs. Here's the Github Link to my code. I have also tried disabling exit animations on the DetailActivity, and tried changing exit animations to fade only, but it's almost like exit animations are not being respected at all.

Here's a video demo (issue can be seen in the last couple of seconds):

enter image description here

MainActivity:

class MainActivity : AppCompatActivity(), ListImageAdapter.ListImageClickListener {

    private lateinit var imageData: ImageData
    private lateinit var listImageAdapter: ListImageAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupGallery()
        prepareTransitions()
    }

    @SuppressLint("RestrictedApi")
    override fun onListImageClick(position: Int, imageView: ImageView) {
        val intent = Intent(this, DetailActivity::class.java)
        intent.putExtra("IMAGE_DATA", imageData)
        val activityOptions = ActivityOptions.makeSceneTransitionAnimation(this, imageView,
                ViewCompat.getTransitionName(imageView))

        startActivityForResult(intent, 101, activityOptions.toBundle())
    }

    override fun onActivityReenter(resultCode: Int, data: Intent?) {
        data?.let { intent ->
            if (intent.hasExtra("IMAGE_DATA")) {
                imageData = intent.getParcelableExtra("IMAGE_DATA")
                listImageAdapter.images = imageData.images
                val position = imageData.images.indexOfFirst { it.selected }
                itemGallery.scrollToPosition(position)

            }
        }
        super.onActivityReenter(resultCode, data)
    }

    private fun setupGallery() {
        imageData = ImageData(getGalleryItems())
        val snapHelper = PagerSnapHelper()
        snapHelper.attachToRecyclerView(itemGallery)
        listImageAdapter = ListImageAdapter(imageData.images, this)
        itemGallery.adapter = listImageAdapter
        itemGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    val selectedView = snapHelper.findSnapView(itemGallery.layoutManager)
                    selectedView?.let {
                        val selectedPosition = itemGallery.layoutManager?.getPosition(selectedView)
                        selectedPosition?.let { onMediumGalleryItemHighlighted(selectedPosition) }
                    }

                }
            }
        })

    }

    private fun onMediumGalleryItemHighlighted(position: Int) {
        imageData.images = imageData.images.mapIndexed { index, galleryItem ->
            when {
                index == position -> galleryItem.copy(selected = true)
                galleryItem.selected -> galleryItem.copy(selected = false)
                else -> galleryItem
            }
        }
    }

    private fun prepareTransitions() {

        setExitSharedElementCallback(
                object : SharedElementCallback() {
                    override fun onMapSharedElements(names: List<String>?, sharedElements: MutableMap<String, View>?) {
                        val selectedPosition = imageData.images.indexOfFirst { it.selected }
                        val selectedViewHolder = itemGallery
                                .findViewHolderForAdapterPosition(selectedPosition)
                        if (selectedViewHolder?.itemView == null) {
                            return
                        }
                        sharedElements!![names!![0]] = selectedViewHolder.itemView.findViewById(R.id.listItemImage)
                    }
                })
    }

    private fun getGalleryItems(): List<Image> {
        return listOf(
                Image(R.drawable.cat, true),
                Image(R.drawable.lion, false),
                Image(R.drawable.tortoise, false)
        )
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:animateLayoutChanges="false"
    >

    <View android:id="@+id/otherContent"
        android:layout_width="match_parent"
        android:layout_height="256dp"
        android:background="@android:color/holo_green_light"
        />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/itemGallery"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:layout_below="@+id/otherContent"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager" />


</RelativeLayout>

DetailActivity:

class DetailActivity : AppCompatActivity() {

    private lateinit var detailImageAdapter: DetailImageAdapter
    private lateinit var imageData: ImageData


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)
        imageData = intent.extras.getParcelable("IMAGE_DATA")
        initViews()
        prepareTransitions()
        resetScrolledPosition()
    }

    private fun initViews() {
        val snapHelper = PagerSnapHelper()
        snapHelper.attachToRecyclerView(detailGallery)
        detailImageAdapter = DetailImageAdapter(imageData.images)
        detailGallery.adapter = detailImageAdapter
        detailGallery.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    val selectedView = snapHelper.findSnapView(detailGallery.layoutManager)
                    selectedView?.let {
                        val selectedPosition = detailGallery.layoutManager?.getPosition(selectedView)
                        selectedPosition?.let { onItemSelected(selectedPosition) }
                    }
                }
            }
        })
    }

    private fun resetScrolledPosition() {
        val position = imageData.images.indexOfFirst { it.selected }
        imageData.images = imageData.images.mapIndexed { index, galleryItem ->
            when {
                index == position -> {
                    galleryItem.copy(selected = true)
                }
                galleryItem.selected -> galleryItem.copy(selected = false)
                else -> galleryItem
            }
        }
        detailImageAdapter.images = imageData.images
        detailGallery.scrollToPosition(position)
        supportStartPostponedEnterTransition()
    }


    private fun onItemSelected(position: Int) {
        imageData.images = imageData.images.mapIndexed { index, galleryItem ->
            when {
                index == position -> galleryItem.copy(selected = true)
                galleryItem.selected -> galleryItem.copy(selected = false)
                else -> galleryItem
            }
        }
    }

    override fun onBackPressed() {
        var resultIntent = Intent()
        resultIntent = resultIntent.putExtra("IMAGE_DATA", imageData)
        setResult(Activity.RESULT_OK, resultIntent)
        super.onBackPressed()

    }

    private fun prepareTransitions() {

        setEnterSharedElementCallback(
                object : SharedElementCallback() {
                    override fun onMapSharedElements(names: List<String>?, sharedElements: MutableMap<String, View>?) {
                        val selectedPosition = imageData.images.indexOfFirst { it.selected }
                        val selectedViewHolder = detailGallery.findViewHolderForAdapterPosition(selectedPosition)
                        if (selectedViewHolder?.itemView == null) {
                            return
                        }
                        sharedElements!![names!![0]] = selectedViewHolder.itemView.findViewById(R.id.detailItemImage)
                    }
                })
    }

}

activity_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    android:animateLayoutChanges="false"
    >
    <android.support.v7.widget.RecyclerView
        android:id="@+id/detailGallery"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager" />

</FrameLayout>
Esperanzaespial answered 13/8, 2018 at 21:32 Comment(10)
I dont kow if i just missed it but I cant find the code where when you return to the MainActivity from the DetailActivity, you change the SharedElement to the new item. I hope that makes senseKepi
just had my answer deleted by review - despite it might have been the correct answer - only because I've asked for some more details there, and I will not waste any more time on this...Archivist
@ArchieG.Quiñones: Sorry, I am unable to follow. Would you mind posting an answer ? Pseudo-code will also help. In onBackPressed() I don't have access to the shared element.Esperanzaespial
@MartinZeitler Not sure what review policies are blocking you. I can add any more details if you can ask for them in a comment. Is it possible for you to put your thoughts in a gist and share the link in the comments? If it works, I can appeal to the SO mods to accept your answer.Esperanzaespial
I would be able to help you better if you share me the code of this project. I believe the problem here is that, the upon return from the detail activity to the MainActivity, you have to re-set the shared element transition to the correct item.Kepi
@Jaguar I've asked in that answer to fork the repo on github and commit your changes there, which would have made it much easier to answer... because it appeared as if that RelativeLayout is at fault. the policy might be not to ask in answers for clarification, and no placeholder answers - while one cannot even prepare/save drafts of answers, in order not to post an answer and improve it later.Archivist
Can you post your project at github?Didache
I have updated the question with github repo for my code.Esperanzaespial
this repository had not been forked and therefore does not contain both versions. github.com/theandroiddev/SharedElementTransition/commit/… #wontfixArchivist
you just have to change the transition name at runtime of the previous recyclerview item when the recyclerview item is scrolled and next item is visible.Aurita
C
1

Please check shared element transition code on GithubLink

SharedElementTransition-master.zip is updated code from your source code in which Transition is working between two RecyclerViews.

android-gallery-master.zip is another code in which Transition is working between RecyclerView and ViewPager.

Hope it will work for you. I will add explanation soon.

Centillion answered 23/8, 2018 at 20:45 Comment(5)
Thank you! The reenter animation works perfect now. Transition from MainActivity to DetailActivity does not look as smooth as return from Detail to Main. Is it normal? It would be great if you could help fix that and add some explanation over what changes you did, because I don't see a commit history. I am marking the answer accepted for now, but hoping you will update your answer soon!Esperanzaespial
@Jaguar I will check form the MainActivity to DetailActivity transition part soon and will provide explanation soon. Transition is working very well and smooth in second demo i.e. Transition between RecyclerView and ViewPager. Have a look into it and please keep your Internet on bz images are loading from URL in it. You can use that code also as an alternate solution. Happy to help you.Centillion
Unfortunately I can't use ViewPager because it's a photo app and I may have 100s of images - otherwise Google's sample using ViewPager (that I posted in my question) was a good example to follow.Esperanzaespial
Ohk Fine. Then I will check again for Enter Transition code to make it as smooth as exit.Centillion
@Jaguar please add window.exitTransition = null in MainActivity.kt and window.enterTransition = null; in DetailActivity.kt below setContentView and check the Enter Transition works correctly or not. Let me know if you still getting problem.Centillion

© 2022 - 2024 — McMap. All rights reserved.