文章目录
点击下载源码
1.Fragment的产生与介绍
Android运行在各种各样的设备中,有小屏幕的手机,超大屏的平板甚至电视。针对屏幕尺寸的差距,很多情况下,都是先针对手机开发一套App,然后拷贝一份,修改布局以适应平板神马超级大屏的。难道无法做到一个App可以同时适应手机和平板么,当然了,必须有啊。Fragment的出现就是为了解决这样的问题。你可以把Fragment当成Activity的一个界面的一个组成部分,甚至Activity的界面可以完全有不同的Fragment组成,更帅气的是Fragment拥有自己的生命周期和接收、处理用户的事件,这样就不必在Activity写一堆控件的事件处理的代码了。更为重要的是,你可以动态的添加、替换和移除某个Fragment。
2.Fragment的生命周期
Fragment
必须依赖Activity
而存在,因此Activity的生命周期会直接影响到Fragment的生命周期。官网这张图很好的说明了两者生命周期的关系:
![](https://img-blog.csdnimg.cn/20181109174145925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hneTQxMw==,size_16,color_FFFFFF,t_70)
可以看到Fragment比Activity多了几个额外的生命周期回调方法:
1.onAttach(Activity)
:当Fragment
与Activity
发生关联时调用。
2.onCreateView(LayoutInflater, ViewGroup,Bundle)
:创建该Fragment
的视图。
3.onActivityCreated(Bundle)
:当Activity
的onCreate
方法返回时调用。
4.onDestoryView()
:与onCreateView
想对应,当该Fragment
的视图被移除时调用。
5.onDetach()
:与onAttach
相对应,当Fragment
与Activity
关联被取消时调用。
注意:除了
onCreateView
,其他的所有方法如果你重写了,必须调用父类对于该方法的实现。
3.静态的使用Fragment
这是使用Fragment
最简单的一种方式,把Fragment
当成普通的控件,直接写在Activity
的布局文件中。步骤:
1、继承Fragment
,重写onCreateView
决定Fragemnt
的布局。
2、在Activity
中声明此Fragment
,就当和普通的View
一样。
app 示例
下面展示一个例子,2个Fragment作为Activity布局的子控件,TitleFragment
用于标题布局,ContentFragment
用于内容布局。
TitleFragment的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#000000">
<ImageButton
android:id="@+id/id_title_left_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="我不是微信"
android:textColor="#fff"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
TitleFragment:
public class TitleFragment extends Fragment {
private ImageButton mLeftButton;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_title, container, false);
mLeftButton = (ImageButton) view.findViewById(R.id.id_title_left_btn);
mLeftButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getActivity(), "i am an ImageButton in TitleFragment !",
Toast.LENGTH_LONG).show();
}
});
return view;
}
}
ContentFragment的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<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=".fragment.ContentFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="使用Fragment做主面板"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
ContentFragment:
public class ContentFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_content, container, false);
}
}
MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
}
}
Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<fragment
android:id="@+id/id_fragment_title"
android:name="xvideodemo.duowan.com.fragmentdemo.fragment.TitleFragment"
android:layout_width="match_parent"
android:layout_height="45dp"/>
<fragment
android:layout_below="@id/id_fragment_title"
android:id="@+id/id_fragment_content"
android:name="xvideodemo.duowan.com.fragmentdemo.fragment.ContentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout >
是不是把Fragment
当成普通的View
一样声明在Activity
的布局文件中,然后所有控件的事件处理等代码都由各自的Fragment
去处理,瞬间觉得Activity
好干净有木有,代码的可读性、复用性以及可维护性是不是瞬间提升了,下面看下效果图:
![](https://img-blog.csdnimg.cn/20181110210721884.gif)
4.动态的使用Fragment
上面已经演示了,最简单的使用Fragment
的方式, 下面介绍如何动态的添加、更新、以及删除Fragment
。
app1 示例
为了动态使用Fragment
,我们修改一下Actvity
的布局文件,中间使用一个FrameLayout
,下面添加二个按钮。
切换按钮,动态切换加载WeixinFragment
或FriendFragment
。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<fragment
android:id="@+id/id_fragment_title"
android:layout_width="match_parent"
android:layout_height="45dp"
android:name="com.example.app1.fragment.TitleFragment"/>
<include
android:id="@+id/id_ly_bottombar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
layout="@layout/bottom_bar" />
<FrameLayout
android:id="@+id/id_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/id_ly_bottombar"
android:layout_below="@id/id_fragment_title" />
</RelativeLayout >
bottom_bar.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ly_main_tab_bottom"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:background="@drawable/bottom_bar" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp" >
<!--beforeDescendants:viewgroup会优先其子类控件而获取到焦点-->
<LinearLayout
android:id="@+id/tab_bottom_weixin"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:descendantFocusability="beforeDescendants"
android:gravity="center"
android:orientation="vertical" >
<ImageButton
android:id="@+id/btn_tab_bottom_weixin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:clickable="false"
android:src="@drawable/tab_weixin_pressed" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="微信"
android:textColor="#ffffff"
android:textSize="12dp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/tab_bottom_friend"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:descendantFocusability="beforeDescendants"
android:gravity="center"
android:orientation="vertical" >
<ImageButton
android:id="@+id/btn_tab_bottom_friend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000"
android:clickable="false"
android:src="@drawable/tab_find_frd_normal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="朋友"
android:textColor="#ffffff"
android:textSize="12dp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
WeixinFrament:
public class WeixinFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_weixin, container, false);
}
}
fragment_weixin.xml:
<?xml version="1.0" encoding="utf-8"?>
<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">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="this is Weixin Fragment!"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
FriendFragment
和WeixinFrament
类似,看效果图就明白了。
MainActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private LinearLayout mTabWeixin;
private LinearLayout mTabFriend;
private WeixinFragment mWeixin;
private FriendFragment mFriend;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
// 初始化控件和声明事件
mTabWeixin = (LinearLayout) findViewById(R.id.tab_bottom_weixin);
mTabFriend = (LinearLayout) findViewById(R.id.tab_bottom_friend);
mTabWeixin.setOnClickListener(this);
mTabFriend.setOnClickListener(this);
// 设置默认的Fragment
setDefaultFragment();
}
private void setDefaultFragment() {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
mWeixin = new WeixinFragment();
transaction.replace(R.id.id_content, mWeixin);
transaction.commit();
}
@Override
public void onClick(View v) {
FragmentManager fm = getSupportFragmentManager();
// 开启Fragment事务
FragmentTransaction transaction = fm.beginTransaction();
switch (v.getId()){
case R.id.tab_bottom_weixin:
if (mWeixin == null){
mWeixin = new WeixinFragment();
}
// 使用当前Fragment的布局替代id_content的控件
transaction.replace(R.id.id_content, mWeixin);
break;
case R.id.tab_bottom_friend:
if (mFriend == null)
{
mFriend = new FriendFragment();
}
transaction.replace(R.id.id_content, mFriend);
break;
}
// 事务提交
transaction.commit();
}
}
可以看到我们使用FragmentManager
对Fragment
进行了动态的加载,这里使用的是replace
方法。运行效果如下:
![](https://img-blog.csdnimg.cn/20181110223930137.gif)
FrameLayout
(帧布局)是六大布局中最为简单的一个布局,这个布局直接在屏幕上开辟出一块空白的区域,当我们往里面添加控件的时候,会默认把它们放到这块区域的左上角,但是我们也可以通过layout_gravity
属性,指定到其他的位置,这种布局方式没有任何的定位方式,所以它应用的场景并不多。
5.Fragment家族常用的API
Fragment
常用的三个类:
android.(support.v4.)app.Fragment
主要用于定义Fragment
。android.(support.v4.)app.FragmentManager
主要用于在Activity
中操作Fragment
。android.(support.v4.)app.FragmentTransaction
保证一些Fragment
操作的原子性,熟悉事务这个词,一定能明白。
.support.v4.app和.app的区别
.support.v4.app
兼容的最低版本是android:minSdkVersion="4"
即1.6版,.app
是android:minSdkVersion="11"
即3.0版。.support.v4.app
在Activity
的布局中是可以使用<fragment>
标签的,.app
不可以使用。.support.v4.app
使用getSupportFragmentManager()
获取FragmentManager
,.app
使用getFragmentManager()
获取。- 导入
.support.v4.app
可以使用implementation 'com.android.support:support-v4:XX.0.0'
。
FragmentTransaction的方法
1.FragmentTransaction transaction = fm.benginTransatcion();
: 开启一个事务。
2.transaction.add()
: 往Activity
中添加一个Fragment
,并在内部根据Add
进去的先后顺序形成了一个链表。
3.transaction.remove()
:从Activity
中移除一个Fragment
,如果被移除的Fragment
没有添加到回退栈(回退栈后面会详细解释),这个Fragment
实例将会被销毁。
4.transaction.replace()
: 实际上就是remove()
然后add()
的合体。
5.transaction.hide()
:隐藏当前的Fragment
,仅仅是设为不可见,并不会销毁。
6.transaction.show()
:显示之前隐藏的Fragment
。
7.detach()
: 会将fragment
的view
从UI中移除,和remove()
不同, 此fragment
实例并不会删除,此时fragment
的状态依然由FragmentManager
维护。
8.attach()
: 利用fragment
的onCreateView()
来重建fragment
的view
视图,并将此fragment
添加到Add
链表中,这里最值得注意的地方在这里:由于是将fragment
添加到Add
链表,所以只能添加到列头,所以attach()
操作的结果是,最新操作的页面始终显示在最前面。
9.transatcion.commit()
: 提交一个事务。
10.add()
和replace()
不能共用。https://blog.csdn.net/liaoqianchuan00/article/details/24327959
commit方法一定要在Activity.onSaveInstance()之前调用
。不然,可能遇到Activity
状态不一致:State loss这样的错误。
上述,基本是操作Fragment
的所有方式了,在一个事务开启到提交可以进行多次的添加、移除、替换等操作。
示例:
a. 你在FragmentA中的EditText填了一些数据,当切换到FragmentB时,如果希望回到FragmentA还能看到数据,则适合你的就是hide
和show
, 当然了不要使劲在那new新的实例。
b.你不希望保留用户操作,你可以使用remove
,然后add
, 或者使用replace
, replace
=remove
+add
。
c.remove
和detach
有一点细微的区别,在不考虑回退栈的情况下,remove
会销毁整个Fragment
实例,而detach
则只是销毁其视图结构,实例并不会被销毁。那么二者怎么取舍使用呢?如果你的当前Activity
一直存在,那么在不希望保留用户操作的时候,你可以优先使用detach
。
6.管理Fragment回退栈
类似于Android系统为Activity
维护一个任务栈,我们也可以通过Activity
维护一个回退栈来保存每次Fragment
事务发生的变化。如果你将Fragment
任务添加到回退栈,当用户点击后退按钮时,将看到上一次的保存的Fragment
。一旦Fragment
完全从后退栈中弹出,用户再次点击后退键,则退出当前Activity
。
看这样一个效果图:
![](https://img-blog.csdnimg.cn/20181111200357271.gif)
点击第一个按钮,切换到第二个界面,点击第二个按钮,切换到第三个界面,然后点击Back键依次回退。这像不像初学Android时的Activity跳转,当然了,这里肯定不是,不然我就跪了。这里是Fragment实现的,用户点击Back,实际是Fragment
回退栈不断的弹栈。
如何添加一个Fragment事务到回退栈:
FragmentTransaction.addToBackStack(String)
app2 示例
下面讲解代码:很明显一共是3个Fragment和一个Activity.
先看Activity的布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<FrameLayout
android:id="@+id/id_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</RelativeLayout>
不同的Fragment
就在这个FrameLayout
中显示。
MainActivity.java:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
getSupportFragmentManager()
.beginTransaction()
.add(R.id.id_content, new FragmentOne(), "ONE")
.commit();
}
}
很简单,直接将FragmentOne
添加到布局文件中的FrameLayout
中,注意这里并没有调用FragmentTransaction.addToBackStack(String)
,因为我不喜欢在当前显示时,点击Back键出现白板。而是正确的相应Back键,即退出我们的Activity
.
下面是FragmentOne.java:
public class FragmentOne extends Fragment implements View.OnClickListener {
private Button mBtn;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
mBtn = view.findViewById(R.id.id_fragment_one_btn);
mBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
getFragmentManager()
.beginTransaction()
.replace(R.id.id_content, new FragmentTwo(), "TWO")
.addToBackStack(null)
.commit();
}
}
我们在点击FragmentOne
中的按钮时,使用了replace
方法,replace
是remove
和add
的合体,并且如果不添加事务到回退栈,前一个Fragment
实例会被销毁。这里很明显,我们调用tx.addToBackStack(null)
;将当前的事务添加到了回退栈,所以FragmentOne
实例不会被销毁,但是视图层次依然会被销毁,即会调用onDestoryView
和onCreateView
。
证据就是:仔细看上面的效果图,我们在跳转前在文本框输入的文本change
,在用户Back得到第一个界面的时候不见了。
接下来FragmentTwo.java:
public class FragmentTwo extends Fragment implements View.OnClickListener {
private Button mBtn;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_two, container, false);
mBtn = view.findViewById(R.id.id_fragment_two_btn);
mBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
getFragmentManager()
.beginTransaction()
.hide(this)
.add(R.id.id_content, new FragmentThree(), "THREE")
//.replace(R.id.id_content, new FragmentThree(), "THREE")
.addToBackStack(null)
.commit();
}
}
这里点击时,我们没有使用replace
,而是先隐藏了当前的Fragment
,然后添加了FragmentThree
的实例,最后将事务添加到回退栈。这样做的目的是为了给大家提供一种方案:如果不希望视图重绘该怎么做,请再次仔细看效果图,我们在FragmentTwo
的EditText
填写的内容,用户Back回来时,数据还在。
最后FragmentThree就是简单的Toast了:
public class FragmentThree extends Fragment implements View.OnClickListener {
private Button mBtn;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_three, container, false);
mBtn = view.findViewById(R.id.id_fragment_three_btn);
mBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
Toast.makeText(getActivity(), "i am a btn in Fragment three", Toast.LENGTH_SHORT).show();
}
}
好了,经过上面的介绍,应该已经知道Fragment
回退栈是怎么一回事了,以及hide
,replace
等各自的应用的场景。
这里极其注意一点:上面的整体代码不具有任何参考价值,纯粹为了显示回退栈,在后面讲解了Fragment
与Activity
通信以后,会重构上面的代码!
7.Fragment与Activity通信
因为所有的Fragment
都是依附于Activity
的,所以通信起来并不复杂,大概归纳为:
- 如果
Activity
中包含自己管理的Fragment
的引用,可以通过引用直接访问所有的Fragment
的public方法。 - 如果
Activity
中未保存任何Fragment
的引用,那么没关系,每个Fragment
都有唯一的TAG
或者ID
,可以通过getFragmentManager.findFragmentByTag()
或者findFragmentById()
获得任何Fragment
实例,然后进行操作。 - 在
Fragment
中可以通过getActivity()
得到当前绑定的Activity
的实例,然后进行操作。 - 如果在
Fragment
中需要Context
,可以通过调用getActivity()
,如果该Context
需要在Activity
被销毁后还存在,则使用getActivity().getApplicationContext()
。
app3 示例
因为要考虑Fragment
的重复使用,所以必须降低Fragment
与Activity
的耦合,而且Fragment
更不应该直接操作别的Fragment
,毕竟Fragment
操作应该由它的管理者Activity
来决定。
下面我通过两种方式的代码,分别重构,FragmentOne和FragmentTwo的点击事件,以及Activity对点击事件的响应:
首先看FragmentOne.java:
public class FragmentOne extends Fragment implements View.OnClickListener {
private Button mBtn;
/**
* 设置按钮点击的回调
*/
public interface FOneBtnClickListener{
void onFOneBtnClick();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
mBtn = view.findViewById(R.id.id_fragment_one_btn);
mBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
if (getActivity() instanceof FOneBtnClickListener){
((FOneBtnClickListener)getActivity()).onFOneBtnClick();
}
}
}
可以看到现在的FragmentOne
不和任何Activity
耦合,任何Activity
都可以使用;并且我们声明了一个接口,来回调其点击事件,想要管理其点击事件的Activity
实现此接口就即可。可以看到我们在onClick
中首先判断了当前绑定的Activity
是否实现了该接口,如果实现了则调用。
再看FragmentTwo.java
public class FragmentTwo extends Fragment implements View.OnClickListener {
private Button mBtn;
private FTwoBtnClickListener fTwoBtnClickListener;
public interface FTwoBtnClickListener{
void onTwoBtnClickListener();
}
public void setfTwoBtnClickListener(FTwoBtnClickListener fTwoBtnClickListener){
this.fTwoBtnClickListener = fTwoBtnClickListener;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_two, container, false);
mBtn = view.findViewById(R.id.id_fragment_two_btn);
mBtn.setOnClickListener(this);
return view;
}
@Override
public void onClick(View v) {
if (fTwoBtnClickListener != null){
fTwoBtnClickListener.onTwoBtnClickListener();
}
}
}
与FragmentOne
极其类似,但是我们提供了setListener
这样的方法,意味着Activity
不仅需要实现该接口,还必须显示调用mFTwo.setfTwoBtnClickListener(this)
。
最后看Activity :
public class MainActivity extends AppCompatActivity implements FragmentOne.FOneBtnClickListener,FragmentTwo.FTwoBtnClickListener {
private FragmentOne mFOne;
private FragmentTwo mFTwo;
private FragmentThree mFThree;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
getSupportFragmentManager()
.beginTransaction()
.add(R.id.id_content, mFOne, "ONE")
.commit();
}
/**
* 按钮点击时的回调
*/
@Override
public void onFOneBtnClick() {
if (mFTwo == null){
mFTwo = new FragmentTwo();
mFTwo.setfTwoBtnClickListener(this);
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.id_content,mFTwo, "TWO")
.addToBackStack(null)
.commit();
}
@Override
public void onTwoBtnClickListener() {
if (mFThree == null){
mFThree = new FragmentThree();
}
getSupportFragmentManager()
.beginTransaction()
.hide(mFTwo)
.add(R.id.id_content, mFThree, "THREE")
//.replace(R.id.id_content, new FragmentThree(), "THREE")
.addToBackStack(null)
.commit();
}
}
代码重构结束,与开始的效果一模一样。上面两种通信方式都是值得推荐的,随便选择一种自己喜欢的。这里再提一下:虽然Fragment
和Activity
可以通过getActivity
与findFragmentByTag
或者findFragmentById
,进行任何操作,甚至在Fragment
里面操作另外的Fragment
,但是没有特殊理由是绝对不提倡的。Activity
担任的是Fragment
间类似总线一样的角色,应当由它决定Fragment
如何操作。另外虽然Fragment
不能响应Intent
打开,但是Activity
可以,Activity
可以接收Intent
,然后根据参数判断显示哪个Fragment
。
8.如何处理运行时配置发生变化
运行时配置发生变化,最常见的就是屏幕发生旋转,这里提一下:很多人觉得强制设置屏幕的方向就可以了,但是有一点,当你的应用被置于后台(例如用户点击了home),长时间没有返回的时候,你的应用也会被重新启动。比如上例:如果你把上面的例子你置于FragmentThree
界面,然后处于后台状态,长时间后你会发现当你再次通过home
打开时,上面FragmentThree
与FragmentOne
叠加在一起,这就是因为你的Activity
重新启动,在原来的FragmentThree
上又绘制了一个FragmentOne
。
app4示例
Activity:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private FragmentOne mFOne;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e(TAG, "onCreate: " + savedInstanceState);
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
mFOne = new FragmentOne();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.id_content, mFOne, "ONE");
transaction.commit();
}
}
Fragment:
public class FragmentOne extends Fragment{
private static final String TAG = "FragmentOne";
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(TAG, "onCreateView");
View view = inflater.inflate(R.layout.fragment_one, container, false);
return view;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.e(TAG, "onDestroyView");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy");
}
}
很简单的代码,当你运行之后,不断的旋转屏幕,你会发现每旋转一次屏幕,屏幕上就多了一个FragmentOne
的实例,并且后台log会打印出许多套生命周期的回调。
11-13 10:20:08.597 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onCreate
11-13 10:20:08.597 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onCreateView
11-13 10:20:11.871 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onDestroyView
11-13 10:20:11.873 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onDestroy
11-13 10:20:11.905 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onCreate
11-13 10:20:11.927 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onCreateView
11-13 10:20:11.934 1146-1146/xvideodemo.duowan.com.app4 E/FragmentOne: onCreate
这是为什么呢,因为当屏幕发生旋转,Activity
发生重新启动,默认的Activity
中的Fragment
也会跟着Activity
重新创建。这样造成当旋转的时候,本身存在的Fragment
会重新启动,然后当执行Activity
的onCreate
时,又会再次实例化一个新的Fragment
,这就是出现的原因。
那么如何解决呢:
其实通过检查onCreate
的参数Bundle savedInstanceState
就可以判断,当前是否发生Activity
的重新创建:
默认的savedInstanceState会存储一些数据,包括Fragment的实例:通过打印可以看出:
11-13 10:26:15.020 2068-2068/xvideodemo.duowan.com.app4 E/MainActivity: onCreate: Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState$1@e66690c, 2131165193=android.view.AbsSavedState$1@e66690c, 2131165204=android.view.AbsSavedState$1@e66690c, 2131165252=android.view.AbsSavedState$1@e66690c}}], android:support:fragments=android.support.v4.app.FragmentManagerState@e49255, android:fragments=android.app.FragmentManagerState@8a6646a}]
所以,我们简单改一下代码,只有在savedInstanceState==null时,才进行创建Fragment实例:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private FragmentOne mFOne;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e(TAG, "onCreate: " + savedInstanceState);
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
mFOne = new FragmentOne();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.id_content, mFOne, "ONE");
transaction.commit();
}
}
}
现在无论进行多次旋转都只会有一个Fragment
实例在Activity
中。
现在还存在一个问题,就是重新绘制时,Fragment
发生重建,原本的数据如何保持?
其实和Activity
类似,Fragment
也有onSaveInstanceState
的方法,在此方法中进行保存数据,然后在onCreate
或者onCreateView
或者onActivityCreated
进行恢复都可以。
9.Fragmeny与ActionBar和MenuItem集成
Fragment
可以添加自己的MenuItem
到Activity
的ActionBar
或者可选菜单中。
- 在
Fragment
的onCreate
中调用setHasOptionsMenu(true);
。 - 然后在
Fragment
子类中实现onCreateOptionsMenu
。 - 如果希望在
Fragment
中处理MenuItem
的点击,也可以实现onOptionsItemSelected
;当然了Activity
也可以直接处理该MenuItem
的点击事件。
app5示例
Fragment:
public class FragmentOne extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one, container, false);
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu1, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.setting:
Toast.makeText(getActivity(), "FragmentMenu1", Toast.LENGTH_SHORT).show();
break;
}
return true;
}
}
Activity:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private FragmentOne mFOne;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e(TAG, "onCreate: " + savedInstanceState);
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
mFOne = new FragmentOne();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.id_content, mFOne, "ONE");
transaction.commit();
}
}
}
注意要加载menu需要toolbar或actionbar,这里就直接用Activity中actionbar了,也就是在styles.xml中声明
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
menu:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/setting"
android:orderInCategory="3"
android:title="Setting">
<!--设置子菜单-->
<menu>
<item
android:title="Setting1">
</item>
<item
android:title="Setting2">
</item>
</menu>
</item>
<item
android:orderInCategory="2"
android:title="Other">
</item>
</menu>
运行效果:
![](https://img-blog.csdnimg.cn/20181113112703888.gif)
10.没有布局的Fragment的作用
没有布局文件Fragment
实际上是为了保存,当Activity
重启时,保存大量数据准备的。
众所周知,Activity
在不明确指定屏幕方向和configChanges
时,当用户旋转屏幕会重新启动。当然了,应对这种情况,Android给出了几种方案:
- 如果是少量数据,可以通过
onSaveInstanceState()
和onRestoreInstanceState()
进行保存与恢复。 - 如果是大量数据,使用
Fragment
保持需要恢复的对象。 - 自已处理配置变化。
难点
假设当前Activity
在onCreate
中启动一个异步线程去夹在数据,当然为了给用户一个很好的体验,会有一个ProgressDialog
,当数据加载完成,ProgressDialog
消失,设置数据。
这里,如果在异步数据完成加载之后,旋转屏幕,使用上述1、2两种方法都不会很难,无非是保存数据和恢复数据。
但是,如果正在线程加载的时候,进行旋转,会存在以下问题:
a. 此时数据没有完成加载,onCreate
重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。
b. 关闭ProgressDialog
的代码在线程的onPostExecutez
中,但是上个线程如果已经杀死,无法关闭之前ProgressDialog
。
c. 谷歌的官方不建议使用ProgressDialog
,这里我们会使用官方推荐的DialogFragment
来创建我的加载框.这样,其实给我们带来一个很大的问题,DialogFragment
说白了是Fragment
,和当前的Activity
的生命周期会发生绑定,我们旋转屏幕会造成Activity
的销毁,当然也会对DialogFragment
造成影响。
下面我将使用几个例子,分别使用上面的3种方式,和如何最好的解决上述的问题。
app6
1.使用onSaveInstanceState()和onRestoreInstanceState()进行数据保存与恢复
代码:
public class SavedInstanceStateUsingActivity extends ListActivity {
private static final String TAG = "MainActivity";
private ListAdapter mAdapter;
private ArrayList<String> mDatas;
private DialogFragment mLoadingDialog;
private LoadDataAsyncTask mLoadDataAsyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
initData(savedInstanceState);
}
// 初始化数据
private void initData(Bundle savedInstanceState){
if (null != savedInstanceState){
mDatas = savedInstanceState.getStringArrayList("mDatas");
}
if (mDatas == null){
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(getFragmentManager(), "LoadingDialog");
mLoadDataAsyncTask = new LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
}else{
initAdapter();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.e(TAG, "onSaveInstanceState");
outState.putSerializable("mDatas", mDatas);
}
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
Log.e(TAG, "onRestoreInstanceState");
}
@Override
protected void onDestroy()
{
Log.e(TAG, "onDestroy");
super.onDestroy();
}
//----------------------------------------------------------------------------------------------
//
private void initAdapter() {
mAdapter = new ArrayAdapter<String>(
SavedInstanceStateUsingActivity.this,
android.R.layout.simple_list_item_1, mDatas
);
try {
setListAdapter(mAdapter);
} catch (Exception e) {
}
}
//模拟耗时操作
private ArrayList<String> generateTimeConsumingDatas() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
"onSaveInstanceState保存数据",
"getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
"Spark"));
}
private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
mDatas = generateTimeConsumingDatas();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
try {
mLoadingDialog.dismiss();
} catch (Exception e) {
}
initAdapter();
}
}
}
界面为一个ListView
,onCreate
中启动一个异步任务去加载数据,这里使用Thread.sleep
模拟了一个耗时操作;当用户旋转屏幕发生重新启动时,会onSaveInstanceState
中进行数据的存储,在onCreate
中对数据进行恢复,免去了不必要的再加载一遍。
当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:
11-13 07:25:42.334 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onCreate
11-13 07:25:45.975 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onSaveInstanceState
11-13 07:25:45.978 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onDestroy
11-13 07:25:45.988 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onCreate
11-13 07:25:45.996 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onRestoreInstanceState
11-13 07:25:48.219 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onSaveInstanceState
11-13 07:25:48.224 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onDestroy
11-13 07:25:48.233 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onCreate
11-13 07:25:48.244 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onRestoreInstanceState
11-13 07:25:50.488 25762-25762/xvideodemo.duowan.com.app6 E/MainActivity: onSaveInstanceState
验证我们的确是重新启动了,但是我们没有再次去进行数据加载。
如果在加载的时候,进行旋转,则会发生错误,所以在两处加了try-catch
。
运行效果:
![](https://img-blog.csdnimg.cn/20181113152731635.gif)
2.使用Fragment来保存对象,用于恢复数据
如果重新启动Activity
需要恢复大量的数据,重新建立网络连接,或者执行其他的密集型操作,这样因为配置发生变化而完全重新启动可能会是一个慢的用户体验。并且,使用系统提供的onSaveIntanceState()
的回调中,使用Bundle
来完全恢复Activity
的状态可能是不现实的(Bundle
不是设计用来携带大量数据的(例如bitmap
),并且Bundle
中的数据必须能够被序列化和反序列化),这样会消耗大量的内存和导致配置变化缓慢。在这样的情况下,当你的Activity
因为配置发生改变而重启,你可以通过保持一个Fragment
来缓解重新启动带来的负担。这个Fragment
可以包含你想要保持的有状态的对象的引用。
当Android系统因为配置变化关闭你的Activity
的时候,你的Activity
中被标识保持的fragments
不会被销毁。你可以在你的Activity
中添加这样的fragements
来保存有状态的对象。
在运行时配置发生变化时,在Fragment
中保存有状态的对象:
a. 继承Fragment
,声明引用指向你的有状态的对象。
b.当Fragment
创建时调用setRetainInstance(boolean)
。保存 Fragment
实例。
c.把Fragment
实例添加到Activity
中。
d.当Activity
重新启动后,使用FragmentManager
对Fragment
进行恢复。
代码:
Fragment:
public class RetainedFragment extends Fragment {
// data object we want to retain
private Bitmap data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public Bitmap getData() {
return data;
}
public void setData(Bitmap data) {
this.data = data;
}
}
比较简单,只需要声明需要保存的数据对象,然后提供getter
和setter
,注意,一定要在onCreate
调用setRetainInstance(true);
然后是:FragmentRetainDataActivity:
public class FragmentRetainDataActivity extends Activity {
private static final String TAG = "FragmentRetainDataActivity";
private RetainedFragment dataFragment;
private DialogFragment mLoadingDialog;
private ImageView mImageView;
private Bitmap mBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_retaindata);
Log.e(TAG, "onCreate");
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (RetainedFragment)fm.findFragmentByTag("data");
// create the fragment and data the first time
if (dataFragment == null){
// add the fragment
dataFragment = new RetainedFragment();
fm.beginTransaction().add(dataFragment, "data").commit();
}
mBitmap = dataFragment.getData();
initData();
}
// 初始化数据
private void initData(){
mImageView = (ImageView) findViewById(R.id.id_imageView);
if (mBitmap == null)
{
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(getFragmentManager(), "LOADING_DIALOG");
RequestQueue newRequestQueue = Volley
.newRequestQueue(FragmentRetainDataActivity.this);
ImageRequest imageRequest = new ImageRequest(
"https://img-my.csdn.net/uploads/201407/18/1405652589_5125.jpg",
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
mBitmap = response;
mImageView.setImageBitmap(mBitmap);
// load the data from the web
dataFragment.setData(mBitmap);
try {
mLoadingDialog.dismiss();
} catch (Exception e) {
}
}
}, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
Log.e(TAG, "onErrorResponse: ");
}
});
newRequestQueue.add(imageRequest);
} else
{
mImageView.setImageBitmap(mBitmap);
}
}
@Override
public void onDestroy()
{
Log.e(TAG, "onDestroy");
super.onDestroy();
// store the data in the fragment
dataFragment.setData(mBitmap);
}
}
注意,因为图片是从网络下载的,所以在AndroidManifest.xml中增加权限:
<uses-permission android:name="android.permission.INTERNET" />
这里在onCreate
使用了Volley
去加载 了一张美女照片,然后在onDestroy
中对Bitmap
进行存储,在onCreate
添加一个或者恢复一个Fragment
的引用,然后对Bitmap
进行读取和设置。这种方式适用于比较大的数据的存储与恢复。
运行效果:
![](https://img-blog.csdnimg.cn/20181113163820552.gif)
3.配置configChanges,自己对屏幕旋转的变化进行处理
这里关键点是在AndroidManifest.xml中设置:
<activity
android:name=".ConfigChangesTestActivity"
android:configChanges="screenSize|orientation" >
</activity>
android:configChanges
指定在某种系统配置改变时不重新加载activity
,如果我们没有在Activity
的configChanges
属性中指定以下选项的话,当配置发生改变的时候都会导致重新创建Activity
(注意API版本)。
属性值 | 含义 |
---|---|
mcc | SIM卡唯一标识IMSI(国际移动用户标识码)中的国家代码,由三位数字组成,中国为:460 这里标识mcc代码发生了改变 |
mnc | SIM卡唯一标识IMSI(国际移动用户标识码)中的运营商代码,有两位数字组成,中国移动TD系统为00,中国联通为01,电信为03,此项标识mnc发生了改变 |
locale | 设备的本地位置发生了改变,一般指的是切换了系统语言 |
touchscreen | 触摸屏发生了改变 |
keyboard | 键盘类型发生了改变,比如用户使用了外接键盘 |
keyboardHidden | 键盘的可访问性发生了改变,比如用户调出了键盘 |
navigation | 系统导航方式发生了改变 |
screenLayout | 屏幕布局发生了改变,很可能是用户激活了另外一个显示设备 |
fontScale | 系统字体缩放比例发生了改变,比如用户选择了个新的字号 |
uiMode | 用户界面模式发生了改变,比如开启夜间模式-API8新添加 |
orientation | 屏幕方向发生改变,比如旋转了手机屏幕 |
screenSize | 当屏幕尺寸信息发生改变(当编译选项中的minSdkVersion和targeSdkVersion均低于13时不会导致Activity重启)-API13新添加 |
smallestScreenSize | 设备的物理屏幕尺寸发生改变,这个和屏幕方向没关系,比如切换到外部显示设备-API13新添加 |
layoutDirection | 当布局方向发生改变的时候,正常情况下无法修改布局的layoutDirection的属性-API17新添加 |
ConfigChangesTestActivity:
public class ConfigChangesTestActivity extends ListActivity {
private static final String TAG = "MainActivity";
private ListAdapter mAdapter;
private ArrayList<String> mDatas;
private DialogFragment mLoadingDialog;
private LoadDataAsyncTask mLoadDataAsyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
initData(savedInstanceState);
}
private void initData(Bundle savedInstanceState) {
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(getFragmentManager(), "LoadingDialog");
mLoadDataAsyncTask = new LoadDataAsyncTask();
mLoadDataAsyncTask.execute();
}
//当配置发生变化时,不会重新启动Activity。但是会回调此方法,用户自行进行对屏幕旋转后进行处理
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
{
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT)
{
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}
//----------------------------------------------------------------------------------------------
//
private void initAdapter() {
mAdapter = new ArrayAdapter<String>(
ConfigChangesTestActivity.this,
android.R.layout.simple_list_item_1, mDatas
);
try {
setListAdapter(mAdapter);
} catch (Exception e) {
}
}
//模拟耗时操作
private ArrayList<String> generateTimeConsumingDatas() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
"onSaveInstanceState保存数据",
"getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
"Spark"));
}
private class LoadDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
mDatas = generateTimeConsumingDatas();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
try {
mLoadingDialog.dismiss();
} catch (Exception e) {
}
initAdapter();
}
}
}
对第一种方式的代码进行了修改,去掉了保存与恢复的代码,重写了onConfigurationChanged
;此时,无论用户何时旋转屏幕都不会重新启动Activity
,并且onConfigurationChanged
中的代码可以得到调用。从效果图可以看到,无论如何旋转不会重启Activity
。
![](https://img-blog.csdnimg.cn/20181113172904231.gif)
4.旋转屏幕的最佳实践
下面要开始今天的难点了,就是处理文章开始时所说的,当异步任务在执行时,进行旋转,如果解决上面的问题。
首先说一下探索过程:
起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要及时的在onDestroy
里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是activity
是一定会被销毁的,对话框的dismiss
也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。
下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:
当然我们还使用Fragment
进行数据保存,毕竟这是官方推荐的:
OtherRetainedFragment:
public class OtherRetainedFragment extends Fragment {
// data object we want to retain
// 保存一个异步的任务
private MyAsyncTask data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyAsyncTask data)
{
this.data = data;
}
public MyAsyncTask getData()
{
return data;
}
}
和上面的差别不大,唯一不同的就是它要保存的对象变成一个异步的任务了,相信看到这,已经知道解决上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private FixProblemsActivity activity;
// 是否完成
private boolean isCompleted;
//进度框
private LoadingDialog mLoadingDialog;
private List<String> items;
public MyAsyncTask(FixProblemsActivity activity)
{
this.activity = activity;
}
// 开始时,显示加载框
@Override
protected void onPreExecute() {
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(activity.getFragmentManager(), "LOADING");
}
// 加载数据
@Override
protected Void doInBackground(Void... voids) {
items = loadingData();
return null;
}
// 加载完成回调当前的Activity
@Override
protected void onPostExecute(Void aVoid) {
isCompleted = true;
notifyActivityTaskCompleted();
if (mLoadingDialog != null)
mLoadingDialog.dismiss();
}
// 设置Activity,因为Activity会一直变化
public void setActivity(FixProblemsActivity activity)
{
// 如果上一个Activity销毁,将与上一个Activity绑定的DialogFragment销毁
if (activity == null)
{
mLoadingDialog.dismiss();
}
// 设置为当前的Activity
this.activity = activity;
// 开启一个与当前Activity绑定的等待框
if (activity != null && !isCompleted)
{
mLoadingDialog = new LoadingDialog();
mLoadingDialog.show(activity.getFragmentManager(), "LOADING");
}
// 如果完成,通知Activity
if (isCompleted)
{
notifyActivityTaskCompleted();
}
}
public List<String> getItems()
{
return items;
}
private List<String> loadingData()
{
try
{
Thread.sleep(5000);
} catch (InterruptedException e)
{
}
return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
"onSaveInstanceState保存数据",
"getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
"Spark"));
}
private void notifyActivityTaskCompleted()
{
if (null != activity)
{
activity.onTaskCompleted();
}
}
}
异步任务中,管理一个对话框,当开始下载前,进度框显示,下载结束进度框消失,并为Activity
提供回调。当然了,运行过程中Activity
不断的重启,我们也提供了setActivity
方法,onDestory
时,会setActivity(null)
防止内存泄漏,同时我们也会关闭与其绑定的加载框;当onCreate
传入新的Activity
时,我们会在再次打开一个加载框,当然了因为屏幕的旋转并不影响加载的数据,所有后台的数据一直继续在加载。是不是很完美~~
主Activity:
public class FixProblemsActivity extends ListActivity {
private static final String TAG = "MainActivity";
private ListAdapter mAdapter;
private List<String> mDatas;
private OtherRetainedFragment dataFragment;
private MyAsyncTask mMyTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG, "onCreate");
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (OtherRetainedFragment) fm.findFragmentByTag("data");
// create the fragment and data the first time
if (dataFragment == null)
{
// add the fragment
dataFragment = new OtherRetainedFragment();
fm.beginTransaction().add(dataFragment, "data").commit();
}
mMyTask = dataFragment.getData();
if (mMyTask != null)
{
mMyTask.setActivity(this);
} else
{
mMyTask = new MyAsyncTask(this);
dataFragment.setData(mMyTask);
mMyTask.execute();
}
// the data is available in dataFragment.getData()
}
@Override
protected void onRestoreInstanceState(Bundle state)
{
super.onRestoreInstanceState(state);
Log.e(TAG, "onRestoreInstanceState");
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
mMyTask.setActivity(null);
super.onSaveInstanceState(outState);
Log.e(TAG, "onSaveInstanceState");
}
@Override
protected void onDestroy()
{
Log.e(TAG, "onDestroy");
super.onDestroy();
}
/**
* 回调
*/
public void onTaskCompleted()
{
mDatas = mMyTask.getItems();
mAdapter = new ArrayAdapter<String>(FixProblemsActivity.this,
android.R.layout.simple_list_item_1, mDatas);
setListAdapter(mAdapter);
}
}
在onCreate
中,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setActivity(this)
;
在onSaveInstanceState
把当前任务加入Fragment
。
效果图类似1,就不贴了。
参考:
https://blog.csdn.net/lmj623565791/article/details/37970961
https://blog.csdn.net/lmj623565791/article/details/37936275