从后面的堆栈恢复片段

我正在使用兼容性包来使用 Android 2.2中的片段。 当使用片段并将它们之间的转换添加到回堆栈时,我希望实现活动的 onResume 的相同行为,即,当一个片段弹出回堆栈后被带到“前台”(用户可见)时,我希望在片段内激活某种回调(例如,在共享 UI 资源上执行某些更改)。

我看到在片段框架中没有内置的回调。是否有一个良好的做法来实现这一点?

113144 次浏览

The following section at Android Developers describes a communication mechanism Creating event callbacks to the activity. To quote a line from it:

A good way to do that is to define a callback interface inside the fragment and require that the host activity implement it. When the activity receives a callback through the interface, it can share the information with other fragments in the layout as necessary.

Edit: The fragment has an onStart(...) which is invoked when the fragment is visible to the user. Similarly an onResume(...) when visible and actively running. These are tied to their activity counterparts. In short: use onResume()

For a lack of a better solution, I got this working for me: Assume I have 1 activity (MyActivity) and few fragments that replaces each other (only one is visible at a time).

In MyActivity, add this listener:

getSupportFragmentManager().addOnBackStackChangedListener(getListener());

(As you can see I'm using the compatibility package).

getListener implementation:

private OnBackStackChangedListener getListener()
{
OnBackStackChangedListener result = new OnBackStackChangedListener()
{
public void onBackStackChanged()
{
FragmentManager manager = getSupportFragmentManager();


if (manager != null)
{
MyFragment currFrag = (MyFragment) manager.findFragmentById(R.id.fragmentItem);


currFrag.onFragmentResume();
}
}
};


return result;
}

MyFragment.onFragmentResume() will be called after a "Back" is pressed. few caveats though:

  1. It assumes you added all transactions to the backstack (using FragmentTransaction.addToBackStack())
  2. It will be activated upon each stack change (you can store other stuff in the back stack such as animation) so you might get multiple calls for the same instance of fragment.

I've changed the suggested solution a little bit. Works better for me like that:

private OnBackStackChangedListener getListener() {
OnBackStackChangedListener result = new OnBackStackChangedListener() {
public void onBackStackChanged() {
FragmentManager manager = getSupportFragmentManager();
if (manager != null) {
int backStackEntryCount = manager.getBackStackEntryCount();
if (backStackEntryCount == 0) {
finish();
}
Fragment fragment = manager.getFragments()
.get(backStackEntryCount - 1);
fragment.onResume();
}
}
};
return result;
}

This is the correct answer you can call onResume() providing the fragment is attached to the activity. Alternatively you can use onAttach and onDetach

onResume() for the fragment works fine...

public class listBook extends Fragment {


private String listbook_last_subtitle;
...


@Override
public void onCreate(Bundle savedInstanceState) {


String thisFragSubtitle = (String) getActivity().getActionBar().getSubtitle();
listbook_last_subtitle = thisFragSubtitle;
}
...


@Override
public void onResume(){
super.onResume();
getActivity().getActionBar().setSubtitle(listbook_last_subtitle);
}
...

If a fragment is put on backstack, Android simply destroys its view. The fragment instance itself is not killed. A simple way to start should to to listen to the onViewCreated event, an put you "onResume()" logic there.

boolean fragmentAlreadyLoaded = false;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onViewCreated(view, savedInstanceState);


if (savedInstanceState == null && !fragmentAlreadyLoaded) {
fragmentAlreadyLoaded = true;


// Code placed here will be executed once
}


//Code placed here will be executed even when the fragment comes from backstack
}

After a popStackBack() you can use the following callback : onHiddenChanged(boolean hidden) within your fragment

A little improved and wrapped into a manager solution.

Things to keep in mind. FragmentManager is not a singleton, it manages only Fragments within Activity, so in every activity it will be new. Also, this solution so far doesn't take ViewPager into account that calls setUserVisibleHint() method helping to control visiblity of Fragments.

Feel free to use following classes when dealing with this issue (uses Dagger2 injection). Call in Activity:

//inject FragmentBackstackStateManager instance to myFragmentBackstackStateManager
FragmentManager fragmentManager = getSupportFragmentManager();
myFragmentBackstackStateManager.apply(fragmentManager);

FragmentBackstackStateManager.java:

@Singleton
public class FragmentBackstackStateManager {


private FragmentManager fragmentManager;


@Inject
public FragmentBackstackStateManager() {
}


private BackstackCallback backstackCallbackImpl = new BackstackCallback() {
@Override
public void onFragmentPushed(Fragment parentFragment) {
parentFragment.onPause();
}


@Override
public void onFragmentPopped(Fragment parentFragment) {
parentFragment.onResume();
}
};


public FragmentBackstackChangeListenerImpl getListener() {
return new FragmentBackstackChangeListenerImpl(fragmentManager, backstackCallbackImpl);
}


public void apply(FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
fragmentManager.addOnBackStackChangedListener(getListener());
}
}

FragmentBackstackChangeListenerImpl.java:

public class FragmentBackstackChangeListenerImpl implements FragmentManager.OnBackStackChangedListener {


private int lastBackStackEntryCount = 0;
private final FragmentManager fragmentManager;
private final BackstackCallback backstackChangeListener;


public FragmentBackstackChangeListenerImpl(FragmentManager fragmentManager, BackstackCallback backstackChangeListener) {
this.fragmentManager = fragmentManager;
this.backstackChangeListener = backstackChangeListener;
lastBackStackEntryCount = fragmentManager.getBackStackEntryCount();
}


private boolean wasPushed(int backStackEntryCount) {
return lastBackStackEntryCount < backStackEntryCount;
}


private boolean wasPopped(int backStackEntryCount) {
return lastBackStackEntryCount > backStackEntryCount;
}


private boolean haveFragments() {
List<Fragment> fragmentList = fragmentManager.getFragments();
return fragmentList != null && !fragmentList.isEmpty();
}




/**
* If we push a fragment to backstack then parent would be the one before => size - 2
* If we pop a fragment from backstack logically it should be the last fragment in the list, but in Android popping a fragment just makes list entry null keeping list size intact, thus it's also size - 2
*
* @return fragment that is parent to the one that is pushed to or popped from back stack
*/
private Fragment getParentFragment() {
List<Fragment> fragmentList = fragmentManager.getFragments();
return fragmentList.get(Math.max(0, fragmentList.size() - 2));
}


@Override
public void onBackStackChanged() {
int currentBackStackEntryCount = fragmentManager.getBackStackEntryCount();
if (haveFragments()) {
Fragment parentFragment = getParentFragment();


//will be null if was just popped and was last in the stack
if (parentFragment != null) {
if (wasPushed(currentBackStackEntryCount)) {
backstackChangeListener.onFragmentPushed(parentFragment);
} else if (wasPopped(currentBackStackEntryCount)) {
backstackChangeListener.onFragmentPopped(parentFragment);
}
}
}


lastBackStackEntryCount = currentBackStackEntryCount;
}
}

BackstackCallback.java:

public interface BackstackCallback {
void onFragmentPushed(Fragment parentFragment);


void onFragmentPopped(Fragment parentFragment);
}

In my activity onCreate()

getSupportFragmentManager().addOnBackStackChangedListener(getListener());

Use this method to catch specific Fragment and call onResume()

private FragmentManager.OnBackStackChangedListener getListener()
{
FragmentManager.OnBackStackChangedListener result = new FragmentManager.OnBackStackChangedListener()
{
public void onBackStackChanged()
{
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (currentFragment instanceof YOURFRAGMENT) {
currentFragment.onResume();
}
}
};


return result;
}
public abstract class RootFragment extends Fragment implements OnBackPressListener {


@Override
public boolean onBackPressed() {
return new BackPressImpl(this).onBackPressed();
}


public abstract void OnRefreshUI();


}




public class BackPressImpl implements OnBackPressListener {


private Fragment parentFragment;


public BackPressImpl(Fragment parentFragment) {
this.parentFragment = parentFragment;
}


@Override
public boolean onBackPressed() {
((RootFragment) parentFragment).OnRefreshUI();
}
}

and final extent your Frament from RootFragment to see effect

My workaround is to get the current title of the actionbar in the Fragment before setting it to the new title. This way, once the Fragment is popped, I can change back to that title.

@Override
public void onResume() {
super.onResume();
// Get/Backup current title
mTitle = ((ActionBarActivity) getActivity()).getSupportActionBar()
.getTitle();
// Set new title
((ActionBarActivity) getActivity()).getSupportActionBar()
.setTitle(R.string.this_fragment_title);
}


@Override
public void onDestroy() {
// Set title back
((ActionBarActivity) getActivity()).getSupportActionBar()
.setTitle(mTitle);


super.onDestroy();
}

I have used enum FragmentTags to define all my fragment classes.

TAG_FOR_FRAGMENT_A(A.class),
TAG_FOR_FRAGMENT_B(B.class),
TAG_FOR_FRAGMENT_C(C.class)

pass FragmentTags.TAG_FOR_FRAGMENT_A.name() as fragment tag.

and now on

@Override
public void onBackPressed(){
FragmentManager fragmentManager = getFragmentManager();
Fragment current
= fragmentManager.findFragmentById(R.id.fragment_container);
FragmentTags fragmentTag = FragmentTags.valueOf(current.getTag());


switch(fragmentTag){
case TAG_FOR_FRAGMENT_A:
finish();
break;
case TAG_FOR_FRAGMENT_B:
fragmentManager.popBackStack();
break;
case default:
break;
}