Search code examples
androidandroid-animationandroid-transitionsshared-element-transition

Android shared element transition between 2 RecyclerViews


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>

Solution

  • 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.