Android解决多个Fragment切换时布局重新实例化问题

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fan7983377/article/details/51889269


本文借鉴自:http://www.jianshu.com/p/d9143a92ad94

至于fragment的使用就不多说了,直奔主题, 


布局文件:

<LinearLayout 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"
    android:orientation="vertical"
    tools:context="fan.fragmentdemo.MainActivity">

    //导航栏
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="第一个"
            android:id="@+id/tv_one"
            android:gravity="center"
            />

        <TextView
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#EEE"
            />

        <TextView
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="第二个"
            android:id="@+id/tv_two"
            android:gravity="center"
            />

        <TextView
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#EEE"
            />

        <TextView
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"
            android:text="第三个"
            android:id="@+id/tv_three"
            android:gravity="center"
            />

    </LinearLayout>

    //内容区域
    <FrameLayout
        android:id="@+id/content"
        android:background="#EEE"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />


</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64


布局预览图: 


 


以前写多个fragment切换是经常使用这种方法切换fragment: 


/**
  * 使用replace切换页面
  * 显示fragment
  */
 private void showFragment(Fragment fg){

     FragmentTransaction transaction = fragmentManager.beginTransaction();
     transaction.replace(R.id.content, fg);
     transaction.commit();

 }
1
2
3
4
5
6
7
8
9
10
11


replace():该方法只是在上一个Fragment不再需要时采用的简便方法,弊端就是如果需要重复使用该fragment时,需要每次都要重新加载一次。比如我在第一个fragment输入信息后,切换第二个fragment后再切换回去,就会造成数据丢失,如下:

而且,如果每切换一次就实例化一次的话,FragmentManager管理下的栈也会爆满,最终会导致手机卡顿,这很明显不是正确的Fragment使用姿势,这时,我们就需要使用show()、hide()、add()了,正确的切换方式是add(),切换时hide(),add()另一个Fragment;再次切换时,只需hide()当前,show()另一个就行了,代码修改如下: 


/**
  * 使用show() hide()切换页面
  * 显示fragment
  */
 private void showFragment(Fragment fg){

     FragmentTransaction transaction = fragmentManager.beginTransaction();

     //如果之前没有添加过
     if(!fg.isAdded()){
         transaction
                 .hide(currentFragment)
                 .add(R.id.content,fg);
     }else{
         transaction
                 .hide(currentFragment)
                 .show(fg);
     }

    //全局变量,记录当前显示的fragment
     currentFragment = fg;

     transaction.commit();

 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


效果:

有上图,可以看出,即使切换到别的fragment,再切换回来,数据还依然存在,这就避免了Fragment切换时布局重新实例化。 


安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)

在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate方法调用后紧接着恢复 


当我们不退出软件,只是后台挂着去干别的事,当系统内存不足回收我们这个app时,再切换回来,app的这几个Fragment界面会重叠。,如下图: 


由上图可以看出,三个fragment全部叠在了一起,而且点击上面菜单也不能消除重叠。显然,这并不是我们想要的,没事,继续解决问题,使用findFragmentByTag:即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。,修改如下: 


/**
  * 使用show() hide()切换页面
  * 显示fragment
  */
 private void showFragment(Fragment fg){

     FragmentTransaction transaction = fragmentManager.beginTransaction();

     //如果之前没有添加过
     if(!fg.isAdded()){
         transaction
                 .hide(currentFragment)
                 .add(R.id.content,fg,fg.getClass().getName());  //第三个参数为当前的fragment绑定一个tag,tag为当前绑定fragment的类名
     }else{
         transaction
                 .hide(currentFragment)
                 .show(fg);
     }

     currentFragment = fg;

     transaction.commit();

 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


别急,还没完,在当前Activity的onCreate()方法里面添加一下代码: 


if (savedInstanceState != null) { // “内存重启”时调用

   //从fragmentManager里面找到fragment
   fgOne = (OneFragment) fragmentManager.findFragmentByTag(OneFragment.class.getName());
   fgTwo = (TwoFragment) fragmentManager.findFragmentByTag(TwoFragment.class.getName());
   fgThree = (ThreeFragment) fragmentManager.findFragmentByTag(ThreeFragment.class.getName());

   //解决重叠问题show里面可以指定恢复的页面
   fragmentManager.beginTransaction()
           .show(fgOne)
           .hide(fgTwo)
           .hide(fgThree)
           .commit();

   //把当前显示的fragment记录下来
   currentFragment = fgOne;

}else{      //正常启动时调用

   fgOne = new OneFragment();
   fgTwo = new TwoFragment();
   fgThree = new ThreeFragment();

   showFragment(fgOne);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


OK,当app后台时遇到“内存重启”的情况下,再返回我们的app,就会恢复到show(fgOne)页面,而且还不会造成重叠问题! 


很显然,这样结束是不道德的,因为有人会问了,如果想记录当前退出的状态以至于下次恢复时直接显示之前的fragment页面怎么办,恩,对于这个问题,我们可以在activity的onSaveInstanceState()方法中记录一下“内存重启”之前的Fragment的页面,然后在oncreate()中取出来,根据保存的页面来显示到指定的fragment,代码如下:

@Override
protected void onSaveInstanceState(Bundle outState) {

    //“内存重启”时保存当前的fragment名字
    outState.putString(STATE_FRAGMENT_SHOW,currentFragment.getClass().getName());
    super.onSaveInstanceState(outState);
}
1
2
3
4
5
6
7


然后在oncreate()方法中添加(修改上面的那个代码) 


if (savedInstanceState != null) { // “内存重启”时调用

    //获取“内存重启”时保存的fragment名字
    String saveName = savedInstanceState.getString(STATE_FRAGMENT_SHOW);

    //从fragmentManager里面找到fragment
    fgOne = (OneFragment) fragmentManager.findFragmentByTag(OneFragment.class.getName());
    fgTwo = (TwoFragment) fragmentManager.findFragmentByTag(TwoFragment.class.getName());
    fgThree = (ThreeFragment) fragmentManager.findFragmentByTag(ThreeFragment.class.getName());

    //如果为空就默认操作
    if(TextUtils.isEmpty(saveName)){
        //解决重叠问题
        fragmentManager.beginTransaction()
                .show(fgOne)
                .hide(fgTwo)
                .hide(fgThree)
                .commit();

        //把当前显示的fragment记录下来
        currentFragment = fgOne;

    }else{

        if(saveName.equals(fgOne.getClass().getName())){    //如果推出之前是OneFragment

            //解决重叠问题
            fragmentManager.beginTransaction()
                    .show(fgOne)
                    .hide(fgTwo)
                    .hide(fgThree)
                    .commit();

            //把当前显示的fragment记录下来
            currentFragment = fgOne;

        }else if(saveName.equals(fgTwo.getClass().getName())){  //如果推出之前是TwoFragment

            //解决重叠问题
            fragmentManager.beginTransaction()
                    .show(fgTwo)
                    .hide(fgOne)
                    .hide(fgThree)
                    .commit();

            //把当前显示的fragment记录下来
            currentFragment = fgTwo;

        }else{    //如果推出之前是ThreeFragment

            //解决重叠问题
            fragmentManager.beginTransaction()
                    .show(fgThree)
                    .hide(fgTwo)
                    .hide(fgOne)
                    .commit();

            //把当前显示的fragment记录下来
            currentFragment = fgThree;

        }

    }


}else{      //正常启动时调用

    fgOne = new OneFragment();
    fgTwo = new TwoFragment();
    fgThree = new ThreeFragment();

    showFragment(fgOne);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73


OK,这样就可以了,我们通过保存当前显示的fragment的类名,当我们在第二个fragment页面时后台,等到“内存重启”后返回该app时,就根据之前保存的类名来判断加载指定的fragment,而且,重叠的问题也解决了! 


本章代码及apk:点击免费下载

当然,有网友问了,如果fragment比较多,那么多if多麻烦,是啊,上面的代码主要就是让大家理解一下思路,具体开发不建议那么写,下面,就说一下上面的代码该如何精简吧:

创建一个List用来存所有的fragment,给fragment设置tag可以使用当前的索引index,然后在onSaveInstanceState中保存当前的索引,在恢复时通过索引来找到fragment

全部代码修改精简后如下:::

//当前显示的fragment
private static final String CURRENT_FRAGMENT = "STATE_FRAGMENT_SHOW";

private TextView tvone;
private TextView tvtwo;
private TextView tvthree;
private FragmentManager fragmentManager;

private Fragment currentFragment = new Fragment();
private List<Fragment> fragments = new ArrayList<>();

private int currentIndex = 0;


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.tvthree = (TextView) findViewById(R.id.tv_three);
    this.tvtwo = (TextView) findViewById(R.id.tv_two);
    this.tvone = (TextView) findViewById(R.id.tv_one);

    fragmentManager = getSupportFragmentManager();

    tvthree.setOnClickListener(this);
    tvtwo.setOnClickListener(this);
    tvone.setOnClickListener(this);


    if (savedInstanceState != null) { // “内存重启”时调用

        //获取“内存重启”时保存的索引下标
        currentIndex = savedInstanceState.getInt(CURRENT_FRAGMENT,0);

        //注意,添加顺序要跟下面添加的顺序一样!!!!
        fragments.removeAll(fragments);
        fragments.add(fragmentManager.findFragmentByTag(0+""));
        fragments.add(fragmentManager.findFragmentByTag(1+""));
        fragments.add(fragmentManager.findFragmentByTag(2+""));

        //恢复fragment页面
        restoreFragment();


    }else{      //正常启动时调用

        fragments.add(new OneFragment());
        fragments.add(new TwoFragment());
        fragments.add(new ThreeFragment());

        showFragment();
    }

}

@Override
protected void onSaveInstanceState(Bundle outState) {

    //“内存重启”时保存当前的fragment名字
    outState.putInt(CURRENT_FRAGMENT,currentIndex);
    super.onSaveInstanceState(outState);
}

@Override
public void onClick(View v) {

    switch (v.getId()){

        case R.id.tv_one:

            currentIndex = 0;

            break;
        case R.id.tv_two:

            currentIndex = 1;

            break;
        case R.id.tv_three:

            currentIndex = 2;

            break;

    }

    showFragment();

}


/**
 * 使用show() hide()切换页面
 * 显示fragment
 */
private void showFragment(){

    FragmentTransaction transaction = fragmentManager.beginTransaction();

    //如果之前没有添加过
    if(!fragments.get(currentIndex).isAdded()){
        transaction
                .hide(currentFragment)
                .add(R.id.content,fragments.get(currentIndex),""+currentIndex);  //第三个参数为添加当前的fragment时绑定一个tag

    }else{
        transaction
                .hide(currentFragment)
                .show(fragments.get(currentIndex));
    }

    currentFragment = fragments.get(currentIndex);

    transaction.commit();

}

/**
 * 恢复fragment
 */
private void restoreFragment(){


    FragmentTransaction mBeginTreansaction = fragmentManager.beginTransaction();

    for (int i = 0; i < fragments.size(); i++) {

        if(i == currentIndex){
            mBeginTreansaction.show(fragments.get(i));
        }else{
            mBeginTreansaction.hide(fragments.get(i));
        }

    }

    mBeginTreansaction.commit();

    //把当前显示的fragment记录下来
    currentFragment = fragments.get(currentIndex);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142


精简后的代码下载:点击下载

最后在说一点:

getActivity()空指针 
可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。

大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。 
比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。

解决办法: 
更”安全”的方法:(对于Fragment已经onDetach这种情况,我们应该避免在这之后再去调用宿主Activity对象,比如取消这些异步任务,但我们的团队可能会有粗心大意的情况,所以下面给出的这个方案会保证安全)

在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是相比空指针闪退,这种做法“安全”些),即:

protected Activity mActivity;
@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    this.mActivity = activity;
}

/**
*  如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替
*/
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    this.mActivity = (Activity)context;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
不过!
上面的方法是我摘抄别人的,个人更推荐的写法是activity在销毁时让fragment也跟着销毁,需要在activity重写onSaveInstanceState方法,把super给注释掉:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        /**
         * 使fragment跟着activity一起回收
         */
//        super.onSaveInstanceState(outState);
    }
--------------------- 
版权声明:本文为CSDN博主「F-Fan」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/fan7983377/article/details/51889269

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值