如何从BackStack中恢复已有的Fragment

166

我正在学习如何使用碎片。我在类的顶部初始化了三个Fragment实例。我像这样将碎片添加到活动中:

声明和初始化:

Fragment A = new AFragment();
Fragment B = new BFragment();
Fragment C = new CFragment();

替换/添加:

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, A);
ft.addToBackStack(null);
ft.commit();

这些片段正常工作。每个片段都附加到活动,并且保存到后堆栈中没有任何问题。

因此,当我启动AC,然后是B,堆栈看起来像这样:

| |
|B|
|C|
|A|
___

当我按下“返回”按钮时,B被销毁,C被恢复。

但是,当我第二次启动片段A时,它不会从返回堆栈中恢复,而是添加到返回堆栈的顶部。

| |
|A|
|C|
|A|
___

我希望恢复A,并销毁其上的所有片段(如果有)。实际上,我只想要默认的后退堆栈行为。

我该如何完成这个操作?

期望的结果:(A将会被恢复,而顶部的片段将被销毁)

| |
| |
| |
|A|
___

编辑:(由A--C提出建议)

这是我试验的代码:

private void selectItem(int position) {
        Fragment problemSearch = null, problemStatistics = null;
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        String backStateName = null;
        Fragment fragmentName = null;
        boolean fragmentPopped = false;
        switch (position) {
        case 0:
            fragmentName = profile;
            break;
        case 1:
            fragmentName = submissionStatistics;
            break;
        case 2:
            fragmentName = solvedProblemLevel;
            break;
        case 3:
            fragmentName = latestSubmissions;
            break;
        case 4:
            fragmentName = CPExercise;
            break;
        case 5:
            Bundle bundle = new Bundle();
            bundle.putInt("problem_no", problemNo);
            problemSearch = new ProblemWebView();
            problemSearch.setArguments(bundle);
            fragmentName = problemSearch;
            break;
        case 6:
            fragmentName = rankList;
            break;
        case 7:
            fragmentName = liveSubmissions;
            break;
        case 8:
            Bundle bundles = new Bundle();
            bundles.putInt("problem_no", problemNo);
            problemStatistics = new ProblemStatistics();
            problemStatistics.setArguments(bundles);
            fragmentName = problemStatistics;
        default:
            break;
        }
        backStateName = fragmentName.getClass().getName();
        fragmentPopped = manager.popBackStackImmediate(backStateName, 0);
        if (!fragmentPopped) {
            ft.replace(R.id.content_frame, fragmentName);
        }
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        ft.addToBackStack(backStateName);
        ft.commit();

        // I am using drawer layout
        mDrawerList.setItemChecked(position, true);
        setTitle(title[position]);
        mDrawerLayout.closeDrawer(mDrawerList);
    }

问题是,我在启动A然后启动B,按下“返回”按钮后,B被移除并且A被重新启动,第二次按下“返回”按钮应该退出应用程序。但它显示一个空白窗口,我必须按第三次“返回”才能关闭它。

另外,当我启动A,然后启动B,然后启动C,然后再次启动B时......

预期结果:

| |
| |
|B|
|A|
___

实际的:

| |
|B|
|B|
|A|
___

我应该使用自定义覆盖onBackPressed()还是我遗漏了什么?

6个回答

310

阅读文档,可以根据事务名称或commit提供的id的方式弹出回退栈。使用名称可能更容易,因为它不需要跟踪可能会更改的数字,并加强了“唯一回退栈条目”的逻辑。

由于您希望每个Fragment只有一个后退栈条目,因此将后退状态命名为Fragment的类名(通过getClass().getName())。然后,在替换Fragment时,请使用popBackStackImmediate()方法。如果返回true,这意味着在后退栈中存在该Fragment的实例。否则,执行Fragment替换逻辑。

private void replaceFragment (Fragment fragment){
  String backStateName = fragment.getClass().getName();

  FragmentManager manager = getSupportFragmentManager();
  boolean fragmentPopped = manager.popBackStackImmediate (backStateName, 0);

  if (!fragmentPopped){ //fragment not in back stack, create it.
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content_frame, fragment);
    ft.addToBackStack(backStateName);
    ft.commit();
  }
}

编辑

问题在于 - 当我启动A,并启动B,然后按返回按钮时,B被删除,A被恢复。再按一次返回按钮应该退出应用程序。但它显示一个空白窗口,需要再按一次才能关闭它。

这是因为将FragmentTransaction添加到回退堆栈中以确保稍后可以弹出上面的片段。一个快速的解决方法是重写onBackPressed(),如果后退堆栈仅包含1个Fragment,则完成Activity。

@Override
public void onBackPressed(){
  if (getSupportFragmentManager().getBackStackEntryCount() == 1){
    finish();
  }
  else {
    super.onBackPressed();
  }
}

关于重复的后退堆栈条目,您的条件语句会在未被弹出的情况下替换片段,这与我的原始代码片段明显不同。您所做的是无论后退堆栈是否被弹出都将其添加到后退堆栈中。

以下内容更接近您想要的效果:

private void replaceFragment (Fragment fragment){
  String backStateName =  fragment.getClass().getName();
  String fragmentTag = backStateName;

  FragmentManager manager = getSupportFragmentManager();
  boolean fragmentPopped = manager.popBackStackImmediate (backStateName, 0);

  if (!fragmentPopped && manager.findFragmentByTag(fragmentTag) == null){ //fragment not in back stack, create it.
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content_frame, fragment, fragmentTag);
    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    ft.addToBackStack(backStateName);
    ft.commit();
  } 
}

由于在可见时选择相同的片段也会导致重复条目,因此条件语句稍作更改。

实现:

我强烈建议不要像您在代码中所做的那样拆开更新后的replaceFragment()方法。所有逻辑都包含在这个方法中,移动部分可能会导致问题。

这意味着您应该将更新后的replaceFragment()方法复制到您的类中,然后更改。

backStateName = fragmentName.getClass().getName();
fragmentPopped = manager.popBackStackImmediate(backStateName, 0);
if (!fragmentPopped) {
            ft.replace(R.id.content_frame, fragmentName);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(backStateName);
ft.commit();

所以它就是简单的

replaceFragment (fragmentName);

编辑 #2

为了在后退栈更改时更新抽屉,可以创建一个方法,该方法接受一个 Fragment 并比较类名。如果有任何匹配项,则更改标题和选择。还要添加一个 OnBackStackChangedListener,并让它在存在有效的 Fragment 时调用您的更新方法。

例如,在 Activity 的 onCreate() 中添加:

getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {

  @Override
  public void onBackStackChanged() {
    Fragment f = getSupportFragmentManager().findFragmentById(R.id.content_frame);
    if (f != null){
      updateTitleAndDrawer (f);
    }

  }
});

另一种方法:

private void updateTitleAndDrawer (Fragment fragment){
  String fragClassName = fragment.getClass().getName();

  if (fragClassName.equals(A.class.getName())){
    setTitle ("A");
    //set selected item position, etc
  }
  else if (fragClassName.equals(B.class.getName())){
    setTitle ("B");
    //set selected item position, etc
  }
  else if (fragClassName.equals(C.class.getName())){
    setTitle ("C");
    //set selected item position, etc
  }
}

现在,每当后台堆栈发生更改时,标题和所选位置将反映可见的Fragment


谢谢你的回答。 :) 它部分地起作用了。我已经编辑了我的问题并取得了进展。请看一下 ) - Kaidul
2
@RishabhSrivastava 请记住,“pop”通常会销毁最上面的片段。至于哪种方法首先被调用,这取决于 - 请查看片段生命周期。对于您的情况,我建议使用“OnBackstackChangedListener”,以便您可以确保正在处理正确的片段。 - A--C
2
@RishabhSrivastava 这就是为什么我建议使用 OnBackstackChangedListener - A--C
1
@AlexanderFarber 您是正确的。当我回答这个问题时,我没有考虑到 instanceof :) - A--C
2
你的解决方案听起来不错,但是如果你有三个片段并点击它们"A-B-C-B",然后按一次返回键,你会回到"A"而不是回到"C"。这是因为“popBackStackImmediate”不仅弹出给定的标签,还弹出其上面的所有内容。有没有什么解决方法呢? - Raeglan
显示剩余19条评论

14

我认为这个方法可以解决你的问题:

public static void attachFragment ( int fragmentHolderLayoutId, Fragment fragment, Context context, String tag ) {


    FragmentManager manager = ( (AppCompatActivity) context ).getSupportFragmentManager ();
    FragmentTransaction ft = manager.beginTransaction ();

    if (manager.findFragmentByTag ( tag ) == null) { // No fragment in backStack with same tag..
        ft.add ( fragmentHolderLayoutId, fragment, tag );
        ft.addToBackStack ( tag );
        ft.commit ();
    }
    else {
        ft.show ( manager.findFragmentByTag ( tag ) ).commit ();
    }
}

这篇内容最初发布在此问题中。


1
请告诉我,如果像这样回答是否违反规定,我只是想让人们在这篇文章中更容易地看到。谢谢。 - erluxman
1
我不确定这是否违反规定(可能不会),但这绝对是有帮助的。 - Kathir
1
第三行的作用是什么?-> manager.findFragmentByTag(tag); 看起来它好像没有做任何事情。 - Rik van Velzen

3
步骤1:将一个接口实现到你的活动类中。
public class AuthenticatedMainActivity extends Activity implements FragmentManager.OnBackStackChangedListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        .............
        FragmentManager fragmentManager = getFragmentManager();           
        fragmentManager.beginTransaction().add(R.id.frame_container,fragment, "First").addToBackStack(null).commit();
    }

    private void switchFragment(Fragment fragment){            
      FragmentManager fragmentManager = getFragmentManager();
      fragmentManager.beginTransaction()
        .replace(R.id.frame_container, fragment).addToBackStack("Tag").commit();
    }

    @Override
    public void onBackStackChanged() {
    FragmentManager fragmentManager = getFragmentManager();

    System.out.println("@Class: SummaryUser : onBackStackChanged " 
            + fragmentManager.getBackStackEntryCount());

    int count = fragmentManager.getBackStackEntryCount();

    // when a fragment come from another the status will be zero
    if(count == 0){

        System.out.println("again loading user data");

        // reload the page if user saved the profile data

        if(!objPublicDelegate.checkNetworkStatus()){

            objPublicDelegate.showAlertDialog("Warning"
                    , "Please check your internet connection");

        }else {

            objLoadingDialog.show("Refreshing data..."); 

            mNetworkMaster.runUserSummaryAsync();
        }

        // IMPORTANT: remove the current fragment from stack to avoid new instance
        fragmentManager.removeOnBackStackChangedListener(this);

    }// end if
   }       
}

步骤2:当您调用另一个片段时,请添加此方法:
String backStateName = this.getClass().getName();

FragmentManager fragmentManager = getFragmentManager();
fragmentManager.addOnBackStackChangedListener(this); 

Fragment fragmentGraph = new GraphFragment();
Bundle bundle = new Bundle();
bundle.putString("graphTag",  view.getTag().toString());
fragmentGraph.setArguments(bundle);

fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragmentGraph)
.addToBackStack(backStateName)
.commit();

2
我知道回答这个问题有点晚,但是我自己解决了这个问题,认为值得与大家分享。
public void replaceFragment(BaseFragment fragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    final FragmentManager fManager = getSupportFragmentManager();
    BaseFragment fragm = (BaseFragment) fManager.findFragmentByTag(fragment.getFragmentTag());
    transaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);

    if (fragm == null) {  //here fragment is not available in the stack
        transaction.replace(R.id.container, fragment, fragment.getFragmentTag());
        transaction.addToBackStack(fragment.getFragmentTag());
    } else { 
        //fragment was found in the stack , now we can reuse the fragment
        // please do not add in back stack else it will add transaction in back stack
        transaction.replace(R.id.container, fragm, fragm.getFragmentTag()); 
    }
    transaction.commit();
}

在onBackPressed()方法中。
 @Override
public void onBackPressed() {
    if(getSupportFragmentManager().getBackStackEntryCount()>1){
        super.onBackPressed();
    }else{
        finish();
    }
}

1
@X-Black...BaseFragment是应用程序中使用的每个片段的基类。 - Vivek Pratap Singh

0
更简单的解决方案是更改这一行代码: ft.replace(R.id.content_frame, A);ft.add(R.id.content_frame, A); 并且在您的XML布局中,请使用:
  android:background="@color/white"
  android:clickable="true"
  android:focusable="true"

Clickable 表示它可以被指针设备点击或触摸设备轻敲。

Focusable 表示它可以从输入设备(如键盘)获得焦点。像键盘这样的输入设备无法根据输入本身决定将其输入事件发送到哪个视图,因此它们将它们发送到具有焦点的视图。


0
getFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {

    @Override
    public void onBackStackChanged() {

        if(getFragmentManager().getBackStackEntryCount()==0) {
            onResume();
        }    
    }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接