Leanback : How do I load fragment only when header item clicked? - android

Problem
I've been trying out Leanback's Page/Row fragment Example. As per the sample, we can use BrowseSupportFragment.FragmentFactory to show fragments when a header item gets focused. It works well, but I need to do some customization.
Question
How do I load the fragment only when the header is clicked, but not when focused?
How can I insert a custom header item without a row/page, so that I can launch a new activity when it is clicked?

My proposition of solving this problem:
TLDR; It is possible but, with some fragment nesting.
You just need to add middle layer for child fragment and open it when middle layer fragment is focused.
Fragment factory -> proxyFocusableFragment -> targetFragment
Create layout in fragment_focusable_proxy.xml with fragment container for target fragment:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/proxy_root"
android:focusable="true"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/proxy_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
It is very important to add flag android:focusable="true" to root view tag, otherwise you will not be able to catch focus in this fragment and side menu would "bounce back".
Create proxy fragment class with focus changer listener:
class FocusableProxyFragment : Fragment(), BrowseSupportFragment.MainFragmentAdapterProvider {
private val adapter by lazy { BrowseSupportFragment.MainFragmentAdapter(this) }
override fun getMainFragmentAdapter(): BrowseSupportFragment.MainFragmentAdapter<*> {
return adapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_proxy_focusable, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
//todo: here add target fragment and remove focus on middle layer
}
}
}
Then, inside focus listener implementation you need to remove this listener and set proxy layer not focusable, otherwise your target fragment will not work properly with leanback:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
//todo: add fragment to container
}
}
}
private fun removeProxyFocus(view: View) {
val rootView = view.findViewById<ViewGroup>(R.id.proxy_root)
rootView.focusable = View.NOT_FOCUSABLE
view.onFocusChangeListener = null
}
Add target fragment to proxy container using childFragmentManager:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
instantiateTargetFragment()
}
}
}
private fun instantiateTargetFragment() {
childFragmentManager
.beginTransaction()
.add(R.id.proxy_fragment_container, TargetFragment())
.commit()
}
Whole fragment class:
class FocusableProxyFragment : Fragment(), BrowseSupportFragment.MainFragmentAdapterProvider {
private val adapter by lazy { BrowseSupportFragment.MainFragmentAdapter(this) }
override fun getMainFragmentAdapter(): BrowseSupportFragment.MainFragmentAdapter<*> {
return adapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_proxy_focusable, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
instantiateTargetFragment()
}
}
}
private fun instantiateTargetFragment() {
childFragmentManager
.beginTransaction()
.add(R.id.proxy_fragment_container, TargetFragment())
.addToBackStack("cutv")
.commit()
}
private fun removeProxyFocus(view: View) {
val rootView = view.findViewById<ViewGroup>(R.id.proxy_root)
rootView.focusable = View.NOT_FOCUSABLE
view.onFocusChangeListener = null
}
}
And at the end in your fragment factory replace original target fragment with proxy.

Related

call a fragment from another class\function kotlin

GameActivity
____FragmentQuest
____FragmentFight
class MapLvl.kt
this is a text RPG Fragment Quest displays a journey through different maps (changing the content in one fragment according to a template) text, pictures, navigation buttons. if there is a mob on the map, then the "Fight" button appears and a fragment of the turn-based fight FightFragment opens (hit the head \ legs\ body protect the head \ legs\ body). after the battle, return to QuestFragment
class MapLvl fills with the content of FightFragment
I need to change QuestFragment from classLvl to FightFragment. how to do it?
it doesn't work:
class MapLvl.kt:
class MapLevels(){
fun changeLvl (bind: FragmentQuestBinding,hero: Hero, activity: GameActivity,db: Maindb) {
when (hero.mapLvl) {
1 -> MapLevels().mapLevel1(bind, activity, hero, db)
2 -> MapLevels().mapLevel2(bind, activity, hero,db)
else -> {}
}
}
fun mapLevel2 (bind: FragmentQuestBinding,activity: GameActivity,hero:Hero,db: Maindb) {
bind.btnAtack.visibility= View.VISIBLE
//the problem is here:
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
}
}
error: FragmentManager has not been attached to a host
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
if you make a call directly from a Fragment, then it works: (but it is necessary not from the fragment but from the class)
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
it Work:
class MapLevels(val bind: FragmentQuestBinding,
val hero: Hero,
val db: Maindb,
val activity: FragmentActivity ){
fun changeLvl() {
when (hero.mapLvl) {
1 -> MapLevels(bind,hero,db,activity).mapLevel1()
2 -> MapLevels(bind,hero,db,activity).mapLevel2()
else -> {}
}
}
fun mapLevel2() {
bind.imgLocation.setImageResource(R.drawable.map_loc02)
bind.txtLocationDiscription.text ="text"
bind.btnAtack.visibility= View.VISIBLE
bind.btnAtack.setOnClickListener {
(activity as FragmentActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
MapLevels().changeLvl(bind,hero,GameActivity(),db)
It looks like you are creating a new GameActivity instance to pass to your changeLvl() method. YOU SHOULD NEVER DO THIS. The new activity is NOT the same one that currently is displayed on the screen. Instead, you should use the requireAcitivty() to get the fragment's parent activity:
MapLevels().changeLvl(bind,hero,requireActivity(),db)
I can't be sure this will fix your current problem because I'm not even sure what the problem is exactly. But it is one issue that you need to change.

Android Navigation Component saveState and restoreState

Guys I need your help.
I use android navigation component and want to save backstack after user press button and restore it after. I found 2 methods
navController.saveState(): Bundle and navController.restoreState(bundle: Bundle).
But i have problem in use it. Seems like saveState work greate (i see bundle, and backstack inside), but i dont understand how to use restoreState, because the documentation says:
Restores all navigation controller state from a bundle. This should be called before any call to setGraph.
https://developer.android.com/reference/kotlin/androidx/navigation/NavController#restorestate
Okay, i did it, seems like backstack restored, but on screen i see first fragment (instead of the one I had when I saved it). What i do wrong?
Code:
FirstFragment
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_forward.setOnClickListener { findNavController().navigate(R.id.action_firstFragment_to_secondFragment) }
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
SecondFragment
class SecondFragment : Fragment() {
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_forward.setOnClickListener { findNavController().navigate(R.id.action_secondFragment_to_thirdFragment) }
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
ThirdFragment
class ThirdFragment : Fragment() {
private val TAG = this::class.java.name
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_third, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
btn_finish.setOnClickListener {
(requireActivity() as MainActivity).saveState() //here save bundle
requireActivity().finishAfterTransition()
}
btn_back.setOnClickListener { requireActivity().onBackPressed() }
}
}
MainActivity
class MainActivity : AppCompatActivity() {
private val TAG = "MySuperActivity"
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "onCreate($savedInstanceState) called")
initNavController()
}
private fun initNavController() {
val navHostFragment = nav_host_fragment as NavHostFragment
val graphInflater = navHostFragment.navController.navInflater
val graph = graphInflater.inflate(R.navigation.main_graph)
navController = navHostFragment.navController
navHostFragment.childFragmentManager
if (App.instance.savedBundle != null) {
Log.d(TAG, "bundle: ${App.instance.savedBundle}")
navController.restoreState(App.instance.savedBundle)
graph.startDestination = R.id.thirdFragment
}
navController.graph = graph
Log.d(TAG, "navController.currentDestination: ${navController.currentDestination}")
Log.d(TAG, "navController.graph.startDestination: ${navController.graph.startDestination}")
}
fun saveState(){
App.instance.savedBundle = navController.saveState()
Log.d(TAG, "saveState() : ${App.instance.savedBundle}")
}
}
here some logs: logs
full code:github
I am not sure if my answer helps you, but I had many issues trying to save the navigation state from handling rotations. The issue that I had comes from an old version of the navigation component, I update to the most recent, and it fixes the issue:
def android_navigation = '2.3.4'
implementation "android.arch.navigation:navigation-fragment-ktx:$android_navigation"
implementation "android.arch.navigation:navigation-ui-ktx:$android_navigation"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$android_navigation"

How to speedup when change fragment in Android

WHY NOT ANY PEOPLE HELP TO ME???
In my application I used BottomNavBar and NavigationGraph for show some fragments!
In one of my fragments I have many views (fragment layout has 1069 lines xml codes) and when select this fragment from BottomNavBar, after some second show me this fragment.
In the other words show me this fragment with delay!
Fragment codes:
class HomeDashboardFragment : Fragment(), HomeDashboardContracts.View {
#NonNull
private var pageTitle: TextView? = null
#NonNull
private var menuIcon: TextView? = null
private lateinit var token: String
private lateinit var presenter: HomeDashboardPresenterImpl
private var giftExpireSplit: List<String> = emptyList()
private var giftExpireDate: List<String> = emptyList()
private var timeUtils: TimeUtils? = null
#NonNull
private var disposable: Disposable? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home_dashboard, container, false)
}
#SuppressLint("WrongConstant")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
presenter = HomeDashboardPresenterImpl(requireContext(), this)
//Initialize views from activity
activity?.let { itActivity ->
pageTitle = itActivity.findViewById(R.id.toolbarMain_title)
menuIcon = itActivity.findViewById(R.id.toolbarMain_menuIcon)
//Set title
pageTitle?.let { itTitle ->
itTitle.text = getString(R.string.menuHomeDashboard)
}
//Open menu
menuIcon?.let { itMenu ->
itMenu.setOnClickListener {
itActivity.findViewById<AwesomeDrawerLayout>(R.id.homePage_drawerLayout).openDrawer(Gravity.END)
}
}
//Get token
token = GoodPrefs.getInstance().getString(PrefsKey.USER_JWT_TOKEN.name, "")
//User registered
if (GoodPrefs.getInstance().isKeyExists(PrefsKey.USER_JWT_TOKEN.name)) {
menuIcon?.visibility = View.VISIBLE
}
//Set layout
presenter.checkRegisterUser(token)
//Load profile data
if (!isEmptyString(token)) {
presenter.getProfile(token, USER_NOTIF_ID)
}
}
}
MainActivity codes for set fragments into BottomNavBar with NavigationGraph :
private fun setupNavigation() {
val navController = Navigation.findNavController(this, R.id.homePage_fragmentNavHost)
NavigationUI.setupWithNavController(homePage_bottomNavBar, navController)
}
override fun onSupportNavigateUp() = Navigation.findNavController(this, R.id.homePage_fragmentNavHost).navigateUp()
How can i fix this issue?
You can add a layout dummy consisting of a single viewGroup. Then in the onViewCreated method inflate the real layout. For example:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.layout_dummy, container, false);
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Handler().post {
val layout = flContainer
val child = layoutInflater.inflate(R.layout.fragment_real, null)
layout.removeAllViews()
layout.addView(child)
setupToolbar()
setupWebView()
}
}
layout_dummy.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="#+id/flContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
You issue is almost assuredly caused by your massive XML file. Loading all of those views takes time. You have A LOT of nesting which slows things down.
You have to optimize your layout. Remove things you don't need. Replace as many nested LinearLayouts as you can with ConstraintLayout. Maybe use a RecyclerView if this is intended to be a long, scrolling view.
Hope that helps.

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.

Fragment View is null when try to access from Activity

I have created a Fragment which is in ViewPager. Fragment is loading fine.
I am using a button to swipe fragments in viewpager and before swiping, I want to check validations. So I did this in Activity :
val pos: Int = viewPager.currentItem
val activeFragment: Fragment? = mAdapter.getItem(pos)
if (viewPager.currentItem == 0) {
if (pos == 0) (activeFragment as FragmentOne).checkValidation()
Below is my Fragment code.
class FragmentOne: BaseFragment() {
fun newInstance(): FragmentOne{
val args = Bundle()
val fragment = FragmentOne()
fragment.setArguments(args)
return fragment
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view : View = inflater.inflate(R.layout.fragment_one, container, false);
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
initiateUI()
}
fun checkValidation(): Boolean {
if (TextUtils.isEmpty(etDateOfBirth.text.toString())) {
showMessage(etDateOfBirth, getString(R.string.str_empty_dob))
return false
}
return true
}
}
When I press next I get error:
java.lang.IllegalStateException: etDateOfBirth must not be null
Can anyone help out?
Didn't you define etDateOfBirth? Define edit text on onViewCreated()
EditText etDateOfBirth = view.findViewById(R.id.sample);

Categories

Resources