Retain focus on previous fragment's view - android

Suppose I have 5 views (1,2,3,4,5) in a fragment A and each view is focusable.
Then, if the current focus in fragment A is at view 3 and clicking that view loads a new fragment, let's say fragment B.
Now, is there any way that when I press back button, the focus is retained on view 3 of fragment A automatically?
Note: I have added those fragments in the backstack. But my fragment contains recycler views. Clicking on an item of recycler view will load another fragment and on back press the Main Activity gets the default focus.
I can implement focusing on recycler view's first item by implementing fragment's onBackPressListener() method. But I have no idea on how to retain focus on a fragment as it was before.

You can always add these fragment in backstack of Fragment Manager.

If you are adding fragment on top of fragment A with add to backstack, it will resume in last focused item when you press back button

for those who still searching for the solution, I tested many ways but this one was the best for me (Kotlin):
Image
Fragment A
1- block fragment a descendant focusability in onStop() in FRAGMENT A to avoid giving focus to a random button by the activity on BACK
override fun onStop() {
println("**onStop")
(view as ViewGroup?)?.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
super.onStop()
}
2- make a lambda to do some things on a resume
private var doOnResume: (() -> Unit?)? = null
3- setup the fragment transition
// add a lambda here too
private fun gotoFragment(onResume: () -> Unit) {
//initialize doOnResume here
doOnResume = onResume
(requireActivity() as MainActivity).apply {
supportFragmentManager.commit {
replace(R.id.app_container, DetailsFragment())
addToBackStack(null)
}
}
}
4- I am using leanback RowSupportFragment it automatically set focus t the last item when you request focus to its verticalGridView
listFragment.setOnContentClickedListener {
gotoFragment {
listFragment.verticalGridView.requestFocus()
}
}
note you are able to give focus to any view you want
private fun onPlayButtonClicked() {
binding.playBTN.setOnClickListener {button->
gotoFragment {
button.requestFocus()
}
}
}
5- call your lambda in the onResume
override fun onResume() {
//unblock descendantFocusability again
(view as ViewGroup?)?.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
//avoid crashing app when gridview is not initialized yet or any other reason
try {
doOnResume?.invoke()
} catch (_:Exception) {
}
super.onResume()
}

Related

How to use main actvitiy floating action button from fragments in single activity?

I'm using BottomAppbar with floating action button in single activity.The Fab has different responsibility in different fragment.
HomeFragment -> adds new item
AddProblemFragment ->saves item(also icon changes)
DetailsFragment -> adds items to selected collections(also icon changes)
I have tried to use listener between activity and fragments. Icon and position changes perfectly but I couldn't handle calling methods that in fragments when clicked.
To solve clicking problem I'm getting fab reference from activity with
fab = requireActivity().findViewById(R.id.fab) in each fragment (in onViewCreated).
So, problem is after orientation changes gives
java.lang.NullPointerException: requireActivity().findViewById(R.id.fab) must not be null
Is there way to avoid getting this error after orientation changes? or Which lifecycle should choose to initialize fab?
Is it possible to initialize fab in BaseFragment and use it because almost each fragment have fab?
The Code Snippets:
//Each fragment same approach
private lateinit var fab:FloatingActionButton
override fun onViewCreated(view:View,savedInstanceState:Bundle?){
fab = requireActivity().findViewById(R.id.fab_btn_add)
fab.setImageResource(R.drawable.ic_related_icon)
fab.setOnClickListener {
//calls method
}
First you need to create an interface for you fab click listener for Example:
interface FabClickCallback {
void onFabClick()
}
and on your activity,
You Need create an instance from this Callback
private FabClickCallback fabCallback;
and you want create new method inside your Activity called for Example:
public void handleFabClickListener(FabClickCallback callback){
// TODO Here put callback
this.fabCallback = callback;
}
and you want trigger this callback when user click on Fab Button like This:
fab.setOnClickListener ( view -> {
if (this.fabCallback != null ) {
this.fabCallback.onFabClick();
}
});
finally on Each Fragment you want handle Fab Click you want call this function and pass your actual fab click code :
((YourActivity) requireActivity()).handleFabClickListener(this);
and your fragment must implements FabClickCallback interface

Listen when a shared element is back on original Fragment

Hello Android community,
I have a very simple app, composed of two fragments A & B and there is a shared element between those two fragments.
When I open Fragment B, the transition is working well, and I'm able to listen when the element has finished the transition. However, when I pop FragmentB, I'd like FragmentA to be informed that the return transition is over.
I made a project example here: https://github.com/JulienDev/shared_element_listener
As you can see, I've tried many different things without success.
Do you know if that's something possible?
Thank you so much :)
I think issue has been solved. First change goToB() method as below :
fun goToB(view: View) {
supportFragmentManager.beginTransaction()
.addSharedElement(view, "view_transition")
.replace(findViewById<ViewGroup>(R.id.preorder_content).id, FragmentB())
//.addToBackStack("")
.disallowAddToBackStack() // Prevent from adding this fragment to Back stack
.commit()
}
Right now, we are inside Fragment B. If back button is clicked now, then I have used call back to handle it programmatically by adding callback in Fragment B. Like :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val transitionView = requireView().findViewById<View>(R.id.view_transition)
transitionView.setOnClickListener {
(activity as MainActivity).goToC(transitionView)
}
// This callback will only be called when Fragment B is at least Started.
requireActivity().onBackPressedDispatcher.addCallback(this) {
(activity as MainActivity).goFromBToA(transitionView)
}
}
I have put another method in Activity.
fun goFromBToA(view: View){
supportFragmentManager.beginTransaction()
.addSharedElement(view, "view_transition")
.replace(findViewById<ViewGroup>(R.id.preorder_content).id, FragmentA())
.disallowAddToBackStack()
.commit()
}
Now I can see transition end log when pop up to Fragment A from B.

How to implement specific back button functionallity in fragment when condition is made and use default back button functionallity otherwise?

I want to clear selection on back button pressed on selection tracker when there are selected items and use default back button functionallity if there are no selected items.
I use Navigation Components for working with fragments.
Code in my fragment which implements OnBackPressedListener interface:
override fun onBackPressed() {
if (tracker?.hasSelection() == true) tracker?.clearSelection()
else findNavController().popBackStack()
}
Clear selection condition is working, but else block does not.
Code from my BaseActivity which determines how to use back button.
override fun onBackPressed() {
val currentFragment = navHost.childFragmentManager.fragments[0]
if (currentFragment is OnBackPressedListener) {
currentFragment.onBackPressed()
} else {
super.onBackPressed()
}
}
So, question is how to exit fragment (or pop backstack) when there are no selected items in selection tracker? It is working when I call
requireActivity().finish()
, but I do not know if this is correct solution.
For this case, you could try registering OnBackPressedCallback on your Fragments via addOnBackPressedCallback. For more detail follow the below-given link:
https://developer.android.com/guide/navigation/navigation-custom-back

Keeping states of recyclerview in fragment with paging library and navigation architecture component

I'm using 2 components of the jetpack: Paging library and Navigation.
In my case, I have 2 fragment: ListMoviesFragment & MovieDetailFragment
when I scroll a certain distance and click a movie item of the recyclerview, MovieDetailFragment is attached and the ListMoviesFragment is in the backstack. Then I press back button, the ListMoviesFragment is bring back from the backstack.
The point is scrolled position and items of the ListMoviesFrament are reset exactly like first time attach to its activity. so, how to keep states of recyclerview to prevent that?
In another way, how to keep states of whole fragment like hide/show a fragment with FragmentTransaction in traditional way but for modern way(navigation)
My sample codes:
fragment layout:
<FrameLayout 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"
tools:context="net.karaokestar.app.SplashFragment">
<TextView
android:id="#+id/singer_label"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Ca sĩ"
android:textColor="#android:color/white"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="#+id/btn_game_more"
android:layout_centerVertical="true"
android:background="#drawable/shape_label"
android:layout_marginTop="10dp"
android:layout_marginBottom="#dimen/header_margin_bottom_list"
android:textStyle="bold"
android:padding="#dimen/header_padding_size"
android:textAllCaps="true"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list_singers"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Fragment kotlin code:
package net.karaokestar.app
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_splash.*
import net.karaokestar.app.home.HomeSingersAdapter
import net.karaokestar.app.home.HomeSingersRepository
class SplashFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_splash, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val singersAdapter = HomeSingersAdapter()
singersAdapter.setOnItemClickListener{
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
}
list_singers.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
list_singers.setHasFixedSize(true)
list_singers.adapter = singersAdapter
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
}
fun getSingersPagination() : LiveData<PagedList<Singer>> {
val repository = HomeSingersRepository()
val pagedListConfig = PagedList.Config.Builder().setEnablePlaceholders(true)
.setPageSize(Configurations.SINGERS_PAGE_SIZE).setPrefetchDistance(Configurations.SINGERS_PAGE_SIZE).build()
return LivePagedListBuilder(repository, pagedListConfig).build()
}
}
Since you use NavController, you cannot keep the view of the list fragment when navigating.
What you could do instead is to keep the data of the RecyclerView, and use that data when the view is recreated after back navigation.
The problem is that your adapter and the singersPagination is created anew every time the view of the fragment is created. Instead,
Move singersAdapter to a field:
private val singersAdapter = HomeSingersAdapter()
Move this part to onAttach
getSingersPagination().observe(viewLifecycleOwner, Observer {
singersAdapter.submitList(it)
})
Call retainInstance(true) in onAttach. This way even configuration changes won't reset the state.
On fragment's onSaveinstanceState save the layout info of the recyclerview:
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_LAYOUT, myRecyclerView.getLayoutManager().onSaveInstanceState());
}
and on onActivityCreated, restore the scroll position:
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
myRecyclerView.getLayoutManager().onRestoreInstanceState(
savedInstanceState.getParcelable(KEY_LAYOUT));
}
}
Try the following steps:
Initialize your adapter in onCreate instead of onCreateView. Keep the initialization one time, and attach it in onCreateView or onViewCreated.
Don't return a new instance of your pagedList from getSingersPagination() method everytime, instead store it in a companion object or ViewModel (preferred) and reuse it.
Check the following code to get a rough idea of what to do:
class SingersViewModel: ViewModel() {
private var paginatedLiveData: MutableLiveData<YourType>? = null;
fun getSingersPagination(): LiveData<YourType> {
if(paginatedLiveData != null)
return paginatedLiveData;
else {
//create a new instance, store it in paginatedLiveData, then return
}
}
}
The causes of your problem are:
You are attaching a new adapter each time, so it jumps to top.
You are creating new paged list each time, so it jumps to the top, thinking the data is all new.
The sample fragment code you posted does not correspond to the problem description, I guess it's just an illustration of what you do in your app.
In the sample code, the actual navigation (the fragment transaction) is hidden behind this line:
findNavController().navigate(SplashFragmentDirections.actionSplashFragmentToSingerFragment2())
The key is how the details fragment is attached.
Based on your description, your details fragment is probably attached with FragmentTransaction.replace(int containerViewId, Fragment fragment). What this actually does is first remove the current list fragment and then add the detail fragment to the container. In this case, the state of the list fragment is not kept. When you press the back button, the onViewCreated of the list fragment will run again.
To keep the state of your list fragment, you should use FragmentTransaction.add(int containerViewId, Fragment fragment) instead of replace. This way, the list fragment remains where it is and it gets "covered" by the detail fragment. When you press the back button, the onViewCreated will not be called, since the view of the fragment did not get destroyed.
Whenever the fragment back again, it get the new PagedList, this cause the adapter present new data thus you will see the recycler view move to top.
By moving the creation of PagedList to viewModel and check to return the exist PagedList instead of create new one will solve the problem. Of course, depends on your app business create new PagedList might require but you can control it completely. (ex: when pull to refresh, when user input data to search...)
Things to keep in mind when dealing with these issues:
In order to let OS handle the state restoration of a view automatically, you must provide an id. I believe that this is not the case (because you must identify the RecyclerView in order to bind data).
You must use the correct FragmentManager. I had the same issue with a nested fragment and all that i had to do was to use ChildFragmentManager.
The SaveInstanceState() is triggered by the activity. As long as activity is alive, it won't be called.
You can use ViewModels to keep state and restore it in onViewCreated()
Lastly, navigation controller creates a new instance of a fragment every time we navigate to a destination. Obviously, this does not work well with keeping persistent state. Until there is an official fix you can check the following workaround which supports attaching/detaching as well as different backstacks per tab. Even if you do not use BottomNavigationView, you can use it to implement an attaching/detaching mechanism.
https://github.com/googlesamples/android-architecture-components/blob/27c4045aa0e40d402bbbde16d7ae0c9822a34447/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt
I had the same problem with my recycle view using the paging library. My list list would always reload and scroll to the top when up navigation button is clicked from my details fragment. Picking up from #Janos Breuer's point I moved the initialisation of my view model and initial list call(repository) to the fragment onCreate() method which is called only once in the fragment lifecycle.
onCreate() The system calls this method when creating the fragment. You should initialize essential components of the fragment that you want to retain when the fragment is paused or stopped, then resumed.
Sorry to be late. I had a similar problem and I figured out a solution (maybe not the best). In my case I adapted the orientation changes.
What you need is to save and retrieve is the LayoutManager state AND the last key of the adapter.
In the activity / fragment that is showing your RecyclerView declare 2 variables:
private Parcelable layoutState;
private int lastKey;
In onSaveInstanceState:
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if(adapter != null) {
lastKey = (Integer)adapter.getCurrentList().getLastKey();
outState.putInt("lastKey",lastKey);
outState.putParcelable("layoutState",dataBinding.customRecyclerView
.getLayoutManager().onSaveInstanceState());
}
}
In onCreate:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//.... setContentView etc...
if(savedInstanceState != null) {
layoutState = savedInstanceState.getParcelable("layoutState");
lastKey = savedInstanceState.getInt("lastKey",0);
}
dataBinding.customRecyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(lastKey != 0) {
dataBinding.customRecyclerView.scrollToPosition(lastKey);
Log.d(LOG_TAG, "list restored; last list position is: " +
((Integer)adapter.getCurrentList().getLastKey()));
lastKey = 0;
if(layoutState != null) {
dataBinding.customRecyclerView.getLayoutManager()
.onRestoreInstanceState(layoutState);
layoutState = null;
}
}
}
});
}
And that's it. Now your RecyclerView should restore properly.
Well... You can check if the view is initialized or not (for kotlin)
Initialize view variable
private lateinit var _view: View
In onCreateView check if view is initialized or not
if (!this::_view.isInitialized) {
return inflater.inflate(R.layout.xyz, container, false)
}
return _view
And in onViewCreated just check for it
if (!this::_view.isInitialized) {
_view = view
// ui related methods
}
This solution is for onreplace()....but if data is not changing on back press you may use add() method of fragment.
Here oncreateview will be called but your data won't be reloaded.
Simple steps...
Give id to all the views which you need to maintain state in fragment back stack. Like Scrollview, root layout, Recyclerview...
The properties which should hold values, initialize in onCreate() of the fragment. Like adapter, count...except view references.
If you are setting observers use lifecycleowner as this#yourFragment instead of viewLifecycleOwner.
That's it. Fragment onCreate() is called only once so setting properties on onCreate() will be there in memory till the fragment onDestroy() is called(not onDestroyView).
All the view referencing code should be there after onCreateView() and before onDestroyView().
BTW If possible you can save the properties in ViewModel which will be there even when fragment configuration changes where all the instance variables will be destroyed.
hope this link will help:
PagingDataAdapter.refresh() not working after fragment navigation
This so simple, only use this method cachedIn()
fun getAllData() {
viewModelScope.launch {
_response.value = repository.getPagingData().cachedIn(viewModelScope)
}
}

How can I save fragment state when the user navigates between fragments in a viewpager?

I'm making an Android App, that uses BottomNavigationViewEx to have a Bottom Navigation widget with 5 sections/fragments, I manage them using a viewpager, but one of this fragment (fragment #3) also uses a tab layout to nest another 2 fragments and I need to keep the selected tab when the user navigates to other fragment using the BottomNavigation icons.
The problem is that I need save the state of the fragment #3 (juts to keep it simple, I call them in this post fragment #), that is the fragment that holds the tablayout.
Inside fragment #3 I'm calling:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("currentDirectoryFragmentId",tabLayout!!.selectedTabPosition)
}
but the method is never being called, and makes sense, because I really never destroy the parent activity, but onDestroy() is being called inside each fragment correctly.
So, How can I save the state of a fragment when the user navigates between fragments that are children of a same activity?
As stated in the comments. You can accomplish this by using a variable inside the parent activity and referring to and setting this variable inside the fragments' onPause() & onResume() methods.
Inside Parent Activity
public static int position = -1;
Inside Child Fragment
#Override
public void onPause() {
super.onPause();
MainActivity.position = viewPager.getCurrentItem();
}
#Override
public void onResume() {
viewPager.setCurrentItem(MainActivity.position);
super.onResume();
}

Categories

Resources