FragmentStateAdapter and ViewPager2 is loading fragment slower than FragmentStatePagerAdapter and ViewPager - android

In my app, am showing list of month calendar which can be scrollable to previous and next month. While swiping to previous and next, my fragments are loading smoothly and without any delay if I use FragmentStatePagerAdapter and ViewPager but it's now deprecated. So, I updated
ViewPager to ViewPager2 
FragmentStatePagerAdapter to FragmentStateAdapter 
After updating, I can see there is a delay in loading the fragments while swiping previous and next. But the same code works smoothly with FragmentStatePagerAdapter and ViewPager.
I also tried mViewPager2.setOffscreenPageLimit(//Interger Value//) but no help.
Please assist me.
fragment_employee_months_holder
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/fragment_months_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true" />
<ProgressBar
android:id="#+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:backgroundTint="#color/colorPrimary" />
</RelativeLayout>
EmployeeSummaryAdapter
class EmployeeSummaryAdapter(
fragment: Fragment,
private val mCodes: List<String>,
private val mListener: NavigationListener,
private val progressBarInterface: ProgressBarInterface
) : FragmentStateAdapter(fragment) {
private val mFragments = SparseArray<EmployeeSummaryFragment>()
override fun createFragment(position: Int): EmployeeSummaryFragment {
val bundle = Bundle()
val code = mCodes[position]
bundle.putString(DAY_CODE, code)
val fragment = EmployeeSummaryFragment(progressBarInterface)
fragment.arguments = bundle
fragment.listener = mListener
mFragments.put(position, fragment)
return fragment
}
override fun getItemCount(): Int = mCodes.size
override fun getItemId(position: Int): Long {
return position.toLong()
}
}
EmployeeSummaryFragmentHolder
class EmployeeSummaryFragmentHolder : MyFragmentHolder(), NavigationListener, ProgressBarInterface {
private var viewPager2: ViewPager2? = null
private lateinit var mEmployeeSummaryAdapter: EmployeeSummaryAdapter
private lateinit var mView: View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentDayCode = Formatter.getTodayCode()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mView = inflater.inflate(R.layout.fragment_employee_months_holder, container, false)
viewPager2 = mView.fragment_months_viewpager
viewPager2!!.id = (System.currentTimeMillis() % 100000).toInt()
Handler(Looper.getMainLooper()).postDelayed({
setupFragment()
}, 0)
return mView
}
private fun setupFragment() {
val codes = getMonths(currentDayCode)
mEmployeeSummaryAdapter = EmployeeSummaryAdapter(this, codes, this, this)
viewPager2.apply {
viewPager2!!.adapter = mEmployeeSummaryAdapter
val myPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) { }
override fun onPageScrolled(position: Int, positionOffset: Float,
positionOffsetPixels: Int) { }
override fun onPageSelected(position: Int) { }
}
viewPager2!!.registerOnPageChangeCallback(myPageChangeCallback)
}
}
}
EmployeeSummaryFragment
class EmployeeSummaryFragment(val progressBarInterface: ProgressBarInterface) :
Fragment(), MonthlyCalendar {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_employee_month, container, false)
...
...
return view
}
override fun onResume() {
super.onResume()
...
...
}
}

I tried to look into your code and I didn't find anything suspicious, but you definitely need to set your mViewPager2.setOffscreenPageLimit(//Interger Value//) into your EmployeeSummaryFragmentHolder.
viewpager2.offscreenPageLimit = 2

Related

Why is my Viewpager not showing any fragments after opening the second time and why can childfragmentmanager not be used?

I have a Fragment (InnerFragment) placed inside a Viewpager (MainViewPager).
This Fragment (InnerFragment) does also contain a ViewPager (NestedViewPager) with multiple Fragments. When I swipe or open the InnerFragment everything works fine and the NestedViewPager shows all Fragments the way it should.
When I leave the InnerFragment after swiping the MainViewPager and go back to the InnerFragment it is blank and nothing shows up.
One solution as described in the internet is using the childfragmentmanager. Unfortunately this doesn't work because if I do so following exception is thrown.
java.lang.IllegalStateException: Fragment ProductImageFragment{c458f99 (1213d869-3715-4541-8nab-f87cyc350630) id=0x8f093165 android:switcher:2111xxxxxx:0} declared target fragment ProductImagesFragment{4b4f25e (99a6aaf6-5500-4821-902f-7bf30f87554c) id=0x7f090126 android:switcher:2131296550:2} that does not belong to this FragmentManager!
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:805)
Maybe it is also important to note, that the MainFragment is implementing an Interface of the Innerfragment.
MainFragment
class ProductImagesFragment : Fragment(),
ProductImageFragment.IProductImageHandler,
ProductImageUploadDialog.ProductImageUploadDialogEventListener {
private lateinit var viewPager: ViewPager
interface IProductImageUploadHandler{
fun onImageUploadToServerFinished(bitmap: Bitmap, imageData: ProductImage)
}
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
return layoutInflater.inflate(R.layout.fragment_product_images,container,false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.viewPagerImages)
setViewPagerAdapter()
}
private fun setViewPagerAdapter(){
val manager = fragmentManager
if(manager!=null){
viewPager.adapter = product?.let {product ->
ImageAdapter(this,
product,productImageList,manager)
}
}
}
class ImageAdapter(private val productImagesFragment: ProductImagesFragment, private val product: Product, private val imagesList:ArrayList<ProductImage>, fragmentManager: FragmentManager) : FragmentPagerAdapter(fragmentManager) {
override fun getCount(): Int {
return imagesList.size
}
override fun getItem(position: Int): Fragment {
val fragment = ProductImageFragment()
val bundle = Bundle()
val image = imagesList[position]
bundle.putInt("productId",product.id)
bundle.putParcelable("productImage",image)
bundle.putInt("maxImages",imagesList.size)
fragment.setTargetFragment(productImagesFragment,1)
fragment.arguments = bundle
return fragment
}
}
}
InnerFragment
class ProductImageFragment : Fragment() {
private lateinit var productImageHandler: IProductImageHandler
interface IProductImageHandler{
fun onImageDeleted(id: Int)
fun onOrderChanged(url: String, newValue: Int, oldValue: Int)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
productImageHandler = targetFragment as IProductImageHandler
}catch (exception: Exception){
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_product_image, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
}
private fun setValues(view: View){
}
}
You must use the childFragmentManager in order for the state to be restored properly.
You shouldn't be using the target fragment API at all. In fact, if you're using the childFragmentManager, your child fragments already have a reference to the parent via requireParentFragment() - just use that instead of targetFragment.
override fun onAttach(context: Context) {
super.onAttach(context)
try {
productImageHandler = requireParentFragment() as IProductImageHandler
}catch (exception: Exception){
}
}

Swip ViewPager2 is slow

I have 7 fragments, I swip between them but it's slow, I did a test adding a fragment that just contains a textView, the swip to this fragment is fast, the difference between my fragments and this one is the amount of components inside.
class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
val fragments:ArrayList<Fragment> = arrayListOf(
Fragment_Onboarding_page1(),
Fragment_Onboarding_page2(),
Fragment_Onboarding_page3(),
Fragment_Onboarding_page4(),
Fragment_Onboarding_page5(),
Fragment_Onboarding_page6(),
Fragment_page6_be_yourself()
)
override fun getItemCount(): Int {
return fragments.size
}
override fun createFragment(position: Int): Fragment {
return fragments[position]
}
}
class EcranAuthentification : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_view_pager)
supportActionBar?.hide()
initViewPager2WithFragments()
}
private fun initViewPager2WithFragments()
{
var viewPager2: ViewPager2 = findViewById(R.id.viewpager)
var adapter = ViewPagerAdapter(supportFragmentManager,lifecycle)
viewPager2.adapter = adapter
}
}
class Fragment_Onboarding_page2 : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView=inflater.inflate(R.layout.fragment__onboarding_page2, container, false)
val viewpager=activity?.findViewById<ViewPager2>(R.id.viewpager)
rootView.skip2.setOnClickListener {
viewpager?.currentItem=6
}
rootView.boutonRetour.setOnClickListener {
viewpager?.currentItem=0
}
return rootView
}
}
Please can you help me

Migrating from ViewPager to ViewPager2 with FragmentStateAdapter causes visible screen yank and StrictMode to be triggered

I've followed the Android documentation and this tutorial to migrate to ViewPager2. When swiping the 4 screens from left to right, there is a visible screen yank which is not a great user experience. I've used strictmode to measure the performance.
The documentation explicitly mentions that createFragment must provide a new Fragment each time so the implementation seems to be implemented correct.
MainActivity:
private fun initializeViewPager(items: List<Item>) {
val tabAdapter = TabAdapter(this, items)
viewPager2.adapter = tabAdapter
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
tab.text = items[position].abbreviation
}.attach()
class TabAdapter(activity: AppCompatActivity, private val items: List<Item>) :
FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return items.size
}
override fun getItemId(position: Int): Long {
return items[position].getId()
}
override fun createFragment(position: Int): Fragment {
return VersesFragment.getInstance(items[position])
}
}
SomeFragment:
class SomeFragment : DaggerFragment() {
var adapter: ItemAdapter? = null
private lateinit var binding: FragmentSomeBinding
private lateinit var item: Item
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleArguments()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentSomeBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//
.. Setting of view elements using the serialized item
//
}
private fun handleArguments() {
val args = arguments
?: throw NullPointerException("getArguments() seems to be null. ARG_BIBLETRANSLATION needed!")
if (!args.containsKey(ARG_BIBLETRANSLATION)) throw NullPointerException("getArguments() does not contain ARG_BIBLETRANSLATION!")
item = args[ARG_BIBLETRANSLATION] as Item
}
companion object {
const val ARG_MY_ITEM = "arg_my_item"
fun getInstance(item: Item): Fragment {
val fragment = VersesFragment()
fragment.arguments = Bundle(2).apply {
putSerializable(ARG_MY_ITEM, item)
}
return fragment
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</layout>
Just for completeness, here is the original code that does perform fine:
MainActivity:
val adapter = SomePagerAdapter(items as ArrayList<Item>)
viewPager.adapter = sdapter
SomePagerAdapter:
class SomePagerAdapter(private val items: ArrayList<Item>) : PagerAdapter() {
override fun instantiateItem(container: ViewGroup, position: Int): Any {
// Same code as in SomeFragment
}
}
The tutorial from raywenderlich is super simple, but when enabling
strictmode, it does get triggered, while strictmode does not get triggered with the old viewpager and the same code:
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
StrictMode.VmPolicy.Builder()
.detectNonSdkApiUsage()
.detectAll()
}
}
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:viewpager2/viewpager2/src/main/java/androidx/viewpager2/adapter/FragmentStateAdapter.java
it seems the adapter caches fragments but does not pre-init views. forward scroll is janky, backward works fine. 1-> 2 -> 3 slow, 3 ->2 ->1 fine

System services not available to Activities before onCreate(), with Android RecyclerView inside a fragment Kotlin

I'm having trouble with Android RecyclerView in Kotlin inside a fragment who is in Home activity.
Here's my code:
myFragmentLayout:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvChapterList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
myFragmentCode:
val chaptersList: ArrayList<String> = ArrayList()
private lateinit var layoutManager: RecyclerView.LayoutManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.courses_layout, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
chaptersList.add("Android")
chaptersList.add("Kotlin")
chaptersList.add("RecyclerView")
layoutManager = LinearLayoutManager(context)
rvChapterList.layoutManager = layoutManager
rvChapterList.adapter = ChapterAdapter(Home(), chaptersList)
}
and myChapterAdapter:
class ChapterAdapter(private val context: Home, private val chaptersList: ArrayList<String>) : RecyclerView.Adapter<ChapterAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.list_item, parent, false))
}
override fun getItemCount(): Int {
return chaptersList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.chapterName?.text = chaptersList.get(position)
holder.itemView.setOnClickListener {
Toast.makeText(context, chaptersList.get(position), Toast.LENGTH_LONG).show()
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val chapterName = view.tvChapterName
}
}
It got this code from this tutorial, it's working inside an activity but not iside a fragment.
rvChapterList.adapter = ChapterAdapter(Home(), chaptersList)
Never create an Activity or other type of Context yourself. You always obtain them from the framework.
Replace that with:
rvChapterList.adapter = ChapterAdapter(requireActivity(), chaptersList)
and change the context property in ChapterAdapter to be a Context or Activity, not Home.

How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?

I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.
There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.
Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.
code sample:
transition start
class TransitionStartFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_transition_start, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val testData = listOf("one", "two", "three")
val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
recyclerView.adapter = adapter
val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
button.setOnClickListener { transitionWithTextViewInFragment() }
}
private fun transitionWithTextViewInFragment(){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
private fun transitionWithTextViewInRecyclerViewItem(view: View){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout 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:orientation="vertical">
<TextView
android:id="#+id/transition_start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/open_transition_end_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/transition_start_text"
android:text="open transition end fragment" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/test_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/open_transition_end_fragment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
adapter for recyclerView
class TestAdapter(
private val items: List<String>,
private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHodler, position: Int) {
val item = items[position]
holder.transitionText.text = item
holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }
}
class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
}
}
in onItemClick I pass the textView form item in recyclerView for transition
transition end
class TransitionEndFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setUpTransition()
return inflater.inflate(R.layout.fragment_transition_end, container, false)
}
private fun setUpTransition(){
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_end_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fun transitionWithTextViewInFragment() - has transition.
fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.
To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
Here is my example with RecyclerView that have fragment shared transition.
In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.txtView.text=item
ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
holder.setClickListener(object : ViewHolder.ClickListener {
override fun onClick(v: View, position: Int) {
when (v.id) {
R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
}
}
})
}
And when clicking on item, my interface that implemented in source fragment:
override fun onClick(text: String, img: ImageView, position: Int) {
val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(img, ViewCompat.getTransitionName(img)!!)
.build()
NavHostFragment.findNavController(this#MainFragment).navigate(action, extras)
}
And in my destination fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
info("onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
info("onCreateView")
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
info("onViewCreated")
val name=SecondFragmentArgs.fromBundle(arguments).name
val position=SecondFragmentArgs.fromBundle(arguments).position
txtViewName.text=name
ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
}
Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.
So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :
just adding a simple check in onCreateView as so
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//doing some stuff here
root
}
}
So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.
I have managed return transitions to work.
Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.
To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().
For example:
Original fragment:
class FragmentOne : Fragment(R.layout.f1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
val items = listOf("one", "two", "three", "four", "five")
.zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
.map { Item(it.first, it.second) }
val rv = view.findViewById<RecyclerView>(R.id.rvItems)
rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
view.doOnPreDraw { startPostponedEnterTransition() }
}
private fun navigateOn(item: Item, view: View) {
val extras = FragmentNavigatorExtras(view to "yura")
findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
}
}
Next fragment:
class FragmentTwo : Fragment(R.layout.f2) {
val item: Item by lazy { arguments?.getSerializable("item") as Item }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
val tv = view.findViewById<TextView>(R.id.tvItemId)
with(tv) {
text = item.id
transitionName = "yura"
setBackgroundColor(item.color)
}
}
}
For more details and deeper explanation see:
https://issuetracker.google.com/issues/118475573
and
https://chris.banes.dev/2018/02/18/fragmented-transitions/
Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.
Here's an example of such a transition:
// FooListFragment.kt
class FooListFragment : Fragment() {
...
private val itemListener = object : FooListener {
override fun onClick(item: Foo, itemView: View) {
...
val transitionName = getString(R.string.foo_details_transition_name)
val extras = FragmentNavigatorExtras(itemView to transitionName)
navController.navigate(directions, extras)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Postpone enter transitions to allow shared element transitions to run.
// https://github.com/googlesamples/android-architecture-components/issues/495
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
...
}
// FooDetailsFragment.kt
class FooDetailsFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = 1000
}
}
}
And don't forget to add unique transition names to the views:
<!-- foo_list_item.xml -->
<LinearLayout ...
android:transitionName="#{#string/foo_item_transition_name(foo.id)}">...</LinearLayout>
<!-- fragment_foo_details.xml -->
<LinearLayout ...
android:transitionName="#string/foo_details_transition_name">...</LinearLayout>
<!-- strings.xml -->
<resources>
...
<string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
<string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
</resources>
The full sample is available on GitHub.
You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.

Categories

Resources