Android之Fragment

不问归期 提交于 2020-03-17 01:15:24

某厂面试归来,发现自己落伍了!>>>

Fragment(碎片)概括:

  • Fragment是依赖于Activity的,不能独立存在的。

  • 一个Activity里可以有多个Fragment。

  • 一个Fragment可以被多个Activity重用。

  • Fragment有自己的生命周期,并能接收输入事件。

  • 我们能在Activity运行时动态地添加或删除Fragment。

优势:

  • 模块化(Modularity):我们不必把所有代码全部写在Activity中,而是把代码写在各自的Fragment中。

  • 可重用(Reusability):多个Activity可以重用一个Fragment。

  • 可适配(Adaptability):根据硬件的屏幕尺寸、屏幕方向,能够方便地实现不同的布局,这样用户体验更好。

核心类:

  • Fragment:Fragment的基类,任何创建的Fragment都需要继承该类。

  • FragmentManager:管理和维护Fragment。他是抽象类,具体的实现类是FragmentManagerImpl。

  • FragmentTransaction:对Fragment的添加、删除等操作都需要通过事务方式进行。他是抽象类,具体的实现类是BackStackRecord。

    Nested Fragment(Fragment内部嵌套Fragment的能力)是Android 4.2提出的,support-fragment库可以兼容到1.6。通getChildFragmentManager()能让e够获得管理子Fragment的FragmentManager,在子Fragment中可以通过getParentFragment()获得父Fragment。

 

基本使用:

创建继承Fragment的类,名为Fragment1:

public class Fragment1 extends Fragment{  

     private static String ARG_PARAM = "param_key"; 
     private String mParam; 
     private Activity mActivity; 

     public static Fragment1 newInstance(String str) {
        Fragment1 frag = new Fragment1();
        Bundle bundle = new Bundle();
        bundle.putString(ARG_PARAM, str);
        fragment.setArguments(bundle);   //设置参数
        return fragment;
    }

     public void onAttach(Context context) {
        mActivity = (Activity) context;
        mParam = getArguments().getString(ARG_PARAM);  //获取参数
    }

     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_1, container, false);
        TextView view = root.findViewById(R.id.text);
        view.setText(mParam);
             return root;
    }    
}

Fragment有很多可以复写的方法,其中最常用的就是onCreateView(),该方法返回Fragment的UI布局,需要注意的是inflate()的第三个参数是false,因为在Fragment内部实现中,会把该布局添加到container中,如果设为true,那么就会重复做两次添加,则会抛出异常 IllegalStateException  

如果在创建Fragment时要传入参数,必须要通过setArguments(Bundle bundle)方式添加,而不建议通过为Fragment添加带参数的构造函数,因为通过setArguments()方式添加,在由于内存紧张导致Fragment被系统杀掉并恢复(re-instantiate)时能保留这些数据。

我们可以在Fragment的onAttach()中通过getArguments()获得传进来的参数,并在之后使用这些参数。如果要获取Activity对象,不建议调用getActivity(),而是在onAttach()中将Context对象强转为Activity对象。

创建完Fragment后,接下来就是把Fragment添加到Activity中。在Activity中添加Fragment的方式有两种:

  • 静态添加:在xml添加Fragment,缺点是一旦添加就不能在运行时删除。

  • 动态添加:运行时添加,这种方式比较灵活,因此建议使用这种方式。

虽然Fragment能在XML中添加,但是这只是一个语法糖而已,Fragment并不是一个View,而是和Activity同一层次的。

这里只给出动态添加的方式。首先Activity需要有一个容器存放Fragment,一般是FrameLayout,因此在Activity的布局文件中加入FrameLayout:

<FrameLayout
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

然后在onCreate()中,通过以下代码将Fragment添加进Activity中。

if (bundle == null) {
    getSupportFragmentManager().beginTransaction()
        .add(R.id.container, Fragment1.newInstance("hello world"), "f1")        //.addToBackStack("fname")
        .commit();
}

这里需要注意几点:

  • 因为我们使用了support库的Fragment,因此需要使用getSupportFragmentManager()获取FragmentManager。

  • add()是对Fragment众多操作中的一种,还有remove(), replace()等,第一个参数是根容器的id(FrameLayout的id,即”@id/container”),第二个参数是Fragment对象,第三个参数是fragment的tag名,指定tag的好处是后续我们可以通过Fragment1 frag = getSupportFragmentManager().findFragmentByTag("f1")从FragmentManager中查找Fragment对象。

  • 在一次事务中,可以做多个操作,比如同时做add().remove().replace()

  • commit()操作是异步的,内部通过mManager.enqueueAction()加入处理队列。对应的同步方法为commitNow()commit()内部会有checkStateLoss()操作,如果开发人员使用不当(比如commit()操作在onSaveInstanceState()之后),可能会抛出异常,而commitAllowingStateLoss()方法则是不会抛出异常版本的commit()方法,但是尽量使用commit(),而不要使用commitAllowingStateLoss()

  • addToBackStack("fname")是可选的。FragmentManager拥有回退栈(BackStack),类似于Activity的任务栈,如果添加了该语句,就把该事务加入回退栈,当用户点击返回按钮,会回退该事务(回退指的是如果事务是add(frag1),那么回退操作就是remove(frag1));如果没添加该语句,用户点击返回按钮会直接销毁Activity。

  • Fragment有一个常见的问题,即Fragment重叠问题,这是由于Fragment被系统杀掉,并重新初始化时再次将fragment加入activity,因此通过在外围加if语句能判断此时是否是被系统杀掉并重新初始化的情况。

生命周期:

  1. onAttach():Fragment和Activity相关联时调用。可以通过该方法获取Activity引用,还可以通过getArguments()获取参数。
  2. onCreate():Fragment被创建时调用。
  3. onCreateView():创建Fragment的布局。
  4. onActivityCreated():当Activity完成onCreate()时调用。
  5. onStart():当Fragment可见时调用。
  6. onResume():当Fragment可见且可交互时调用。
  7. onPause():当Fragment不可交互但可见时调用。
  8. onStop():当Fragment不可见时调用。
  9. onDestroyView():当Fragment的UI从视图结构中移除时调用。
  10. onDestroy():销毁Fragment时调用。
  11. onDetach():当Fragment和Activity解除关联时调用。

上面的方法中,只有onCreateView()在重写时不用写super方法,其他都需要。

Fragment的实现原理和Back Stack:

我们知道Activity有任务栈,用户通过startActivity将Activity加入栈,点击返回按钮将Activity出栈。Fragment也有类似的栈,称为回退栈(Back Stack),回退栈是由FragmentManager管理的。默认情况下,Fragment事务是不会加入回退栈的,如果想将Fragment事务加入回退栈,则可以加入addToBackStack("")。如果没有加入回退栈,则用户点击返回按钮会直接将Activity出栈;如果加入了回退栈,则用户点击返回按钮会回滚Fragment事务。

我们将通过最常见的Fragment用法,讲解Back Stack的实现原理:

getSupportFragmentManager().beginTransaction()
    .add(R.id.container, f1, "f1")
    .addToBackStack("")
    .commit();

上面这个代码的功能就是将Fragment加入Activity中,内部实现为:创建一个BackStackRecord对象,该对象记录了这个事务的全部操作轨迹(这里只做了一次add操作,并且加入回退栈),随后将该对象提交到FragmentManager的执行队列中,等待执行。

BackStackRecord有三重含义:

  • 继承了FragmentTransaction,即是事务,保存了整个事务的全部操作轨迹。

  • 实现了BackStackEntry,作为回退栈的元素,正是因为该类拥有事务全部的操作轨迹,因此在popBackStack()时能回退整个事务。

  • 继承了Runnable,即被放入FragmentManager执行队列,等待被执行。

addToBackStack()对应的是popBackStack(),有以下几种变种:

  • popBackStack():将回退栈的栈顶弹出,并回退该事务。

  • popBackStack(String name, int flag):name为addToBackStack(String name)的参数,通过name能找到回退栈的特定元素,flag可以为0或者FragmentManager.POP_BACK_STACK_INCLUSIVE,0表示只弹出该元素以上的所有元素,POP_BACK_STACK_INCLUSIVE表示弹出包含该元素及以上的所有元素。这里说的弹出所有元素包含回退这些事务。

  • popBackStack()是异步执行的,是丢到主线程的MessageQueue执行,popBackStackImmediate()是同步版本。

Fragment通讯:

Fragment想调用Activity,可通过接口方式;

Activity想调用Fragment,可通过FragmentManager查找并使用。

DialogFragment:

DialogFragment是Android 3.0提出的,代替了Dialog,用于实现对话框。他的优点是:即使旋转屏幕,也能保留对话框状态。

如果要自定义对话框样式,只需要继承DialogFragment,并重写onCreateView(),该方法返回对话框UI。这里我们举个例子,实现进度条样式的圆角对话框。

public class ProgressDialogFragment extends DialogFragment {    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); //消除Title区域
        getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));  //将背景变为透明
        setCancelable(false);  //点击外部不可取消
        View root = inflater.inflate(R.layout.fragment_progress_dialog, container);
        return root;
    }
     public static ProgressDialogFragment newInstance() {
        return new ProgressDialogFragment();
    }
}

Fragment懒加载:

懒加载主要用于ViewPager且每页是Fragment的情况,场景为微信主界面,底部有4个tab,当滑到另一个tab时,先显示”正在加载”,过一会才会显示正常界面。

默认情况,ViewPager会缓存当前页和左右相邻的界面。实现懒加载的主要原因是:用户没进入的界面需要有一系列的网络、数据库等耗资源、耗时的操作,预先做这些数据加载是不必要的。

这里懒加载的实现思路是:用户不可见的界面,只初始化UI,但是不会做任何数据加载。等滑到该页,才会异步做数据加载并更新UI。

这里就实现类似微信那种效果,整个UI布局为:底部用PagerBottomTabStrip(https://github.com/tyzlmjj/PagerBottomTabStrip)项目实现,上面是ViewPager,使用FragmentPagerAdapter。逻辑为:当用户滑到另一个界面,首先会显示正在加载,等数据加载完毕后(这里用睡眠1秒钟代替)显示正常界面。

ViewPager默认缓存左右相邻界面,为了避免不必要的重新数据加载(重复调用onCreateView()),因为有4个tab,因此将离线缓存的半径设置为3,即setOffscreenPageLimit(3)

懒加载主要依赖Fragment的setUserVisibleHint(boolean isVisible)方法,当Fragment变为可见时,会调用setUserVisibleHint(true);当Fragment变为不可见时,会调用setUserVisibleHint(false),且该方法调用时机:

  • onAttach()之前,调用setUserVisibleHint(false)

  • onCreateView()之前,如果该界面为当前页,则调用setUserVisibleHint(true),否则调用setUserVisibleHint(false)

  • 界面变为可见时,调用setUserVisibleHint(true)
    *界面变为不可见时,调用setUserVisibleHint(false)

懒加载Fragment的实现:

public class LazyFragment extends Fragment {

    private View mRootView;
    private boolean mIsInited;
    private boolean mIsPrepared;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mRootView = inflater.inflate(R.layout.fragment_lazy, container, false);
        mIsPrepared = true;
        lazyLoad();
            return mRootView;
    }
            
     public void lazyLoad() {
        if (getUserVisibleHint() && mIsPrepared && !mIsInited) { 
               //异步初始化,在初始化后显示正常UI
            loadData();
        }
    }
               
     private void loadData() {
         new Thread() {
         public void run() {
                //1. 加载数据
                //2. 更新UI
                //3. mIsInited = true
            }
        }.start();
    }   
   
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) { 
           super.setUserVisibleHint(isVisibleToUser);
            if (isVisibleToUser) {
            lazyLoad();
        }
    }
   
    public static LazyFragment newInstance() {
           return new LazyFragment();
    }
}

注意点:

  • 在Fragment中有两个变量控制是否需要做数据加载:

    • mIsPrepared:表示UI是否准备好,因为数据加载后需要更新UI,如果UI还没有inflate,就不需要做数据加载,因为setUserVisibleHint()会在onCreateView()之前调用一次,如果此时调用,UI还没有inflate,因此不能加载数据。

    • mIsInited:表示是否已经做过数据加载,如果做过了就不需要做了。因为setUserVisibleHint(true)在界面可见时都会调用,如果滑到该界面做过数据加载后,滑走,再滑回来,还是会调用setUserVisibleHint(true),此时由于mIsInited=true,因此不会再做一遍数据加载。

  • lazyLoad():懒加载的核心类,在该方法中,只有界面可见(getUserVisibleHint()==true)、UI准备好(mIsPrepared==true)、过去没做过数据加载(mIsInited==false)时,才需要调loadData()做数据加载,数据加载做完后把mIsInited置为true。

保存数据:

@Override
protected void onSaveInstanceState(Bundle outState) {
   outState.putInt("someVar", someVar);
   outState.putString(“text”, tv1.getText().toString());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
   super.onActivityCreated(savedInstanceState);
   someVar = savedInstanceState.getInt("someVar", 0);
   tv.setText(savedInstanceState.getString(“text”));
}

配合ViewPager使用时,保存状态:

    @NonNull
    @Override
    public Fragment getItem(int position) {
        return fragments != null && fragments.size() > 0 ? fragments.get(position) : null;
    }

    @Override
    public int getCount() {
        return fragments != null ? fragments.size() : 0;
    }

    /**
     * 重写以下两个方法,使得Fragment在切换时保持原来的状态
     */
    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        fragmentManager.beginTransaction().show(fragment).commit();
        return fragment;
//        return super.instantiateItem(container, position);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
//        super.destroyItem(container, position, object);
        Fragment fragment = fragments.get(position);
        fragmentManager.beginTransaction().hide(fragment).commit();
    }

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!